ProxioDocs
API Reference

Webhooks

Receive events from Proxio instead of polling, as a signed JSON envelope or delivered to Discord or Slack. Full event catalog, payload shape, an X-Proxio-Signature verification walkthrough in Python and Node.js (t=,v1= scheme with 300s tolerance, JSON format only), retry and backoff, auto-disable, secret rotation, and a test endpoint.

Webhooks push events to your server so you can react to orders, usage thresholds, and expirations without polling. An endpoint delivers in one of three formats: a signed JSON envelope (the default), a Discord embed, or a Slack message. JSON deliveries are signed with HMAC-SHA256, so you can prove they came from Proxio.

Scopes: read to list, write to create, update, delete, rotate the secret, and test.

Event catalog

Events differ in how quickly they reach you. Most are emitted inline, at the moment the change commits, treat those as real time. The rest are state crossings that are detected by a periodic check: most of those lag by up to about 15 minutes between the condition becoming true and the delivery arriving, though two that first have to wait on an external payment or cleanup step before the condition is even knowable lag longer, see their Timing cell and the callout below the table.

EventFires whenTiming
order.paidAn order transitions to paid (new, top-up, or renewal).Immediate
order.failedAn order reaches a terminal, unpaid state (expired or canceled).Checked periodically, up to about 75 minutes
service.createdA new service is provisioned.Immediate
service.renewedA renewal order finishes fulfilling (top-up applied or expiry moved).Checked periodically, up to about 15 minutes
service.expiring_soonA package's expiry is within 7, 3, or 1 days.Checked periodically, up to about 15 minutes
service.expiredA package passed its expiry.Checked periodically, up to about 15 minutes
usage.threshold_reachedA metered package crosses 80% or 95% used.Checked periodically, up to about 15 minutes
credential.createdA new credential is created on a service.Immediate
credential.rotatedA credential's password is rotated.Immediate
whitelist.changedAn IP whitelist binding is added to or removed from a credential.Immediate
wallet.low_balanceWallet balance drops below the low-balance threshold.Checked periodically, up to about 15 minutes
wallet.topup_completedA wallet top-up is confirmed and the balance moves.Immediate
wallet.topup_failedA wallet top-up will not complete; no balance moved.Checked periodically, up to about 45 minutes

Every event above is subscribable. webhook.test is not: it is delivered only when you call the test endpoint, and passing it in events returns VALIDATION_ERROR with details: [{ "field": "events", "issue": "unknown" }]. Your handler should still recognize the webhook.test type on the wire.

Don't build a timer on the periodic events

If your logic needs to act the instant a package expires or a threshold is crossed, poll GET /services/{id}/usage or GET /services/{id} instead. The periodic events are reliable but not prompt. order.paid, service.created, credential.created, credential.rotated, whitelist.changed, and wallet.topup_completed are the ones you can treat as real time.

order.failed and wallet.topup_failed run behind the rest

Don't assume a flat 15 minutes across every periodic event. order.failed and wallet.topup_failed genuinely take longer end to end, budget the windows in the table above for those two specifically, and treat every other periodic row's ~15 minutes as the one you can rely on elsewhere.

The catalog is closed for v1; new event types are additive, so ignore any type you don't recognize.

Payload

Every event shares one envelope: id, type, created_at, api_version, and an event-specific data object.

{
  "id": "evt_7Ke2jP4mQ",
  "type": "usage.threshold_reached",
  "created_at": "2026-07-17T09:30:00.000Z",
  "api_version": "1",
  "data": {
    "service_id": "clpkg_2a9x",
    "threshold": 80,
    "used":  { "bytes": "40000000000", "bytes_num": 40000000000, "gigabytes": 40 },
    "limit": { "bytes": "50000000000", "bytes_num": 50000000000, "gigabytes": 50 }
  }
}

Each delivery is POSTed to your URL with Content-Type: application/json and these headers:

HeaderExampleMeaning
X-Proxio-Signaturet=1752745800,v1=5d41…Timestamp and HMAC signature. JSON format only.
X-Proxio-Eventusage.threshold_reachedThe event type.
X-Proxio-Deliverycldlv_3k9vThe delivery id, for support and dedup.

The envelope above is the JSON delivery format. Discord and Slack endpoints receive the same event reshaped for their platform (see Delivery formats) and are not signed, they carry only the X-Proxio-Event and X-Proxio-Delivery headers.

Event payloads

