Skip to content

WhatsApp OTP verification (v1) ​

Otpera's OTP API sends a one-time code to a phone number on WhatsApp and checks the code your user types. Otpera generates the code, delivers it, enforces expiry and attempt limits, and stores it only as a hash. Your server never sees the code.

Use it for sign-up and login, phone-number verification, checkout confirmation, password resets and two-factor authentication.

How it works ​

text
1. Your server  ── POST /verifications {to} ─────────▶ Otpera
2. Otpera       ── WhatsApp: "Your code is 482913" ──▶ User
3. User         ── types 482913 into your app ───────▶ Your server
4. Your server  ── POST /verifications/:id/check ────▶ Otpera → { valid: true }
StepEndpointPurpose
1POST /verificationsCreate a verification and send the code
2POST /verifications/{id}/checkCheck the code the user entered
–POST /verifications/{id}/resendSend a new code for the same verification
–GET /verifications/{id}Read the current status

All endpoints need an API key with the verifications:write scope. Base URL: https://api.otpera.com/api/v1.

Prerequisites ​

  1. A workspace on otpera.com with a WhatsApp number linked (Dashboard → WhatsApp, scan the QR).
  2. An API key with verifications:write (Dashboard → API keys). See Authentication.

1. Create a verification ​

POST /verifications

Request body ​

FieldTypeRequiredDescription
tostringyesRecipient phone in E.164 (+201012345678). Egyptian local numbers (01012345678) are also accepted.
purposestringnoYour label, a-z 0-9 _ . -, up to 40 chars. Default verification. E.g. signup, login, checkout.
locale"ar" | "en"noLanguage of the WhatsApp message. Defaults to the workspace language.
channel"whatsapp"noOnly WhatsApp today; accepted so your code can be explicit.

Send an Idempotency-Key header so a retried request never sends a second code.

bash
curl -X POST https://api.otpera.com/api/v1/verifications \
  -H "Authorization: Bearer $OTPERA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2b9e-1c55-4c1e-8f0a-3f7b0e7f9a21" \
  -d '{ "to": "+201012345678", "purpose": "signup", "locale": "ar" }'
js
const res = await fetch('https://api.otpera.com/api/v1/verifications', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OTPERA_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({ to: '+201012345678', purpose: 'signup', locale: 'ar' }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const { data } = await res.json();
// Save data.id with the user's session.
php
$res = Http::withToken(config('services.otpera.key'))
    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
    ->post('https://api.otpera.com/api/v1/verifications', [
        'to' => '+201012345678',
        'purpose' => 'signup',
        'locale' => 'ar',
    ]);

session(['otpera_verification_id' => $res->json('data.id')]);
python
import os, uuid, requests

res = requests.post(
    "https://api.otpera.com/api/v1/verifications",
    headers={
        "Authorization": f"Bearer {os.environ['OTPERA_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"to": "+201012345678", "purpose": "signup", "locale": "ar"},
    timeout=15,
)
res.raise_for_status()
verification_id = res.json()["data"]["id"]

Response 201 Created ​

json
{
  "data": {
    "id": "8b0c3f8e-2f7a-4f5e-9a51-0d6c1e6b2f10",
    "recipient_e164": "+201012345678",
    "purpose": "signup",
    "channel": "whatsapp",
    "status": "pending",
    "attempts": 0,
    "max_attempts": 5,
    "send_count": 1,
    "delivery_status": "sent",
    "delivery_error": null,
    "expires_at": "2026-11-05T10:10:00.000Z",
    "verified_at": null,
    "last_sent_at": "2026-11-05T10:00:00.000Z",
    "created_at": "2026-11-05T10:00:00.000Z"
  }
}

The user receives a WhatsApp message like:

Your My Store verification code is 482913. It expires in 10 minutes. Don't share it with anyone.

A new verification for the same number and purpose cancels any earlier pending one, so only the latest code works.

2. Check the code ​

POST /verifications/{id}/check

FieldTypeRequiredDescription
codestringyesThe 4–8 digit code the user typed
bash
curl -X POST https://api.otpera.com/api/v1/verifications/8b0c3f8e-2f7a-4f5e-9a51-0d6c1e6b2f10/check \
  -H "Authorization: Bearer $OTPERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "482913" }'

Response 200 OK ​

json
{
  "data": {
    "id": "8b0c3f8e-2f7a-4f5e-9a51-0d6c1e6b2f10",
    "valid": true,
    "status": "verified",
    "reason": null,
    "attemptsRemaining": 4
  }
}

Always branch on valid. When it is false, reason tells you why:

reasonstatusWhat to show the user
incorrect_codepending"Wrong code" + attemptsRemaining
too_many_attemptsfailed"Too many attempts. Request a new code."
expiredexpired"Code expired. Request a new code."
already_verifiedverifiedThe code was already used; treat as a replay
canceledcanceledA newer code was requested; ask for the latest one

A code verifies at most once, even if two checks arrive at the same time.

3. Resend a code ​

POST /verifications/{id}/resend. No body. Sends a fresh code for the same verification and extends its expiry.

  • 30-second cooldown between sends (429 with retryAfter seconds).
  • Up to 5 sends per verification; after that, create a new verification.
  • Only while the verification is pending (409 otherwise).

4. Get a verification ​

GET /verifications/{id} returns the same object as the create response. The code is never included.

Statuses ​

StatusMeaning
pendingCode sent; waiting for a check
verifiedThe correct code was checked
expired10 minutes passed without a correct check
failedToo many wrong attempts, or WhatsApp delivery failed
canceledSuperseded by a newer verification for the same number/purpose

Limits ​

LimitValue
Code lifetime10 minutes
Wrong attempts per verification5
Sends per verification5
Resend cooldown30 seconds
New verifications per phone number5 per 10 minutes
New verifications per workspace300 per hour

Each delivered code uses 1 credit from your plan.

Errors ​

HTTPWhen
401Missing or invalid API key
403Key lacks verifications:write
404Verification id not found in this workspace
409No WhatsApp number linked (whatsapp_not_linked), or not pending
422Invalid phone number, bad purpose, non-digit code, or bad Idempotency-Key
429Rate limit or resend cooldown

Full format: Errors & rate limits.

Best practices ​

  • Call Otpera from your server only. Your frontend talks to your backend; your backend holds the API key.
  • Bind the verification to the session. Store data.id server-side with the user's session; don't trust an id sent back from the browser alone.
  • Rate-limit your own endpoint that triggers POST /verifications (per IP and per account) to stop SMS-pumping style abuse.
  • Show a resend timer of 30 seconds in your UI to match the cooldown.
  • Normalize phone numbers to E.164 before sending; Otpera rejects invalid numbers with 422.
  • Use purpose to separate flows (e.g. login vs checkout) so one doesn't cancel the other.

otpera.com · WhatsApp order confirmation & OTP