Documentation
Channels & routing
How otp.com chooses a delivery channel, why you do not pass one per request, and how automatic fallback keeps delivery rates high.
otp.com verifies over SMS, WhatsApp, Telegram, and Email. You do not choose a channel per request: your account routing does, based on the recipient and your configuration. This keeps the API a single call and lets delivery adapt without a code change on your side. Every channel delivers a code the user types back; on WhatsApp the code arrives over chat after the user opens a link (see below).
How the channel is chosen
When you call POST /otp/send, routing decides the channel from:
- The recipient type. An email address is emailed. A phone number walks your channel order.
- Your default order. The sequence of channels to try for phone recipients, set once for the app.
- Per-country overrides. Some countries deliver better on a specific channel; overrides let you tune that without touching code.
The response tells you which channel was used:
{
"otp_id": "9f3c1b2a-…",
"status": "pending",
"channel": "sms",
"masked_recipient": "+1****71",
"action_url": null
}
WhatsApp works like every other channel: the user enters a code and you call
POST /otp/verify. The only difference is how the code reaches them. When routing
selects WhatsApp the code is not sent yet: the send response returns an action_url
(a wa.me link) and the OTP stays pending. You show the user a “Get your code on
WhatsApp” button that opens action_url, which launches WhatsApp with a prefilled
message to your business number. The user sends that message, we reply over WhatsApp
with their code, and they enter it in your UI.
From there it is identical to the other channels: call POST /otp/verify with the
otp_id and the code the user entered. The otp.approved webhook
still fires on approval if you prefer a push signal over the verify response.
WhatsApp does not auto-advance to another channel: a plain resend re-returns the same link. But the user is not stuck if the code does not arrive:
No WhatsApp? Verify by SMS. For a user without WhatsApp, call
POST /otp/resend with {"channel":"sms"} to move that pending
OTP onto SMS. It delivers a fresh code (action_url comes back null) and you
continue with the normal verify step. This is a user-initiated
switch; the server still never auto-falls-back off WhatsApp on its own.
Automatic fallback
If a channel does not get the code through, or the user never verifies it,
POST /otp/resend advances to the next channel in your order. You do not
implement any of this logic: one call moves the same OTP to the next channel, and
the response shows the new channel.
WhatsApp is only ever a first channel. Its flow starts with the user opening a
wa.me link, so a resend cannot advance into it: if WhatsApp sits further down
your order, the chain skips it and moves on to the next channel.
When the chain is exhausted (no further channel to try), resend returns 409
ResendNotAllowedError. A resend attempted within the cooldown window returns 429
ResendCooldownError.
One recipient field, phone or email
recipient accepts either a phone number in E.164 form (+14155552671) or an
email address. The same POST /otp/send call works for both; routing picks the
right delivery path from the value you pass.