data is event-specific. Where a payload embeds a resource (order, service), it matches the list shape of the corresponding REST endpoint (GET /services, not the connection-carrying GET /services/{id} detail), so a service here has limit / remaining rather than a connection block.

order.paid

{ "order": {
  "id": "clord_9f2a", "status": "PAID", "category": "RESIDENTIAL",
  "is_unlimited": false, "quantity_gb": 50, "ip_quantity": null,
  "subtotal": "125.00", "discount": "12.50", "total": "112.50",
  "paid_via": "WALLET", "service_id": "clpkg_2a9x",
  "created_at": "2026-07-17T09:30:00.000Z"
} }

This event can arrive in two different shapes

order.paid is normally emitted the instant the order transitions to PAID, with the full order object above. It can also arrive up to 15 minutes later with a minimal { "order": { "id": "clord_9f2a" } } payload instead. At most one of the two reaches you, but you cannot predict which shape it will be. The only field you can rely on is order.id; if you need the rest, re-read it with GET /orders/{id}.

order.failed

{ "order": { "id": "clord_5h8k", "status": "EXPIRED" }, "reason": "payment_expired" }

status is EXPIRED (left unpaid too long) or CANCELED, and reason matches it one-to-one: "payment_expired" for EXPIRED, "canceled" for CANCELED. A PENDING order whose most recent payment attempt failed is not order.failed, the order itself can still be paid, only a terminal, unpaid order fires this. Re-read GET /orders/{id} for the full order.

service.created

{ "service": {
  "id": "clpkg_2a9x", "category": "RESIDENTIAL", "product_key": "RESIDENTIAL",
  "is_unlimited": false, "status": "active",
  "limit": { "bytes": "50000000000", "bytes_num": 50000000000, "gigabytes": 50 },
  "remaining": { "bytes": "50000000000", "bytes_num": 50000000000, "gigabytes": 50 },
  "expires_at": "2026-08-16T09:30:00.000Z", "auto_renewal_enabled": false,
  "created_at": "2026-07-17T09:30:00.000Z"
} }

Same caveat as order.paid: this can also arrive up to 15 minutes later with a minimal { "service": { "id": "clpkg_2a9x", "category": "RESIDENTIAL" } } payload instead. Only service.id is guaranteed; re-read GET /services/{id} for the rest.

service.renewed

{ "service_id": "clpkg_2a9x", "order_id": "clord_7c1d", "expires_at": "2026-09-01T00:00:00.000Z" }

Fires once a renewal (or an auto-renewal cycle) has actually applied, a metered top-up or an expiry extension. expires_at is the service's expiry at that point, moved forward for a day-priced extend, unchanged for a metered top-up that only added data, or null if the service carries no expiry at all. order_id is the renewal order; re-read GET /orders/{id} for the amount charged.

service.expiring_soon

{ "service_id": "clpkg_2a9x", "expires_at": "2026-08-16T09:30:00.000Z", "days_remaining": 3 }

days_remaining is a bucket, not an exact count: 7, 3, or 1, whichever window the expiry fell into when the event fired.

service.expired

{ "service_id": "clpkg_2a9x", "expires_at": "2026-07-17T09:30:00.000Z" }

usage.threshold_reached

{
  "service_id": "clpkg_2a9x",
  "threshold": 80,
  "used":  { "bytes": "40000000000", "bytes_num": 40000000000, "gigabytes": 40 },
  "limit": { "bytes": "50000000000", "bytes_num": 50000000000, "gigabytes": 50 }
}

threshold is always exactly 80 or 95, never anything in between; a metered package can fire both, once each, in the same billing cycle as usage climbs past each line.

credential.created

{
  "service_id": "clpkg_2a9x",
  "credential": {
    "id": "clsub_9m4p", "label": "scraper-a", "username": "k7p2q1m9x3ab",
    "created_at": "2026-07-17T09:30:00.000Z"
  }
}

