Integrations

WhatsApp OTP API for developers 2026 — send and verify a code, with PHP, Laravel, Node.js and Python examples

The full contract of the WhatsApp verification-code API: send, verify, resend and status, the public key and HMAC signing, every error code and limit, and the verification callback to your server — with ready code in PHP, Laravel, Node.js, Python and curl.

9 min read Updated 1 October 2026 ArabyBot team
The ArabyBot teamOfficial Meta partner · written from our own platform data
On this page 11
  1. Before the first line of code: an OTP app and two keys
  2. Five endpoints — the whole contract
  3. The quickest try: curl
  4. PHP — one function for every call
  5. Laravel — through Http
  6. Node.js — fetch, no libraries
  7. Python — requests
  8. Errors and limits — and what to do about each
  9. The verification callback to your server
  10. What you pay
  11. Developer questions

The short answer

With two calls: POST /wa_otp_api/send with the user's number returns a request_id, then POST /wa_otp_api/verify with that request_id and the code they typed. Authentication is a public key in the X-Public-Key header, with optional HMAC-SHA256 signing using the secret. The code goes out from your connected number at Meta's rate for the recipient's country, or from the shared number at a flat price per code with no Meta account. It is the only public developer API open today.

Also asked as: «WhatsApp OTP PHP» · «WhatsApp OTP Laravel» · «WhatsApp OTP Node.js» · «OTP API» · «كيف ارسل كود تحقق على الواتساب من الموقع» · «كود OTP لا يصل» · «خدمة OTP للشركات» · «كيف أرسل OTP من API؟» · «WhatsApp OTP integration» · «WhatsApp API example» · «WhatsApp API error» · «OTP واتساب لا يصل»

Before the first line of code: an OTP app and two keys

  1. Create an OTP app in the dashboard

    From OTP in the ArabyBot dashboard: the app name, the name shown in the message, then the sender — the ready shared number, or a WhatsApp number connected to your account. You also set the code length, how long it stays valid and how many verify attempts it gets.

  2. Take the public key and the secret

    The public key starts with ab_otp_ and travels in the X-Public-Key header on every call. The secret starts with sk_otp_ and is shown once at creation: keep it in your server's environment variables — it is what you sign requests with and how you check our signature on callbacks.

  3. Choose what you lock down

    Three optional settings on the app: request signing (any call without a valid X-Signature is refused), allowed domains for browser calls, and your servers' IP addresses. Switch on the first and third if the call goes through your server.

Five endpoints — the whole contract

Server: https://app.arabybot.com · full spec at /openapi.json
EndpointBodyReturns
POST /wa_otp_api/sendphone, optional lang, length (4–8), expiry_seconds (60–900), metadatarequest_id, expires_in, expires_at
POST /wa_otp_api/verifyrequest_id, coderequest_id, phone, verified_at
POST /wa_otp_api/resendrequest_idA fresh code, same shape as send
GET /wa_otp_api/status/{request_id}—status, attempts, expires_at, verified_at
GET /wa_otp_api/ping—The app's name and settings — to check the key works

The phone is in international format, digits only or with a leading +: 966501234567. Every error comes back in one shape: {"success":false,"error_code":"…","message":"…"}, so build your logic on error_code, not on the message text.

The quickest try: curl

# 1) send the code
curl -X POST https://app.arabybot.com/wa_otp_api/send \
  -H "Content-Type: application/json" \
  -H "X-Public-Key: ab_otp_…" \
  -d '{"phone":"966501234567","lang":"ar"}'

{"success":true,"request_id":"3f6c2a9e-41b7-4c0d-9a51-7e2b8d4f1c60",
 "expires_in":300,"expires_at":"2026-10-01 14:05:00"}

# 2) verify what the user typed
curl -X POST https://app.arabybot.com/wa_otp_api/verify \
  -H "Content-Type: application/json" \
  -H "X-Public-Key: ab_otp_…" \
  -d '{"request_id":"3f6c2a9e-41b7-4c0d-9a51-7e2b8d4f1c60","code":"482913"}'

{"success":true,"request_id":"3f6c2a9e-…","phone":"966501234567",
 "verified_at":"2026-10-01 14:01:12"}

PHP — one function for every call

