API reference

Two endpoints do the work. Everything else is optional.

Quickstart

  1. 1. Create an application in the dashboard. A test key is issued with it.
  2. 2. Call /start with a phone number, keep the returned id.
  3. 3. Call /check with that id and whatever the user typed.
# 1. Send the code
curl https://api.zameel.co/api/v1/verify/start \
  -H "Authorization: Bearer $ZAMEEL_AUTH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+966501234567" }'

# { "data": { "id": "b9c1…", "status": "pending" } }

# 2. Check what the user typed
curl https://api.zameel.co/api/v1/verify/check \
  -H "Authorization: Bearer $ZAMEEL_AUTH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "verification_id": "b9c1…", "code": "402913" }'

# { "data": { "status": "verified", "verified": true } }

Authentication

Every request carries your key as a bearer token. Keys look like zauth_live_… or zauth_test_…, and only a hash of each key is stored, so if you lose one, revoke it and create another.

Authorization: Bearer zauth_live_aeff4bfac44643ea734ba9f510593b98

Keys belong on your server. A key in a mobile app or browser bundle can be extracted and used to spend your balance.

Start a verification

POST/api/v1/verify/start

Generates a code and delivers it over WhatsApp. The code is never in the response. That is the point.

FieldTypeNotes
tostring, requiredDestination in E.164, e.g. +966501234567
languagestring, optionalOverrides the application default (ar / en)
metadataobject, optionalEchoed back on the verification record
{
  "success": true,
  "data": {
    "id": "b9c1e0d2-…",
    "status": "pending",
    "to": "+966*****4567",
    "country": "966",
    "attempts": 0,
    "delivery_status": "sent",
    "expires_at": "2026-09-03T12:05:00.000Z"
  }
}

Check a code

POST/api/v1/verify/check

Consumes one attempt. A correct code is burned immediately, so the same code can never be replayed.

FieldTypeNotes
verification_idstring, requiredThe id returned by /start
codestring, required4–10 digits, as the user typed it
{
  "success": true,
  "data": { "status": "verified", "verified": true, "attempts": 1 }
}

A wrong code returns 400 INCORRECT_CODE with attempts_remaining. After the last attempt the code is destroyed and further checks return 410 CODE_EXPIRED, so request a new one.

Other endpoints

GET/api/v1/verify/:id

Current state of a verification, without consuming an attempt.

POST/api/v1/verify/:id/cancel

Abandons a pending verification, e.g. when the user edits their number.

GET/api/v1/verify/usage

Your current hourly and daily consumption and remaining credit.

Errors

Failures return success: false, a human-readable message, and a stable errors.code. Branch on the code, not the message.

CodeHTTPMeaning
INVALID_PHONE_NUMBER400The destination is not valid E.164.
UNKNOWN_COUNTRY400The dial code could not be resolved.
INVALID_CODE_FORMAT400The submitted code is not 4–10 digits.
INCORRECT_CODE400Wrong code. `attempts_remaining` is included.
APP_INACTIVE403The application is paused.
COUNTRY_NOT_ALLOWED403That country is not enabled for this application.
INSUFFICIENT_CREDIT402Prepaid balance is exhausted.
VERIFICATION_NOT_FOUND404No such verification for this application.
VERIFICATION_NOT_PENDING409Already verified, failed, or cancelled.
CODE_EXPIRED410The code passed its lifetime.
RESEND_TOO_SOON429Cooldown active. `retry_after` is in seconds.
NUMBER_HOURLY_LIMIT429Per-destination hourly limit reached.
NUMBER_DAILY_LIMIT429Per-destination daily limit reached.
APP_HOURLY_QUOTA429Application-wide hourly quota exhausted.
APP_DAILY_QUOTA429Application-wide daily quota exhausted.
TOO_MANY_ATTEMPTS429Wrong-guess limit hit; the code is burned.
DELIVERY_FAILED502WhatsApp rejected the message.
NO_SENDER_AVAILABLE503No healthy sender. Retry shortly.

Test mode

A zauth_test_ key runs the identical path: the same rate limits, the same attempt lockout, the same records in your log, but nothing reaches WhatsApp and nothing is billed. Every test verification accepts the code 000000.

Test keys also ignore the country allowlist and your credit balance, so you can integrate fully before enabling live sending.

Ready to send one?

Create an application and copy your test key.

Get API keys