eSMS AfricaeSMS Africa
eSMS Verify

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_key

Every 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/start

Pass 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."
}
FieldTypeRequiredDescription
tostringYesRecipient in international format.
app_idstringNoA Verify App id. When set, its template/sender/policy/country-allowlist apply and the inline fields below are ignored.
channelstringNoOnly sms is available (default sms).
code_lengthintegerNoDigits in the code, 4-8 (default 6).
expiry_secondsintegerNoLifetime of the code, 60-900 seconds (default 300).
templatestringNoCustom 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_idstringNoSender 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

StatusCodeDescription
400invalid_phoneto is not a valid number.
400unsupported_channelOnly sms is supported.
400template_missing_codeInline template has no {code} placeholder.
402insufficient_balanceNot enough balance to send the code.
403recipient_opted_outThe recipient has opted out.
403app_disabledThe Verify App is disabled.
403country_not_allowedDestination country is outside the app's allow-list.
404app_not_foundapp_id doesn't exist for your account.
429too_many_requestsMore than 5 starts for this number in the last hour.
502otp_send_failedThe 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" }
FieldTypeRequiredDescription
codestringYesThe code the user entered.
verification_idstringNo*The ID returned by start.
tostringNo*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" }
statusMeaning
approvedCode matched - the number is verified.
pendingWrong code, attempts remain (attempts_remaining included).
failedToo many wrong attempts (reason: "too_many_attempts").
expiredThe code lifetime elapsed before a correct entry.
canceledThe 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}/resend

Returns 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=20

Query: 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/resend response includes "sandbox_code" - the exact code to submit.
  • The fixed code 000000 always passes check (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.

On this page