Fires for a credential created through POST /services/{id}/credentials, never for the primary credential a service is provisioned with (that's covered by service.created).

credential.rotated

{
  "service_id": "clpkg_2a9x",
  "credential": { "id": "clsub_9m4p", "username": "k7p2q1m9x3ab" },
  "rotated_at": "2026-08-20T14:02:00.000Z"
}

The new password is never in this payload

credential.rotated tells a subscriber that a password changed, not what it changed to. The plaintext password is shown exactly once, in the rotate-password response itself, and a webhook body is a copy whose destination you don't control, so it never carries a credential secret. If your system needs the new password, read it from the API response that triggered the rotation.

whitelist.changed

{
  "service_id": "clpkg_2a9x",
  "credential_id": "clsub_7h2k",
  "action": "added",
  "binding": { "id": "clbind_3k9v", "ip": "203.0.113.5" }
}

action is "added" or "removed", matching a binding created through the whitelist endpoints or deleted from them.

wallet.low_balance

{ "currency": "USD", "balance": "4.32" }

Fires when the wallet balance drops below the account's low-balance threshold ($5 by default), at most once per UTC calendar day while it stays below that line, not on every check.

wallet.topup_completed

{
  "topup": {
    "id": "cltop_4n7q3x", "status": "completed", "amount": "25.00",
    "currency": "USD", "payment_method": "card",
    "created_at": "2026-08-20T14:00:00.000Z"
  },
  "wallet": { "currency": "USD", "balance": "67.50" }
}

This is how a pipeline learns it can spend: POST /wallet/topups only opens a payment link, the balance moves when the provider confirms, and this event is that confirmation. wallet.balance is the account's balance at the moment this event fired, not "the balance this top-up produced", those are the same number unless something else moved the balance in between. Re-read GET /wallet/topups/{id} if you need the top-up's own record.

wallet.topup_failed

{
  "topup": {
    "id": "cltop_8w3f2p", "status": "failed", "amount": "25.00",
    "currency": "USD", "payment_method": "crypto",
    "created_at": "2026-08-20T13:30:00.000Z"
  },
  "reason": "payment_failed",
  "retryable": true
}

No balance moved. retryable: true doesn't mean this same top-up will change state again, a failed top-up is a dead end, it means the failure is always recoverable the same way: open a new POST /wallet/topups.

webhook.test

{ "message": "This is a test event from Proxio. If you can verify its signature, your endpoint is ready." }

Only ever produced by the test endpoint; see there for details.

Delivery formats

An endpoint's format decides how each event is delivered. It defaults to json, and every format delivers to a single url.

FormatDeliverySigned
jsonThe signed envelope above, POSTed to your url.Yes (X-Proxio-Signature)
discordA Discord embed (title, per-field data, timestamp, footer with the event id) POSTed to a Discord incoming-webhook url.No
slackA Slack message (text fallback plus Block Kit blocks: a header with the event name, the event fields, and a context line with the event id) POSTed to a Slack incoming-webhook url.No
  • json is the default and the only signed format. It also has a rotatable signing secret.
  • discord requires url to be a Discord incoming-webhook URL (on discord.com, discordapp.com, or the ptb/canary subdomains, with a /api/webhooks/… path). Pasting a Discord URL in the dashboard auto-suggests this format.
  • slack requires url to be a Slack incoming-webhook URL, the https://hooks.slack.com/services/… address Slack gives you when you add an Incoming Webhook to a channel. Pasting one in the dashboard auto-suggests this format. Anything else returns VALIDATION_ERROR with details: [{ "field": "url", "issue": "not_slack" }].

Verifying the signature

Signature verification applies to the JSON format only. Discord and Slack deliveries are authenticated by the secret in the incoming-webhook URL itself and carry no X-Proxio-Signature.

The X-Proxio-Signature header uses a Stripe-style scheme: t= is the Unix timestamp the request was signed, and v1= is the hex HMAC-SHA256 of the signed payload. To verify:

  1. Parse t and v1 from the header.
  2. Build the signed message "{t}.{rawRequestBody}", the raw bytes exactly as received, not a re-serialized copy.
  3. Compute HMAC_SHA256(secret, signedMessage) with your webhook's whsec_… secret and compare it to v1 in constant time.
  4. Reject the request if the timestamp is more than 300 seconds from now (replay protection).

Sign the raw body

HMAC is over the exact bytes Proxio sent. Read the raw request body before any JSON parsing or framework re-serialization, otherwise whitespace or key-order changes will break the signature.

import hashlib
import hmac
import time


def verify_signature(secret: str, signature_header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    # signature_header looks like: "t=1752745800,v1=5d41..."
    parts = dict(item.split("=", 1) for item in signature_header.split(","))
    timestamp = int(parts["t"])

    # 1. Replay protection: reject stale timestamps.
    if abs(time.time() - timestamp) > tolerance:
        raise ValueError("Timestamp outside tolerance")

    # 2. Recompute the HMAC over "{t}.{rawBody}".
    signed_message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed_message, hashlib.sha256).hexdigest()

    # 3. Constant-time compare.
    if not hmac.compare_digest(expected, parts["v1"]):
        raise ValueError("Signature mismatch")
    return True


# Flask example:
# @app.post("/webhooks/proxio")
# def handler():
#     verify_signature(WHSEC, request.headers["X-Proxio-Signature"], request.get_data())
#     event = request.get_json()
#     ...
#     return "", 200
import crypto from "node:crypto"

function verifySignature(secret, signatureHeader, rawBody, tolerance = 300) {
  // signatureHeader looks like: "t=1752745800,v1=5d41..."
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((item) => item.split("=", 2)),
  )
  const timestamp = Number(parts.t)

  // 1. Replay protection: reject stale timestamps.
  if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) {
    throw new Error("Timestamp outside tolerance")
  }

  // 2. Recompute the HMAC over "{t}.{rawBody}".
  const signedMessage = `${timestamp}.` + rawBody
  const expected = crypto.createHmac("sha256", secret).update(signedMessage).digest("hex")

  // 3. Constant-time compare.
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error("Signature mismatch")
  }
  return true
}

