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.
| Event | Fires when | Timing |
|---|---|---|
order.paid | An order transitions to paid (new, top-up, or renewal). | Immediate |
order.failed | An order reaches a terminal, unpaid state (expired or canceled). | Checked periodically, up to about 75 minutes |
service.created | A new service is provisioned. | Immediate |
service.renewed | A renewal order finishes fulfilling (top-up applied or expiry moved). | Checked periodically, up to about 15 minutes |
service.expiring_soon | A package's expiry is within 7, 3, or 1 days. | Checked periodically, up to about 15 minutes |
service.expired | A package passed its expiry. | Checked periodically, up to about 15 minutes |
usage.threshold_reached | A metered package crosses 80% or 95% used. | Checked periodically, up to about 15 minutes |
credential.created | A new credential is created on a service. | Immediate |
credential.rotated | A credential's password is rotated. | Immediate |
whitelist.changed | An IP whitelist binding is added to or removed from a credential. | Immediate |
wallet.low_balance | Wallet balance drops below the low-balance threshold. | Checked periodically, up to about 15 minutes |
wallet.topup_completed | A wallet top-up is confirmed and the balance moves. | Immediate |
wallet.topup_failed | A 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:
| Header | Example | Meaning |
|---|---|---|
X-Proxio-Signature | t=1752745800,v1=5d41… | Timestamp and HMAC signature. JSON format only. |
X-Proxio-Event | usage.threshold_reached | The event type. |
X-Proxio-Delivery | cldlv_3k9v | The 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.
| Format | Delivery | Signed |
|---|---|---|
json | The signed envelope above, POSTed to your url. | Yes (X-Proxio-Signature) |
discord | A Discord embed (title, per-field data, timestamp, footer with the event id) POSTed to a Discord incoming-webhook url. | No |
slack | A 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 |
jsonis the default and the only signed format. It also has a rotatable signing secret.discordrequiresurlto be a Discord incoming-webhook URL (ondiscord.com,discordapp.com, or theptb/canarysubdomains, with a/api/webhooks/…path). Pasting a Discord URL in the dashboard auto-suggests this format.slackrequiresurlto be a Slack incoming-webhook URL, thehttps://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 returnsVALIDATION_ERRORwithdetails: [{ "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:
- Parse
tandv1from the header. - Build the signed message
"{t}.{rawRequestBody}", the raw bytes exactly as received, not a re-serialized copy. - Compute
HMAC_SHA256(secret, signedMessage)with your webhook'swhsec_…secret and compare it tov1in constant time. - 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 "", 200import 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.
| Attempt | Delay before it |
|---|---|
| 1 | immediate |
| 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
| Field | Type | Notes |
|---|---|---|
events | string[] | Required. Event types to subscribe to. |
format | string | json (default), discord, or slack. |
url | string | Required 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. |
enabled | boolean | Defaults 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:
issue | Meaning |
|---|---|
INVALID_URL | Not a parseable URL at all. |
NOT_HTTPS | The scheme isn't https://. |
FORBIDDEN_HOST | The host is localhost or ends in .internal / .local. |
FORBIDDEN_IP | The host, or an address it resolves to, is private, loopback, link-local, CGNAT, or otherwise non-routable. |
DNS_FAILED | The 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 onceimport { 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 once201 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 arecent_deliveriesarray. Each entry carriesid,event_type,status,response_status,attempts,last_attempt_at, andcreated_at.statusis one ofPENDING(queued or retrying),DELIVERED(a2xxlanded), orFAILED(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}updatesevents,enabled,format, orurl, use it to re-enable an auto-disabled endpoint or to switch format. The storedurlis reused when you switch format without sending a new one, and is re-checked against the new format, so switching todiscordorslackneeds that platform's incoming-webhookurl.DELETE /webhooks/{id}returns204 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" } }Related pages
Orders
List, quote, and place Proxio orders. GET /orders and /orders/{id} read your order history, POST /orders/quote prices one without buying it, POST /orders places a wallet-paid purchase, and POST /services/{id}/renew tops up or extends a service. Pricing is server-authoritative and both purchase writes require an idempotency key.
Deliveries
The durable webhook delivery log and replay. GET /webhooks/{id}/deliveries and /deliveries/{deliveryId} hold 30 days of history per endpoint, POST .../redeliver replays one, and GET /events is the account-wide view across every endpoint, with type, delivered, and webhook_id filters.

