Skip to content
Documentation menu

Documentation

Webhooks

Get a POST to your endpoint when an OTP or delivery changes state, verified with an HMAC signature, so you do not have to poll.

Webhooks notify your backend when an OTP or delivery changes state, so you do not have to poll. Set an endpoint URL and a signing secret on the API Keys screen in your dashboard. The otp.approved webhook is a push signal for approval on every channel, a convenient alternative to reading the POST /otp/verify response.

Events

Event When it fires
otp.approved The OTP was verified with a correct code.
otp.failed Verify attempts were exhausted without a correct code.
delivery.delivered A message reached the recipient (provider delivery receipt).
delivery.failed A message failed to deliver.

Payload

Every event is a POST with the same envelope. The otp.* events carry an otp_id and status; the delivery.* events also carry a delivery_id.

{
  "id": "evt_…",
  "type": "otp.approved",
  "payload": { "otp_id": "9f3c1b2a-…", "status": "approved" }
}
// delivery.delivered / delivery.failed
{
  "id": "evt_…",
  "type": "delivery.delivered",
  "payload": { "otp_id": "9f3c1b2a-…", "delivery_id": "d1a2…", "status": "delivered" }
}

Verifying the signature

Every request carries two headers:

Header Value
X-Webhook-Signature Hex HMAC-SHA256 of {timestamp}.{rawBody}, keyed with your signing secret.
X-Webhook-Timestamp The Unix timestamp used in the signature.

Recompute the signature over the raw request body, compare it in constant time, and reject the request if it does not match or the timestamp is stale.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, headers, secret) {
  const ts = headers['x-webhook-timestamp'];
  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const got = headers['x-webhook-signature'] ?? '';
  return got.length === expected.length &&
    timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Retries

A failing endpoint is retried with backoff; repeated failures pause delivery until you re-enable it on the API Keys screen. Respond 2xx quickly and do the work asynchronously. Treat events as idempotent: dedupe on the event id, since a retry can redeliver one you already processed.