// Express example (raw body required):
// app.post("/webhooks/proxio", express.raw({ type: "application/json" }), (req, res) => {
//   verifySignature(WHSEC, req.header("X-Proxio-Signature"), req.body.toString("utf8"))
//   const event = JSON.parse(req.body.toString("utf8"))
//   res.sendStatus(200)
// })

Respond 2xx quickly to acknowledge a delivery. Do the heavy work asynchronously, a slow handler counts as a failure and triggers a retry.

Retries and backoff

If your endpoint doesn't return 2xx within the 10-second delivery timeout, the delivery is retried with exponential backoff. There are 6 attempts total: the initial attempt plus 5 retries.

AttemptDelay before it
1immediate
2~10s
3~20s
4~40s
5~80s
6~160s

The retries span roughly 5 minutes; after the 6th attempt fails the delivery is marked failed.

Delivery is at-least-once

Plan for duplicates. An attempt is retried whenever your endpoint doesn't answer 2xx inside the timeout, and a response your server produced but never got back to us (a timeout after your handler already committed, a connection reset, a 500 from a proxy in front of you) is indistinguishable from a genuine failure. The same event can therefore be delivered to you more than once.

What is deduplicated is emission, not delivery: the same underlying state crossing detected twice, such as a periodic check re-observing a threshold that already fired, will not create a second delivery. That guarantee does not extend to the retry loop.

Make your handler idempotent. Key on the event id (evt_…) in the payload, or on the X-Proxio-Delivery header, record which ones you've processed, and make a repeat a no-op. Respond 2xx before doing slow work so a late acknowledgement doesn't trigger an avoidable retry.

Dead endpoints auto-disable

After 20 consecutive failed deliveries, the endpoint is automatically disabled (enabled: false) to stop hammering a dead URL. A single success resets the failure counter. Re-enable a disabled endpoint with a PATCH once it's healthy again.

Manage endpoints

Also in the dashboard

Webhooks can also be managed in the dashboard under Settings → Webhooks: create an endpoint with a format select and its contextual fields, pick events, enable or disable, reveal the secret once or rotate it (json), send a test, delete, and review recent deliveries.

List

GET /webhooks returns your endpoints (cursor-paginated), never including the secret.

