API reference
OTP API
Verify a phone number in two calls. Oxtro generates the code, delivers it on WhatsApp and checks what the user typed.
How it works
- Your server calls
POST /v1/otp/sendwith the phone number and gets arequest_id. - Oxtro generates a numeric code, stores only its hash, and sends it from your connected number.
- The user types the code into your app. Your server sends it with the
request_idtoPOST /v1/otp/verify.
The code never passes through your servers, so there is nothing for you to store or expire.
Send a code
POST/v1/otp/sendScope otp:send
| Field | Type | Description |
|---|---|---|
connection_id | string, required | |
to | string, required | |
template_id | string | |
code_length | integer | From 4 to 8. Default: 6. |
expires_in_minutes | integer | From 1 to 60. Default: 5. |
curl https://api.getoxtro.com/v1/otp/send \
-H "Authorization: Bearer $OXTRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "conn_123",
"to": "+213555123456",
"code_length": 6,
"expires_in_minutes": 5
}'{
"request_id": "otp_123",
"expires_in": 300,
"status": "sent"
}expires_in is in seconds. code_length, expires_in_minutes and template_id fall back to the OTP settings of your workspace, which start at 6 digits and 5 minutes. An OTP template can use the variables {{otp}} and {{expires_in}}. This endpoint accepts an Idempotency-Key header.
Limits on sending
| Limit | Default | Response when hit |
|---|---|---|
| Resend cooldown per phone | 60 seconds | 429 OTP_COOLDOWN_ACTIVE with details.retry_after_seconds |
| Codes per phone per hour | 5 | 429 RATE_LIMITED |
| Codes per phone per day | 10 | 429 RATE_LIMITED |
| Monthly OTP requests | Set by your plan | 403 QUOTA_EXCEEDED |
The cooldown and the daily cap can be changed in the OTP settings of your workspace.
Verify a code
POST/v1/otp/verifyScope otp:verify
| Field | Type | Description |
|---|---|---|
request_id | string | |
to | string | |
code | string, required |
Identify the request by request_id. If you only have the phone number, send to instead and Oxtro checks the most recent code sent to it.
curl https://api.getoxtro.com/v1/otp/verify \
-H "Authorization: Bearer $OXTRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"request_id": "otp_123",
"code": "492821"
}'{
"verified": true,
"request_id": "otp_123",
"phone_number": "+213555123456",
"verified_at": "2026-10-10T09:42:31.000Z"
}A correct code returns 200 with verified: true. A wrong code is not a 200 with verified: false: it is an HTTP 400 with an error code. Treat OTP_INVALID, OTP_EXPIRED and OTP_MAX_ATTEMPTS as "not verified" and anything else as a failure.
{
"error": {
"code": "OTP_INVALID",
"message": "Invalid verification code.",
"request_id": "req_123",
"details": { "remaining_attempts": 2 }
}
}| HTTP | Code | When |
|---|---|---|
| 400 | OTP_INVALID | Wrong code. details.remaining_attempts says how many tries are left. |
| 400 | OTP_EXPIRED | The code is past its expiry. Send a new one. |
| 400 | OTP_MAX_ATTEMPTS | Too many wrong tries (3 by default). The request is locked. Send a new code. |
| 404 | NOT_FOUND | No OTP request matches the request_id or phone number. |
| 409 | CONFLICT | The code was already verified. A code works once. |
Webhook events
OTP activity is also sent as otp.sent, otp.verified and otp.failed webhook events, which is useful for spotting abuse.