PHP's built-in cURL is enough, no libraries. Keep the request_id in the user's server-side session, not in the page.

<?php
// One helper for every call. Keys come from the environment, never from the page.
function arabybot_otp(string $action, array $body): array
{
    $raw     = json_encode($body);
    $headers = [
        'Content-Type: application/json',
        'X-Public-Key: ' . getenv('ARABYBOT_OTP_PUBLIC_KEY'),
    ];
    if ($secret = getenv('ARABYBOT_OTP_SECRET')) {   // only if request signing is on
        $headers[] = 'X-Signature: sha256=' . hash_hmac('sha256', $raw, $secret);
    }
    $ch = curl_init('https://app.arabybot.com/wa_otp_api/' . $action);
    curl_setopt_array($ch, [
        CURLOPT_POST => true, CURLOPT_POSTFIELDS => $raw, CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15,
    ]);
    $res = json_decode((string) curl_exec($ch), true);
    curl_close($ch);

    return is_array($res) ? $res : ['success' => false, 'error_code' => 'network'];
}

session_start();
$sent = arabybot_otp('send', ['phone' => '201001234567', 'lang' => 'ar']);
$_SESSION['otp_request'] = $sent['request_id'] ?? null;

// …later, when the user submits the code
$check = arabybot_otp('verify', ['request_id' => $_SESSION['otp_request'], 'code' => $_POST['code']]);
if (! empty($check['success'])) {
    // the number is verified
}

Laravel — through Http

Laravel's Http client writes the request in a few lines. If you switch request signing on, sign the very bytes you send: build the JSON yourself and send it with withBody, because ->post() re-encodes the array.

use Illuminate\Support\Facades\Http;

// config/services.php → 'arabybot' => ['otp_key' => env('ARABYBOT_OTP_PUBLIC_KEY')]
$otp = Http::withHeaders(['X-Public-Key' => config('services.arabybot.otp_key')])
    ->baseUrl('https://app.arabybot.com/wa_otp_api')
    ->timeout(15);

$sent = $otp->post('/send', ['phone' => $request->phone, 'lang' => 'ar'])->json();
session(['otp_request' => $sent['request_id'] ?? null]);

$check = $otp->post('/verify', [
    'request_id' => session('otp_request'),
    'code'       => $request->code,
])->json();

if ($check['success'] ?? false) {
    // verified
}

// With request signing on, sign the exact bytes you send:
// $raw = json_encode($body);
// Http::withHeaders([... 'X-Signature' => 'sha256='.hash_hmac('sha256', $raw, $secret)])
//     ->withBody($raw, 'application/json')->post('https://app.arabybot.com/wa_otp_api/send');

Node.js — fetch, no libraries

import crypto from 'node:crypto';

// Node 18+ (built-in fetch). Keys from the environment.
async function otp(action, body) {
  const raw = JSON.stringify(body);
  const headers = {
    'Content-Type': 'application/json',
    'X-Public-Key': process.env.ARABYBOT_OTP_PUBLIC_KEY,
  };
  if (process.env.ARABYBOT_OTP_SECRET) {            // only if request signing is on
    headers['X-Signature'] = 'sha256=' + crypto
      .createHmac('sha256', process.env.ARABYBOT_OTP_SECRET).update(raw).digest('hex');
  }
  const res = await fetch(`https://app.arabybot.com/wa_otp_api/${action}`,
    { method: 'POST', headers, body: raw });
  return res.json();
}

const sent  = await otp('send', { phone: '971501234567', lang: 'ar' });
const check = await otp('verify', { request_id: sent.request_id, code: '482913' });
console.log(check.success ? 'verified' : check.error_code);

Python — requests

import hashlib, hmac, json, os
import requests

