Verifications API
Start, check, fetch, resend, cancel and list one-time-passcode verifications.
The Verify API runs the whole one-time-passcode flow: eSMS generates the code, sends it, stores it hashed, expires it, and validates the user's input. Codes are rate-limited per phone and have a capped number of check attempts. For the app-level brand/template/policy, see Verify Apps.
Authentication
Authorization: Bearer esms_live_your_api_keyEvery endpoint is available under both /api/... (legacy) and /v1/... (versioned) -
e.g. POST /v1/verify/start. New integrations should pin /v1.
Start a verification
POST https://sms.esmsafrica.io/v1/verify/startPass an app_id to use a Verify App's brand, template, countries and policy - or send
the inline fields directly.
{ "to": "+254712345678", "app_id": "vera_ab12cd34ef56gh78ij90" }Or without an app:
{
"to": "+254712345678",
"channel": "sms",
"code_length": 6,
"expiry_seconds": 300,
"sender_id": "MyApp",
"template": "Your code is {code}. Expires in {min} min."
}| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient in international format. |
app_id | string | No | A Verify App id. When set, its template/sender/policy/country-allowlist apply and the inline fields below are ignored. |
channel | string | No | Only sms is available (default sms). |
code_length | integer | No | Digits in the code, 4-8 (default 6). |
expiry_seconds | integer | No | Lifetime of the code, 60-900 seconds (default 300). |
template | string | No | Custom message; must contain {code} or you get 400 template_missing_code. {min} → expiry in minutes, {app} → app name. Default: Your verification code is {code}. It expires in {min} minutes. |
sender_id | string | No | Sender ID; falls back to the route default. |
Header: Idempotency-Key: <your-unique-key> (optional) - a repeated start with the
same key within 1 hour returns the original verification (idempotent_replay: true)
without sending a second code.
Response (201)
{
"verification_id": "ver_9f8c1234ab",
"app_id": "vera_ab12",
"to": "+2547••••5678",
"channel": "sms",
"status": "pending",
"environment": "live",
"attempts": 0,
"max_attempts": 5,
"attempts_remaining": 5,
"sends": 1,
"expires_at": "2026-08-16T12:34:56+00:00",
"created_at": "2026-08-16T12:29:56+00:00",
"verified_at": null,
"cost": 0.0096,
"cost_currency": "USD"
}The to field is masked. Keep verification_id to check the code later. With a test key
the response also includes "sandbox_code": "482193" (see Test mode).
Errors
| Status | Code | Description |
|---|---|---|
400 | invalid_phone | to is not a valid number. |
400 | unsupported_channel | Only sms is supported. |
400 | template_missing_code | Inline template has no {code} placeholder. |
402 | insufficient_balance | Not enough balance to send the code. |
403 | recipient_opted_out | The recipient has opted out. |
403 | app_disabled | The Verify App is disabled. |
403 | country_not_allowed | Destination country is outside the app's allow-list. |
404 | app_not_found | app_id doesn't exist for your account. |
429 | too_many_requests | More than 5 starts for this number in the last hour. |
502 | otp_send_failed | The code couldn't be sent. You were not charged. Safe to retry. |
Check a code
POST https://sms.esmsafrica.io/v1/verify/check{ "verification_id": "ver_9f8c1234ab", "code": "482193" }| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The code the user entered. |
verification_id | string | No* | The ID returned by start. |
to | string | No* | Phone number - checks the most recent pending verification for that number. |
* Provide verification_id or to (else 400 missing_selector). Unknown id → 404 not_found.
Response
{ "status": "approved", "verification_id": "ver_9f8c1234ab" }status | Meaning |
|---|---|
approved | Code matched - the number is verified. |
pending | Wrong code, attempts remain (attempts_remaining included). |
failed | Too many wrong attempts (reason: "too_many_attempts"). |
expired | The code lifetime elapsed before a correct entry. |
canceled | The verification was canceled. |
Each wrong check consumes one attempt. To read status without consuming an attempt,
use Fetch a verification.
Fetch a verification
Read the current state without consuming an attempt.
GET https://sms.esmsafrica.io/v1/verify/{verification_id}Returns the same object shape as the start response (minus cost). A pending
verification past its expiry is returned as expired.
Resend a code
Send a fresh code for the same verification (new code, reset expiry and attempt budget). Up to 4 sends total (original + 3 resends).
POST https://sms.esmsafrica.io/v1/verify/{verification_id}/resendReturns the verification object (with sends incremented). Errors: 409 not_pending
(already approved/failed/canceled), 429 resend_limit, 502 otp_send_failed (refunded).
Cancel a verification
Void an in-flight verification (e.g. the user abandoned the flow).
POST https://sms.esmsafrica.io/v1/verify/{verification_id}/cancel{ "status": "canceled", "verification_id": "ver_9f8c1234ab" }List verifications
GET https://sms.esmsafrica.io/v1/verify?status=pending&app_id=vera_ab12&page=0&limit=20Query: status, app_id, to, page (default 0), limit (1-100, default 20). Scoped
to the key's environment (test vs live). Returns
{ "verifications": [...], "total", "page", "limit" }.
Test mode
With an esms_test_ API key:
- No real SMS is sent and there is no charge.
- The
start/resendresponse includes"sandbox_code"- the exact code to submit. - The fixed code
000000always passescheck(handy for automated tests). - Test verifications are isolated - a live key can't see or check them, and vice-versa.
Full example
# 1. Start (idempotent)
curl -X POST https://sms.esmsafrica.io/v1/verify/start \
-H "Authorization: Bearer esms_live_your_api_key" \
-H "Idempotency-Key: signup-9f8c-2026" \
-H "Content-Type: application/json" \
-d '{ "to": "+254712345678", "app_id": "vera_ab12" }'
# 2. Check
curl -X POST https://sms.esmsafrica.io/v1/verify/check \
-H "Authorization: Bearer esms_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "verification_id": "ver_9f8c1234ab", "code": "482193" }'
# 3. (optional) Resend if the user didn't get it
curl -X POST https://sms.esmsafrica.io/v1/verify/ver_9f8c1234ab/resend \
-H "Authorization: Bearer esms_live_your_api_key"You are charged the normal per-segment SMS price for each code that is actually sent
(cost in the response). A code that fails to send is refunded automatically, and
esms_test_ keys never incur a charge.