WhatsApp OTP API Reference
Send and verify OTP codes via WhatsApp using a Sanctum API key. Designed for identity verification, two-step login, and other security flows.
Authentication
The API uses Bearer Token authentication. Create an API key from
Dashboard › Settings › API Keys
and grant it the otp:send and/or otp:verify abilities.
Authorization: Bearer YOUR_API_KEY Content-Type: application/json Accept: application/json
Base URL
https://app.zervixo.com/api/v1
All requests must include Accept: application/json.
Responses are always JSON.
POST /api/v1/otp/send
Sends an OTP code to a phone number via WhatsApp.
Requires the otp:send ability on the API key.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | ✓ |
Phone number in E.164 format (e.g. +966501234567).
|
| purpose | string | — |
Purpose of the code. Accepted: login, register, verify. Default: verify.
|
| expires_in | integer | — |
Code validity duration in seconds. Default: 300 (5 minutes). Max: 3600.
|
| locale | string | — |
Message language. Accepted: ar, en. Default: tenant setting.
|
Responses
{
"success": true,
"message": "OTP sent",
"expires_at": "2026-10-20T14:35:00Z",
"ref": "otp_01jd3k8m4v2xq6z"
}
POST /api/v1/otp/verify
Verifies a previously sent OTP code.
Requires the otp:verify ability on the API key.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | ✓ | Phone number in E.164 format, matching the one used in send. |
| code | string | ✓ | The 6-digit OTP code entered by the user. |
| ref | string | — | Verification reference returned by /otp/send. Recommended for tying requests together. |
Responses
{
"success": true,
"verified": true,
"phone": "+966501234567"
}
{
"success": false,
"verified": false,
"error": "invalid_or_expired"
}
Rate limits
Limits are enforced per API key. When exceeded the API returns 429 Too Many Requests
with a Retry-After header indicating how many seconds to wait.
Error codes
| HTTP | Error code | Description |
|---|---|---|
| 401 | unauthenticated | API key is missing, expired, or revoked. |
| 403 | insufficient_scope | The key lacks the required ability (otp:send or otp:verify). |
| 422 | validation_error | A required field is missing or contains an invalid value. |
| 400 | invalid_or_expired | The OTP code is wrong or has expired. |
| 429 | too_many_requests | Rate limit exceeded. See the Retry-After header. |
| 500 | server_error | Internal server error. Retry or contact support. |
Code examples
cURL — Send OTP
curl -X POST https://app.zervixo.com/api/v1/otp/send \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "phone": "+966501234567", "purpose": "login", "expires_in": 300, "locale": "ar" }'
cURL — Verify OTP
curl -X POST https://app.zervixo.com/api/v1/otp/verify \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "phone": "+966501234567", "code": "483921", "ref": "otp_01jd3k8m4v2xq6z" }'
PHP (Guzzle) — Send OTP
use GuzzleHttp\Client; $client = new Client([ 'base_uri' => 'https://app.zervixo.com/api/v1/', 'headers' => [ 'Authorization' => 'Bearer ' . $apiKey, 'Accept' => 'application/json', ], ]); $response = $client->post('otp/send', [ 'json' => [ 'phone' => '+966501234567', 'purpose' => 'login', 'expires_in' => 300, 'locale' => 'ar', ], ]); $data = json_decode($response->getBody(), true); echo $data['ref']; // store this for verify step
PHP (Guzzle) — Verify OTP
$response = $client->post('otp/verify', [ 'json' => [ 'phone' => '+966501234567', 'code' => $userEnteredCode, 'ref' => $ref, ], ]); $data = json_decode($response->getBody(), true); if ($data['verified']) { // proceed with login / registration } else { // show "incorrect code" message to user }