def otp(action, body):
    raw = json.dumps(body)
    headers = {"Content-Type": "application/json",
               "X-Public-Key": os.environ["ARABYBOT_OTP_PUBLIC_KEY"]}
    secret = os.environ.get("ARABYBOT_OTP_SECRET")
    if secret:  # only if request signing is on
        headers["X-Signature"] = "sha256=" + hmac.new(
            secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
    return requests.post(f"https://app.arabybot.com/wa_otp_api/{action}",
                         data=raw, headers=headers, timeout=15).json()

sent = otp("send", {"phone": "9647701234567", "lang": "ar"})
check = otp("verify", {"request_id": sent["request_id"], "code": "482913"})
print("verified" if check.get("success") else check.get("error_code"))

Errors and limits — and what to do about each

Exactly as the server returns them
error_codeHTTPMeaning and what to do
missing_key · invalid_key401The public key is missing, wrong, or the app is paused.
missing_signature · bad_signature401Signing is on and the request is unsigned, or the signature covers a different body from the one sent.
origin_blocked · ip_blocked403The call came from a domain or IP not allowed in the app's settings.
invalid_phone · missing_phone422Fix the number to international format before sending.
cooldown · rate_limit_phone429Too many requests for one number. Read retry_after in seconds and show a countdown.
rate_limit_app · rate_limit_app_day429The app's limit: 60 sends a minute and 20,000 a day by default.
invalid_code401Wrong code; attempts_remaining comes with it.
too_many_attempts429Attempts used up; request a new code.
expired · already_resolved410The code expired, or was already used.
quota_exceeded · insufficient_balance · package_expired402Your plan or wallet does not cover the send.
wa_send_failed502WhatsApp refused the send; a shared-number charge is refunded.

The verification callback to your server

If you set a callback URL on the app, an otp.verified event reaches you the moment a code verifies, signed with your secret. Compute HMAC-SHA256 of the body as received and compare it with the header before you trust it.

// The callback your server receives when a code is verified (if you set a callback URL)
POST https://your-site.example/otp-callback
X-ArabyBot-Event: otp.verified
X-ArabyBot-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with your sk_otp_ secret>

{"event":"otp.verified","app_id":"12","request_id":"3f6c2a9e-…",
 "phone":"966501234567","verified_at":"2026-10-01T14:01:12+00:00"}

What you pay

On your own number the code goes out as an approved authentication template and Meta bills the recipient country's rate: $0.0036 to an Egyptian number and $0.0107 to a Saudi one, measured on 1,147 codes. On the shared number it is a flat price per code from your wallet ($0.0300), with no Meta account. The table for every Arab country is in the verification-code price in every Arab country.

Developer questions

1

Do I need my own WhatsApp API number to send the code?

No. When you create an OTP app you choose the sender: the shared number the platform runs, working within minutes with no Meta account at a flat price per code taken from your wallet, or your own number connected to the platform, sending through an approved authentication template on your number, with Meta billing you the recipient country's rate directly.

2

Do I verify by phone number or by request_id?

By request_id, always. The send call returns it; keep it in the user's session on your server, then post it with the code they typed to the verify call. A code is accepted once only, and is refused after it expires or after the attempt limit you set for the app.

3

What does one code cost?

On your own number: Meta's authentication rate for the recipient's country — $0.0036 to an Egyptian number and $0.0107 to a Saudi one; the full table is in the code price in every Arab country. On the shared number: a flat $0.0300 per code. Every resend is a new billed message.

4

What if the send fails?

The call returns wa_send_failed with HTTP 502 and WhatsApp's reason, and on the shared number you are not charged, because the deduction is refunded automatically. Show the user a "try again" button after a pause; do not resend in a loop.

5

Can I call it straight from the browser?

Technically yes — the API accepts browser calls from the domains you allow in the app's settings. But the right design routes the call through your server: that is where you keep the request_id away from the user, switch on request signing with the secret, and restrict calls to your servers' IP addresses.

6

Is there an API to send WhatsApp messages other than the code?

Not open yet. The OTP API is the only public developer API available today, alongside the order-intake and cart-event webhooks and the AI agent's tools — detailed in the developer guide.

The ArabyBot teamOfficial Meta business partner in the Middle East 188 guides Figures from our platform All guides

How we write these guides Figures come from our platform and Meta's actual invoices, dated with their sample; we describe what works today, not what we plan, and say what we do not do and when someone else is the better choice. Reviewed by the ArabyBot team on 1 October 2026.

Share this guide
And you run all of it from your phoneEverything on this page is also run from the ArabyBot mobile app — replies, orders, campaigns and AI reports. Not a web dashboard only.

Create an OTP app and send the first code

On the shared number the first code arrives within minutes of signing up, with no Meta account.