Developer API and webhooks

Webhooks: subscribe, verify, and retries

Subscribe a public URL to code events, verify each delivery with the Standard Webhooks signature, and understand the retry backoff and auto-disable behavior.

Subscribe to events

Webhooks live alongside API tokens in your dashboard under API & webhooks, in the Webhooks card. Enter a target URL and choose at least one event, then click Add webhook.

The events you can subscribe to are:

  • code.scanned - a dynamic code was scanned
  • code.created - a new code was created
  • code.updated - a code was changed

The target must be a public https URL - a localhost or private-network address is rejected with "That URL isn't reachable from the public internet (no localhost or private-network targets)." Once added, the webhook appears in a table with its URL, events, signing secret, status (Active or Disabled), and a Delete action that stops deliveries immediately.

Verify every delivery

Each delivery is signed using the Standard Webhooks scheme, so any off-the-shelf Standard-Webhooks receiver library can verify it. Every request carries three headers:

webhook-id:        <this delivery's id>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<base64 HMAC-SHA256>

The signature is an HMAC-SHA256 over the string {webhook-id}.{webhook-timestamp}.{raw-body}, keyed by your subscription's signing secret. The secret starts with whsec_ and is shown in the webhooks table.

To verify a request:

  1. Recompute the HMAC-SHA256 of {id}.{timestamp}.{body} using the secret. Important: key the HMAC on the base64-decoded bytes of the secret (the part after whsec_), not the raw text - that's what the Standard Webhooks spec and its libraries do.
  2. Compare your result to the v1,... value in webhook-signature in constant time.
  3. Reject stale timestamps - if webhook-timestamp is more than 5 minutes from now, treat it as a replay and drop it.

The webhook-signature header may eventually carry several space-separated v1,... values to support key rotation. Accept the delivery if any of them matches, and always verify against the raw request body exactly as received (don't re-serialize the JSON first).

Retries and auto-disable

We deliver over HTTP POST with a 10-second timeout and don't follow redirects (a 3xx counts as undelivered). A 2xx response marks the delivery as sent. Anything else is retried on a fixed backoff:

30 seconds → 2 minutes → 10 minutes → 1 hour → 4 hours.

After those five retries are exhausted, that delivery is marked dead and not tried again. If a subscription keeps failing - 20 consecutive failed or dead deliveries - it auto-disables, its status flips to Disabled, and it stops receiving events until you fix and re-add it. Delivery history is pruned after 14 days.

Because a delivery is retried, your endpoint should be idempotent - use the webhook-id header to recognize and ignore a delivery you've already processed. Return a 2xx quickly; do slow work after acknowledging.

Webhooks fire on your codes' activity - create a dynamic code to start receiving code.scanned events.

Create a QR code

FAQ

Where do I get the signing secret? It's shown in the webhooks table next to each subscription, and it starts with whsec_.

Why are my deliveries failing on a local URL? Only public https URLs are accepted. localhost and private-network addresses are blocked.

How many times will you retry? Five retries after the first attempt - at 30s, 2m, 10m, 1h, and 4h - then the delivery is marked dead.

Why did my webhook turn Disabled? It auto-disables after 20 consecutive failed deliveries. Fix your endpoint, delete the disabled webhook, and add it again.