Skip to content
Documentation menu

Documentation

POST /otp/send

Start a verification. Generate a one-time code and deliver it to the recipient over the channel your account routing chooses, or return a WhatsApp link the user opens to finish on chat.

Start a verification: generate a code and deliver it to the recipient. The channel is chosen by your app’s routing (default order plus per-country overrides), so you do not pass one. An optional locale picks the message language.

Pass client_ip whenever you can: it is the IP of the end user who triggered the OTP (from your request context, e.g. req.ip behind your load balancer), not your server’s address. Requests without it share a much tighter per-app rate limit, and the value powers IP-based abuse protection for your own traffic: VPN/proxy/Tor and datacenter IPs are throttled, and known-abusive IPs are refused outright. A private or reserved address counts as absent.

When routing selects WhatsApp, read action_url and completion before you decide what to render: in the two link modes nothing has been sent yet and the response carries a wa.me link the user must open. See WhatsApp verification.

curl -X POST https://api.otp.com/api/v1/otp/send \
  -H "Authorization: Bearer otp_live_•••" \
  -H "Content-Type: application/json" \
  -d '{"recipient":"+14155552671","locale":"en","client_ip":"81.2.69.142"}'

Request body

FieldTypeRequiredDescription
recipientstringyesPhone number in E.164 form (+14155552671) or an email address.
localestringnoMessage language, for example en. Defaults to your app setting.
client_ipstringnoIP of the end user who triggered the OTP (IPv4 or IPv6). Strongly recommended: requests without it share a much tighter rate limit. A malformed value is rejected (422); a private/reserved one counts as absent.

There is no channel field. Routing selects the channel from the recipient and your configuration. See Channels & routing.

Idempotency

Send an idempotency-key header and a repeat of the same call replays the first response instead of sending a second code. Use it wherever a retry is possible: a network timeout, a queue redelivery, a user double-tap on your own submit button. Scope the key to the thing being verified, for example signup:<user_id>.

curl -X POST https://api.otp.com/api/v1/otp/send \
  -H "Authorization: Bearer otp_live_•••" \
  -H "Content-Type: application/json" \
  -H "idempotency-key: signup:8f21c4" \
  -d '{"recipient":"+14155552671","client_ip":"81.2.69.142"}'

The key is scoped to your company and the replay is matched on the key alone, so give each verification its own key: reuse it for a different recipient and you get the first recipient’s OTP back, not a new one. Keys are at most 128 characters; a longer one is a 422. The SDKs expose the key as a parameter on the send call.

Response

{
  "otp_id": "9f3c1b2a-…",
  "status": "pending",
  "channel": "sms",
  "masked_recipient": "+1****71",
  "action_url": null,
  "completion": "code"
}
FieldDescription
otp_idIdentifier for this verification. Pass it to verify, resend, and status.
statusAlways pending on a fresh send.
channelThe channel the OTP was routed to.
masked_recipientThe recipient with most characters masked, safe to log or display.
action_urlA WhatsApp wa.me link the user opens to finish on chat, in the two WhatsApp link modes; null on every other channel and under the WhatsApp Template mode.
completionHow this OTP finishes. "code": the user enters the code and you call verify. "inbound": the user’s WhatsApp message approves it, there is no code (see WhatsApp verification).

The code itself is never returned. On WhatsApp in a link mode it is not sent with the response either: the user opens the action_url first, and completion says whether a code then comes back to them or the message itself approves the OTP.

Common errors

StatusTypeWhen
422InvalidRecipientErrorrecipient is not a valid phone or email.
409NoEnabledChannelErrorNo enabled channel can reach this recipient. Configure your channels first.
403GeoBlockedErrorThe recipient’s country is blocked.
402InsufficientFundsErrorNot enough balance to send.
403IpReputationBlockedErrorThe declared client_ip is a known abuse source.
429RateLimitExceededErrorToo many requests for this recipient, or too many without a usable client_ip (the message names the scope).
422InvalidIdempotencyKeyErrorThe idempotency-key header is longer than 128 characters.

See Error codes for the full list.