curl https://dashboard.proxio.net/api/v1/webhooks \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.get(
    f"{BASE}/webhooks",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=15,
)
resp.raise_for_status()
print(resp.json()["data"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/webhooks`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
})
console.log((await res.json()).data)

Create

POST /webhooks. Subscribe to events and pick a delivery format. For a json endpoint the response includes the signing secret once, store it now (discord and slack endpoints don't sign, so no secret is returned).

Body

FieldTypeNotes
eventsstring[]Required. Event types to subscribe to.
formatstringjson (default), discord, or slack.
urlstringRequired for every format (HTTPS, must resolve to a public address). For discord it must be a Discord incoming-webhook URL; for slack a Slack incoming-webhook URL.
enabledbooleanDefaults to true.

Validation failures return VALIDATION_ERROR with details[].field/issue: a missing or malformed field reports issue: "invalid_type", a url that isn't the right shape for the chosen format reports "not_discord" or "not_slack", and an unrecognized event type reports "unknown".

url is also run through an SSRF guard, both when you create or update the endpoint and again right before every delivery (so a URL that resolves somewhere safe today but gets DNS-rebound later is still caught). A rejected URL comes back the same way, field: "url", with one of these issue codes:

issueMeaning
INVALID_URLNot a parseable URL at all.
NOT_HTTPSThe scheme isn't https://.
FORBIDDEN_HOSTThe host is localhost or ends in .internal / .local.
FORBIDDEN_IPThe host, or an address it resolves to, is private, loopback, link-local, CGNAT, or otherwise non-routable.
DNS_FAILEDThe host didn't resolve.
curl -X POST https://dashboard.proxio.net/api/v1/webhooks \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "url": "https://example.com/webhooks/proxio", "events": ["order.paid", "usage.threshold_reached"], "enabled": true }'
import uuid
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.post(
    f"{BASE}/webhooks",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": "https://example.com/webhooks/proxio",
        "events": ["order.paid", "usage.threshold_reached"],
        "enabled": True,
    },
    timeout=15,
)
resp.raise_for_status()
print(resp.json()["data"]["secret"])  # whsec_..., shown once
import { randomUUID } from "node:crypto"

const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/webhooks`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    url: "https://example.com/webhooks/proxio",
    events: ["order.paid", "usage.threshold_reached"],
    enabled: true,
  }),
})
console.log((await res.json()).data.secret) // whsec_..., shown once

201 response:

{
  "data": {
    "id": "clwh_2b8n",
    "url": "https://example.com/webhooks/proxio",
    "format": "json",
    "events": ["order.paid", "usage.threshold_reached"],
    "enabled": true,
    "secret": "whsec_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aY",
    "failure_count": 0,
    "last_delivery_at": null,
    "created_at": "2026-07-17T09:30:00.000Z",
    "updated_at": "2026-07-17T09:30:00.000Z"
  },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}

The secret is only present on the json create response (see the replay note if you send an Idempotency-Key). The endpoint url is unique per account, so registering a URL you already have returns DUPLICATE_RESOURCE (409), which also covers pointing two endpoints at the same Discord or Slack incoming webhook. You can register up to 20 webhooks per account; beyond that returns LIMIT_REACHED (409).

Get, update, delete

  • GET /webhooks/{id} returns one endpoint (no secret) plus a recent_deliveries array. Each entry carries id, event_type, status, response_status, attempts, last_attempt_at, and created_at. status is one of PENDING (queued or retrying), DELIVERED (a 2xx landed), or FAILED (all 6 attempts were exhausted, or the endpoint was disabled, or the URL failed the SSRF re-check at send time). The array holds at most the 10 most recent deliveries, newest first, and is not paginated, so it is a health snapshot rather than a delivery log. Keep your own record if you need full history.
  • PATCH /webhooks/{id} updates events, enabled, format, or url, use it to re-enable an auto-disabled endpoint or to switch format. The stored url is reused when you switch format without sending a new one, and is re-checked against the new format, so switching to discord or slack needs that platform's incoming-webhook url.
  • DELETE /webhooks/{id} returns 204 No Content.
# Re-enable an endpoint after fixing it:
curl -X PATCH https://dashboard.proxio.net/api/v1/webhooks/clwh_2b8n \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'

Rotate the secret

POST /webhooks/{id}/rotate-secret regenerates the signing secret for a json endpoint and returns the new secret once. The scope is write and an Idempotency-Key is accepted.

curl -X POST https://dashboard.proxio.net/api/v1/webhooks/clwh_2b8n/rotate-secret \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
{
  "data": { "secret": "whsec_5tZ0aY9fJ2kQ7xR4mN8pL1dW6vB3cH5" },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}

Only json endpoints have a signature to rotate. Calling this on a discord or slack endpoint returns VALIDATION_ERROR with details: [{ "field": "format", "issue": "unsupported" }].

Test

POST /webhooks/{id}/test sends a synthetic webhook.test delivery so you can validate your signature handling before real events flow. This is the only way a webhook.test event is produced, it cannot be subscribed to, and it is sent regardless of which events the endpoint has selected.

curl -X POST https://dashboard.proxio.net/api/v1/webhooks/clwh_2b8n/test \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.post(
    f"{BASE}/webhooks/clwh_2b8n/test",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=15,
)
resp.raise_for_status()
print(resp.json()["data"]["delivery_id"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/webhooks/clwh_2b8n/test`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}` },
})
console.log((await res.json()).data.delivery_id)

200 response:

{ "data": { "delivery_id": "cldlv_3k9v" }, "meta": { "request_id": "req_8Ke2jP4mQ" } }

On this page