Skip to content
Oxtro
Documentation menu

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

  1. Your server calls POST /v1/otp/send with the phone number and gets a request_id.
  2. Oxtro generates a numeric code, stores only its hash, and sends it from your connected number.
  3. The user types the code into your app. Your server sends it with the request_id to POST /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

FieldTypeDescription
connection_idstring, required
tostring, required
template_idstring
code_lengthintegerFrom 4 to 8. Default: 6.
expires_in_minutesintegerFrom 1 to 60. Default: 5.
curl
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
  }'
200 OK
{
  "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

LimitDefaultResponse when hit
Resend cooldown per phone60 seconds429 OTP_COOLDOWN_ACTIVE with details.retry_after_seconds
Codes per phone per hour5429 RATE_LIMITED
Codes per phone per day10429 RATE_LIMITED
Monthly OTP requestsSet by your plan403 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

FieldTypeDescription
request_idstring
tostring
codestring, 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
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"
  }'
200 OK
{
  "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.

400 Bad Request
{
  "error": {
    "code": "OTP_INVALID",
    "message": "Invalid verification code.",
    "request_id": "req_123",
    "details": { "remaining_attempts": 2 }
  }
}
HTTPCodeWhen
400OTP_INVALIDWrong code. details.remaining_attempts says how many tries are left.
400OTP_EXPIREDThe code is past its expiry. Send a new one.
400OTP_MAX_ATTEMPTSToo many wrong tries (3 by default). The request is locked. Send a new code.
404NOT_FOUNDNo OTP request matches the request_id or phone number.
409CONFLICTThe 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.