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.