Documentation
How verification works
The lifecycle of a one-time passcode at otp.com, from send to approved, including statuses, attempts, resend, and expiry.
A verification is a small state machine. Understanding its states makes the API predictable: every response tells you where the OTP is, and every call moves it forward.
The lifecycle
- Send.
POST /otp/sendgenerates a code, delivers it over the chosen channel, and returns anotp_idwith statuspending. The code itself is never returned by the API. If routing picks WhatsApp, the code is not sent yet: the response returns anaction_url(awa.melink) the user opens to receive the code over WhatsApp. - The user confirms. You call
POST /otp/verifywith theotp_idand what they typed. This is the same on every channel, WhatsApp included: on WhatsApp the user opensaction_url, receives the code over chat, and enters it like any other. Theotp.approvedwebhook fires on approval if you prefer a push signal over the verify response. - Match or miss. A correct code sets status
approved. A wrong code keeps the OTPpendinguntil attempts run out, then it becomesfailed. - Fallback (optional). If nothing arrived,
POST /otp/resendadvances to the next channel. If time runs out, the OTP becomesexpired.
Statuses
GET /otp/{otp_id} returns one of:
| Status | Meaning |
|---|---|
pending |
Waiting for the user to enter a correct code. |
approved |
Verified: a correct code was entered. This is the success state. |
failed |
Attempts were exhausted without a correct code. |
expired |
The code, or the WhatsApp link, timed out before it was used. |
Attempts and expiry
A wrong code does not fail the OTP immediately: the user gets a few attempts. Only
when attempts are exhausted does the status move to failed. Independently, every
code has a lifetime; once it passes, the status becomes expired even if attempts
remain. In both cases you start over with a fresh POST /otp/send.
Resend and fallback
When a code does not arrive, or the user never verifies it, POST /otp/resend
re-issues it on the next enabled channel rather than retrying the same one.
That is the whole fallback model, and it needs no channel logic on your side.
WhatsApp only takes part when it is first in your order, since the user has to
open a wa.me link to start the chat; a resend never advances into WhatsApp, and
it is skipped if it sits further down the chain. Two guardrails apply:
- A short cooldown between resends. Calling too soon returns
429. - A finite fallback chain. When there is no further channel, resend returns
409.
See Channels & routing for how the chain is chosen.
Billing
Billing is per verification sent, at the published rate for the destination
country and channel. The charge is tied to the send rather than to the final
status, so it is the same whether the OTP ends approved, expired or failed.
- One send, one charge. Where a channel needs more than one message to carry a single code, as WhatsApp does with its inbound and outbound pair, that counts as one verification.
- Verifying is free.
POST /otp/verifycosts nothing, however many attempts it takes. - A resend is a new verification at the same rate, so it is worth factoring resends into your retry policy.
- Nothing is charged for a send that never reaches a delivery provider, or for
anything sent with a
otp_test_…sandbox key.