Webhooks
Every meaningful state change is delivered to your HTTPS endpoint as a signed JSON payload. Use webhooks instead of polling for transaction status, deposits, approvals, and screening results.
Register an endpoint
POST/v1/webhooks
{"url": "https://api.acme.example/qustody-hook", "events": ["transaction.status_changed", "deposit.confirmed"]}
// 201 →
{"id": "…", "tenantId": "…", "url": "…",
"secret": "<64 hex chars>", // shown ONCE — store it for signature verification
"events": [ … ], "status": "ACTIVE", "createdAt": "…"}
Production endpoints must be HTTPS. Manage endpoints with:
| Endpoint | Purpose |
|---|---|
GET/v1/webhooks | List (secret redacted) |
DELETE/v1/webhooks/{id} | Remove (204) |
POST/v1/webhooks/{id}/test | Send a synthetic webhook.test event |
GET/v1/webhooks/{id}/deliveries | Delivery log per endpoint (also tenant-wide at /v1/webhooks/deliveries) |
POST/v1/webhooks/{id}/replay/{deliveryId} | Redeliver a specific event |
Event catalog
| Event | Fires when |
|---|---|
transaction.created | A transaction is accepted |
transaction.status_changed | Any lifecycle transition |
transaction.completed / transaction.failed | Terminal outcomes |
deposit.detected / deposit.confirmed | Inbound funds seen / confirmed |
approval.required / approval.decision | Approval workflow |
screening.submitted|completed|flagged|blocked | AML screening pipeline |
token.deployment_requested|deployed|deployment_failed | Tokenization |
Delivery format
POST your-endpoint
X-Qustody-Signature: <hex HMAC-SHA256 of the raw body>
X-Webhook-Signature: <same value — deprecated alias>
X-Webhook-ID: <delivery id>
X-Webhook-Timestamp: 2026-08-06T12:00:00Z
{"id": "…", "type": "transaction.status_changed", "tenantId": "…",
"timestamp": "…", "data": { …full transaction object… }}
Verify the signature
Compute HMAC-SHA256 over the raw request body with your endpoint secret and compare it (constant-time) to X-Qustody-Signature:
Go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
want := hex.EncodeToString(mac.Sum(nil))
ok := hmac.Equal([]byte(want), []byte(r.Header.Get("X-Qustody-Signature")))
Node.js
const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(req.headers["x-qustody-signature"]));
Verify before parsing
Read the body bytes, verify the HMAC, and only then JSON-parse. Reject anything unsigned or stale by
X-Webhook-Timestamp to blunt replay.Delivery semantics
- Any 2xx response marks the delivery successful — respond fast, process async.
- Failures retry with exponential backoff (
base × 2^(attempt−1), capped), then mark the deliveryFAILED; use the replay endpoint to recover. - A circuit breaker pauses an endpoint after 5 consecutive failures.
- Deliveries may arrive out of order or (rarely) more than once — key your processing on the delivery
id.

