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.

Request header
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: application/json
⚠ Never embed API keys in source code or public repositories. Use environment variables or a secrets store.

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.

POST /api/v1/otp/send

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

200 Code sent successfully.
{
  "success": true,
  "message": "OTP sent",
  "expires_at": "2026-10-20T14:35:00Z",
  "ref": "otp_01jd3k8m4v2xq6z"
}
422 Validation error.
429 Rate limit exceeded.
401 Missing or invalid API key.

POST /api/v1/otp/verify

Verifies a previously sent OTP code. Requires the otp:verify ability on the API key.

POST /api/v1/otp/verify

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

200 Code is valid.
{
  "success": true,
  "verified": true,
  "phone": "+966501234567"
}
200 Invalid or expired code.
{
  "success": false,
  "verified": false,
  "error": "invalid_or_expired"
}
429 Rate limit exceeded.
401 Missing or invalid API key.

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.

POST /api/v1/otp/send
100 req
/ hour
POST /api/v1/otp/verify
20 req
/ minute
ⓘ If you need higher limits for production use, contact the support team.

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
}