Webhooks push every transaction state transition to your endpoint as it happens. They are the recommended integration pattern: one subscription replaces a polling loop, and delivery comes with signatures, retries, and a 30-day delivery log.
Register a subscription
curl -X POST https://api-staging.coinbax.com/api/v1/webhooks \
-H "X-API-Key: $COINBAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/coinbax-webhook",
"eventTypes": ["transaction.created", "transaction.escrowed", "transaction.completed", "transaction.refunded", "transaction.failed"]
}'
The response includes the subscription’s signing secret. Like API keys, it
is shown once; store it securely. Requires the write:webhooks
scope.
You can register multiple subscriptions, each with its own URL, event
selection, and secret. An optional ipWhitelist restricts which addresses
may receive deliveries for the subscription. Manage subscriptions with
GET/PATCH/DELETE /webhooks/{id}, and send a test delivery with
POST /webhooks/{id}/test.
Event types
| Event | Fires when |
|---|---|
transaction.created |
A transaction is created; always the first event in a lifecycle |
transaction.escrowed |
Funds lock on-chain in the escrow contract |
transaction.completed |
Funds released to the receiver (terminal) |
transaction.failed |
A pre-escrow control failed or risk rejected the transaction (terminal) |
transaction.refunded |
Funds returned to the sender (terminal) |
transaction.rescinded |
The sender cancelled during the hold period (terminal) |
dispute.created |
A dispute is opened |
dispute.resolved |
A dispute is resolved |
dispute.escalated |
A dispute escalates to the next resolution tier |
evidence.submitted |
Evidence is submitted on a dispute |
The canonical, always-current list is available at
GET /webhooks/events/types.
Payload shape
Deliveries are HTTPS POSTs with a JSON body. Transaction events carry the transaction plus event-specific fields:
// transaction.escrowed
{
"transaction": { "id": "9f8b3c2a-...", "status": "escrowed" },
"escrowAddress": "0x...",
"txHash": "0x..."
}
// transaction.refunded
{
"transaction": { "id": "9f8b3c2a-...", "status": "refunded" },
"reason": "Customer request"
}
Each delivery includes these headers:
| Header | Contents |
|---|---|
X-Webhook-Event |
The event type, for example transaction.completed |
X-Webhook-Signature |
HMAC-SHA256 hex digest of the request body, keyed by your secret |
X-Webhook-Delivery |
Unique delivery ID, for idempotency |
Verify the signature
Every delivery is signed. Verify before trusting anything in the payload:
import crypto from "node:crypto";
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected),
);
}
Two details that bite people:
- Verify against the raw request body, not re-serialized parsed JSON. Re-serialization can reorder keys and change bytes, which breaks the HMAC.
- Use a timing-safe comparison (
crypto.timingSafeEqualor your language’s equivalent), never===on the hex strings.
Reject any request whose signature does not match.
Delivery, retries, and the circuit breaker
- At-least-once. Deliveries can repeat. Dedupe on
X-Webhook-Deliverywithin a reasonable window (24 hours is a good default). - Respond fast. Coinbax waits up to 30 seconds for a 2xx response. Acknowledge immediately and process asynchronously; a slow handler is indistinguishable from a failing one.
- Retries. Non-2xx responses, timeouts, and network errors are retried with exponential backoff.
- Circuit breaker. Each subscription has a circuit breaker. Repeated
failures trip it OPEN, pausing deliveries so a broken endpoint does not
accumulate an unbounded retry queue. It transitions to HALF_OPEN to test
recovery and closes again once deliveries succeed. You can force a reset
with
POST /webhooks/{id}/reset-circuit-breaker. - Delivery logs.
GET /webhooks/{id}/delivery-logsreturns a 30-day audit trail with request and response details per delivery, which is the fastest way to debug a misbehaving endpoint.
Rotate the secret
Secrets rotate with zero downtime using dual signing:
POST /webhooks/{id}/rotate-secretreturns a new secret. During the grace period Coinbax signs deliveries with both the old and the new secret, so your endpoint can verify with either.- Deploy the new secret to your handler.
POST /webhooks/{id}/complete-rotationretires the old secret.
Rotate immediately if a secret may have been exposed, and on your normal credential rotation schedule otherwise.
Handler checklist
- Verify the HMAC signature against the raw body, timing-safe
- Dedupe on
X-Webhook-Delivery - Return 2xx quickly; process async
- Treat event ordering as advisory; fetch the transaction with
GET /transactions/{id}when you need authoritative current state - Serve HTTPS with a valid certificate
Next steps
- Your first transaction: the events in lifecycle order
- Errors and the response envelope: the envelope your GET calls return
- Payments API reference: every webhook endpoint