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 receive the code.
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, the code is not sent yet: the response carries an
action_url (a wa.me link) the user opens to receive the code over chat, and the
OTP stays pending until they enter it and you verify. 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
| Field | Type | Required | Description |
|---|---|---|---|
recipient |
string | yes | Phone number in E.164 form (+14155552671) or an email address. |
locale |
string | no | Message language, for example en. Defaults to your app setting. |
client_ip |
string | no | IP 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
}
| Field | Description |
|---|---|
otp_id |
Identifier for this verification. Pass it to verify, resend, and status. |
status |
Always pending on a fresh send. |
channel |
The channel the OTP was routed to. |
masked_recipient |
The recipient with most characters masked, safe to log or display. |
action_url |
A WhatsApp wa.me link the user opens to receive the code over chat, when the channel is whatsapp; null on every other channel. |
The code itself is never returned. On WhatsApp it is not sent with the response:
the user opens the action_url to receive the code over chat, then enters it and
you verify it like any other channel.
Common errors
| Status | Type | When |
|---|---|---|
422 |
InvalidRecipientError |
recipient is not a valid phone or email. |
403 |
GeoBlockedError |
The recipient’s country is blocked. |
402 |
InsufficientFundsError |
Not enough balance to send. |
403 |
IpReputationBlockedError |
The declared client_ip is a known abuse source. |
429 |
RateLimitExceededError |
Too many requests for this recipient, or too many without a usable client_ip (the message names the scope). |
422 |
InvalidIdempotencyKeyError |
The idempotency-key header is longer than 128 characters. |
See Error codes for the full list.