Webhooks and signatures
A webhook endpoint receives events as they happen: a transfer completes, a deposit route credits funds, an account is frozen. The platform delivers each event over HTTPS with a Standard Webhooks signature, and retries until your endpoint accepts it.
Every request on this page runs in CI against an in-process core on FakeChain, so a response that stops matching its # expect line fails the build. The {{KEY}} and {{uuid}} placeholders work as in the quickstart.
Register an endpoint
Create an endpoint with POST /v1/webhook-endpoints. The URL must be absolute HTTPS. enabled_events names the event types to subscribe to, or ["*"] for all of them. The response carries a secret; store it, because the platform shows it only once.
POST /v1/webhook-endpoints
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"url": "https://example.com/hooks/stablecoin",
"description": "transfer events for the ledger",
"enabled_events": ["transfer.completed", "transfer.failed"]
}
# expect 201
# save ENDPOINT = $.id
New endpoints start disabled. An endpoint is enabled only after a test event succeeds, so a typo in the URL cannot silently drop live events. POST /v1/webhook-endpoints/{id}/test sends one signed test delivery; it reaches the network, so it is not run in CI.
POST /v1/webhook-endpoints/{{ENDPOINT}}/test
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{}
# expect 200
The event payload
Each delivery is JSON: an event id, the event type, the created time, and a data.object whose shape depends on the type. Every object carries a monotonic version; when deliveries arrive out of order, keep the highest.
{
"id": "evt_9001",
"type": "transfer.completed",
"created": "2026-10-16T12:00:00.000Z",
"data": {
"object": {
"object": "transfer",
"id": "trf_...",
"kind": "mint",
"status": "completed",
"previous_status": "submitted",
"reason": null,
"version": 3
}
}
}
The event types the platform publishes are listed in the API reference.
Verify the signature
Every delivery carries three Standard Webhooks headers: webhook-id, webhook-timestamp and webhook-signature. The signature is v1, followed by the base64 HMAC-SHA256 over id.timestamp.body, keyed by the endpoint's secret. verifyWebhook from @scr/sdk recomputes it and also refuses a timestamp more than five minutes old, which blunts a replay.
import { verifyWebhook } from "@scr/sdk";
export function onDelivery(secret: string, headers: Headers, rawBody: string): boolean {
if (!verifyWebhook(secret, headers, rawBody)) return false;
const event = JSON.parse(rawBody) as { id: string };
return event.id.length > 0;
}
Verify against the raw request body, before any JSON parser reformats it. Read webhook-signature as a whole header: during secret rotation it can carry several space-separated signatures, and any one of them verifies.
Retries and redelivery
Delivery is at-least-once, not exactly-once. Up to ten attempts are spread over 24 hours with exponential backoff, and every attempt is signed again with a fresh webhook-timestamp. Ordering is not promised, so a later delivery can arrive before an earlier one; deduplicate by webhook-id, which is stable across attempts, and ignore an object whose version is not newer.
A delivery that keeps failing is recorded. An operator can retry it with POST /v1/events/{id}/redeliver, which queues a fresh delivery for the stored event. Rotating an endpoint's secret with POST /v1/webhook-endpoints/{id}/rotate-secret keeps the previous secret signing for 24 hours too, so a receiver can switch without dropping events during the overlap.