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. 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

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.