Skip to content
Testnet only: a reference implementation, not a production service.Tokens and bank transfers here are simulated and have no value.

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.

Edit on GitHub