Skip to content
Documentation menu

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. SMS, Telegram, and Email all deliver a code the user types back. WhatsApp is the one channel with more than one shape, and the send response tells you which one you are in.

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, and how this verification finishes:

{
  "otp_id": "9f3c1b2a-…",
  "status": "pending",
  "channel": "sms",
  "masked_recipient": "+1****71",
  "action_url": null,
  "completion": "code"
}

completion is "code" on every channel except one WhatsApp mode. Read it rather than branching on channel: it is the field that says whether to show a code input.

WhatsApp

WhatsApp has three modes, and your account runs exactly one of them. In the default mode (Code back) the send response carries an action_url (a wa.me link) and the OTP stays pending: nothing has reached the user yet. You show a “Continue on WhatsApp” button that opens the link, the user sends the prefilled message, we reply with their code over chat, 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 other two modes change what your code does:

  • Inbound approve also returns a link, but the user’s message approves the OTP by itself. completion is "inbound", there is no code, and nothing to verify: you poll GET /otp/{otp_id} or take the otp.approved webhook.
  • Template pushes the code straight to the user, like SMS. action_url is null and there is nothing WhatsApp-specific to build.

Full detail, screenshots of all three, and the sandbox rules are on WhatsApp verification.

No WhatsApp? Verify by SMS. In the two link modes, a user without WhatsApp (or one whose code never arrives) is not stuck: 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. A plain resend, with no channel, only re-returns the same link.

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. Once the order is exhausted, a resend repeats the last usable channel rather than refusing.

Where WhatsApp can sit in that order depends on the mode your account runs. Under Template it is a code channel like any other: put it anywhere, and a plain resend advances into it and out of it exactly like SMS. Under the two link modes (Code back and Inbound approve) it works as the first channel only. Reaching the user there takes a step only your app can do, showing the wa.me link, so a plain resend skips WhatsApp anywhere but first; the only way onto it from further down is your own resend {"channel":"whatsapp"}. The routing panel shows a warning when a link-mode account has WhatsApp below first. The way out is also explicit: an OTP already on a link needs resend {"channel":"…"} to move off it, because a plain resend there means “show me the link again”.

Two guardrails bound the whole thing. A resend attempted within the cooldown window returns 429 ResendCooldownError. Once the OTP’s send attempts are spent, or no enabled channel is left for the recipient, resend returns 409 ResendNotAllowedError.

Blocked countries

Some destination countries are paused platform-wide as a fraud control: certain ranges see SMS pumping and toll fraud, so sending to them is stopped for every account at once. A send to one is refused with 403 GeoBlockedError.

If your product legitimately needs one of them, request access from the Routes screen in your dashboard and say why. An approved country opens for your account only, and stays open even if the platform block is later lifted and re-applied.

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.