API docs
Webhooks & events
Receive signed events the moment a verification is sent, delivered, approved, or closed - so your backend reacts without polling. Webhooks are set per request, with nothing to register.
A verification moves through several states after you send it: it is dispatched to a carrier, delivered to the handset, and eventually approved, expired, or closed. Rather than polling GET /v1/verifications/{id}, pass a webhook_url when you send and OTPRelay will POST a JSON event to it as each milestone occurs.
Receiving events
There is no endpoint to register. Include an HTTPS webhook_url in the send requestand OTPRelay delivers that verification's events to it.
Each event mirrors the verification object and its attempts[], so you always see which carrier and SMS route handled delivery.
Delivery & retries
Your endpoint must acknowledge each event quickly. Respond 200 as soon as you have stored it, then do any slower processing asynchronously.
| Behaviour | Value |
|---|---|
| Expected response | 200 OK |
| Timeout | 5 seconds |
| On non-2xx or timeout | Retried up to 3 times with backoff, then dropped. |
| Duplicates | Possible on retry - make your handler idempotent (dedupe on the event id, evt_…). |
Event types
Each event corresponds to a transition in the verification lifecycle. The verification.delivered and verification.failed events are SMS delivery signals; the rest reflect the overall status of the verification.
| Event | When it fires |
|---|---|
verification.sent | OTPRelay accepted the verification and dispatched the SMS to a carrier. |
verification.delivered | The carrier confirmed the SMS reached the handset. With route failover, fires for whichever attempt delivered. |
verification.failed | An attempt could not be delivered (carrier rejection, no route, or undeliverable). Automatic SMS route failover may still recover the send. |
verification.approved | The user submitted the correct code and the verification is now approved. |
verification.expired | The code's TTL elapsed before approval. The verification is closed. |
verification.max_attempts_reached | The check limit (5) was exhausted with no correct code. The verification is closed. |
Event payload
Every delivery uses the same envelope: a top-level id (evt_…), a type, a created_at timestamp, and a data.object containing the affected resource - the verification, including the attempts[] that produced the event and any metadata you set when sending.
Verifying signatures
Every request carries an OTPRelay-Signature header so you can confirm it came from OTPRelay and was not tampered with. The signature is an HMAC-SHA256 computed over the timestamp and the raw, unparsed request body, keyed by your account's webhook signing secret (whsec_…). Compute the same HMAC and compare it against the header before trusting the event - re-serializing the parsed JSON will change the signature, so use the raw bytes OTPRelay sent.
Signature scheme is being finalized
Webhooks pair naturally with delivery & failover for delivery visibility and rate limits & fraud for safeguards. Event error bodies follow the standard error format.