Webhooks
Server-pushed event delivery so you don't have to poll. HMAC-signed payloads, automatic retries with exponential backoff, replay UI for any failed delivery.
How it works
- You register an HTTPS endpoint and pick the events you want from /settings/security. We hand you a signing secret once at creation — save it.
- Whenever a subscribed event happens on your account (a trade fills, a deposit credits, etc.), we sign a JSON payload with your secret and POST it to your endpoint.
- You respond with any
2xxstatus. Non-2xx, network errors, or timeouts trigger an automatic retry with exponential backoff (5 attempts total over ~2 hours). - If your endpoint racks up 10 consecutive failures, the subscription is auto-disabled. You can re-enable it from /settings/security once you've fixed the root cause.
Create a subscription
Webhook subscriptions are managed from the dashboard — not the public v1 API. Open /settings/security → Webhooks and fill in:
- Endpoint URL — must be HTTPS in production.
- Description (optional) — a label so you can tell multiple subscriptions apart.
- Events — any subset of the catalog below.
Max 5 active subscriptions per account. The signing secret is shown once at creation — store it in your app's secrets manager.
Event catalog
Payload envelope
Every webhook body is the same outer shape — only data changes per event type.
id is unique per logical event — use it to deduplicate on your side (we deliver at least once, never less).
Payload data shapes
The data object inside the envelope is event-specific. All numeric values are sent as decimal-formatted strings so JSON parsers don't round 18-decimal balances down to 53-bit floats. Where two parties get notified for the same logical event (P2P), the counterparty's payload includes "counterparty": true.
webhook.testUser-triggered probe — same envelope as a real event.order.filleddeposit.creditedThe credited asset can differ from the deposited asset — e.g. non-USDT stables credit as USDT.withdrawal.completedwithdrawal.rejectedSent when a broadcast fails and locked funds are refunded. `reason` is human-readable.alert.triggeredOne-shot — the alert is marked triggered server-side and will not fire again.p2p.trade.createdBoth parties receive the same payload; the counterparty's also includes `"counterparty": true`.p2p.trade.paidBuyer marked payment as sent. Counterparty (seller) also gets it with `counterparty: true`.p2p.trade.completedEscrow released. Counterparty payload includes `counterparty: true`.p2p.dispute.opened`reason` is mirrored from the disputing party. Counterparty payload includes `counterparty: true`.Delivery headers
Verifying signatures
The Nextbridge-Signature header has one timestamp and one or more signature parts: t=<unix-ts>,v1=<hex>[,v1=<hex>]. During a secret rotation we emit two v1= values — accept the delivery if any matches your HMAC. Reject anything older than ~5 minutes to prevent replay.
Node.js (Express)
Python (FastAPI)
Always use a constant-time compare (timingSafeEqual /hmac.compare_digest) — a plain === leaks timing data attackers can use to brute-force a forged signature.
Without the timestamp check, an attacker who captures one of our deliveries off the wire (or out of a misconfigured log) can resend the exact bytes to your endpoint days later and the signature still matches — because botht= and the HMAC are inside the payload they captured. The abs(now - t) < 300 check makes that attack window five minutes wide instead of forever. Pair it with idempotency on event.id for full coverage: even within the 5-minute window, a replayed event is a no-op because you've already processed that id.
Rotating secrets
If your signing secret leaks (committed to a repo, posted in a chat, exposed via a third party), click Rotate on the subscription in /settings/security.
- We generate a new secret and show it to you once. The old secret becomes a previous secret with a 24-hour expiry.
- During those 24 hours, every delivery is signed with both the new and old secret. The header carries two
v1=values; receivers accept the delivery if either matches. - Add the new secret to your config alongside the old one (see the verification snippets above — the example reads
NEXTBRIDGE_WEBHOOK_SECRETSas a comma-separated list). - When you've confirmed your endpoint accepts the new secret, remove the old one from your config. After 24h the worker drops it automatically.
Calling rotate again before the previous window closes overwrites the previous secret — only ever two secrets are active at once.
Retries & auto-disable
- A non-2xx response, network error, or 15-second timeout counts as a failure.
- Up to 5 total attempts per delivery (1 initial + 4 retries) with exponential backoff managed by Inngest — final attempt is roughly 2 hours after the first.
- After 10 consecutive failures across deliveries, the subscription auto-disables. New events stop firing to it until you re-enable.
- Re-enable from /settings/security after your endpoint is healthy again — failure counter resets to 0.
Replay & history
Click Deliveries on any subscription to see its last 50 deliveries with status, HTTP code, attempt count, and timing. Any non-delivered row has a Replay button — clicking it re-enqueues the same payload (same event id, same body, same signature seed) so your endpoint can process it again.
The worker is idempotent on the delivered status — replaying an already-delivered row is a no-op, so you can't accidentally double-deliver from the UI.
Best practices
- Respond fast. Acknowledge with a 2xx within a few seconds, then do the slow work in a background job. Holding the connection past 15 seconds looks like a failure to us.
- Process idempotently. Deliveries are at-least-once. Track the
idof each event you've already processed and no-op on duplicates. - Verify every payload. An unsigned-but-valid-looking POST to your endpoint is the most likely attack vector. Reject anything without a valid signature.
- Reject stale deliveries. Anything more than ~5 minutes old is probably a replay attempt — reject it even if the signature is valid.
- Subscribe narrowly. Pick only the event types you actually need so your endpoint isn't spammed with events you discard.