# Webhooks

> Subscribe to transaction lifecycle events, verify HMAC signatures, and rely on retries with circuit breaker protection.
> Source: https://developers.coinbax.com/docs/api/webhooks
> Last updated: 2026-07-16

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

```bash
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](/docs/api/scopes).

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:

```json
// transaction.escrowed
{
  "transaction": { "id": "9f8b3c2a-...", "status": "escrowed" },
  "escrowAddress": "0x...",
  "txHash": "0x..."
}
```

```json
// 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:

```javascript
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.timingSafeEqual` or 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-Delivery`
  within 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-logs` returns 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:

1. `POST /webhooks/{id}/rotate-secret` returns 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.
2. Deploy the new secret to your handler.
3. `POST /webhooks/{id}/complete-rotation` retires 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](/docs/getting-started/first-transaction): the events in lifecycle order
- [Errors and the response envelope](/docs/api/errors): the envelope your GET calls return
- [Payments API reference](/reference/coinbax-api/): every webhook endpoint