Custody API

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:

EndpointPurpose
GET/v1/webhooksList (secret redacted)
DELETE/v1/webhooks/{id}Remove (204)
POST/v1/webhooks/{id}/testSend a synthetic webhook.test event
GET/v1/webhooks/{id}/deliveriesDelivery log per endpoint (also tenant-wide at /v1/webhooks/deliveries)
POST/v1/webhooks/{id}/replay/{deliveryId}Redeliver a specific event

Event catalog

EventFires when
transaction.createdA transaction is accepted
transaction.status_changedAny lifecycle transition
transaction.completed / transaction.failedTerminal outcomes
deposit.detected / deposit.confirmedInbound funds seen / confirmed
approval.required / approval.decisionApproval workflow
screening.submitted|completed|flagged|blockedAML screening pipeline
token.deployment_requested|deployed|deployment_failedTokenization

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