App Verification (OTP)
The Verify API (/api/v1/verify) is a turnkey One-Time Password (OTP) service that generates, hashes, delivers, rate-limits, and validates verification codes without requiring you to manage OTP state or register a template first.
How Verification Works
- Request an OTP: Call
POST /api/v1/verify/sendwith the user's phone number. Emess generates a cryptographically random numeric code, stores only its HMAC-SHA256 hash, and dispatches the SMS over the critical priority queue. - Verify the Code: When your user submits the code they received, call
POST /api/v1/verify/checkwithcodeand eitherverificationIdornumber. - Single-Use & Brute-Force Protected: Each verification allows up to 5 attempts before locking (
MAX_ATTEMPTS) and is immediately consumed onceAPPROVED.
POST/api/v1/verify/send
Generates a new OTP, cancels any previous PENDING verification for the same phone number, and dispatches the SMS on the critical priority tier.
Parameters (Body)
| Name | Type | Required | Description |
|---|---|---|---|
| number | string | Yes | Recipient phone number (E.164 or local format, e.g. +243812345678). |
| brand | string | No | Brand name displayed in the SMS body (max 32 chars). Defaults to your application's name. |
| length | integer | No | Number of digits in the generated OTP (4 to 8). Default: 6. |
| ttlSeconds | integer | No | Code validity window in seconds (60 to 900). Default: 300 (5 minutes). |
| lang | string | No | Language code: "fr" (default), "en", or DR Congo national languages: "ln" (Lingala), "sw" (Swahili), "kg" (Kikongo), "lu" (Tshiluba). Automatically selects the pre-approved public verification template for that language. |
Pre-Approved Public Verification Templates
Verification SMS messages are delivered on the critical priority tier (4.0 token multiplier). Custom templates are not accepted on the verify endpoint; instead, the system automatically uses the official public template matching your lang parameter:
- French (
fr, slug:verify-otp-fr):"Votre code de vérification {brand} est {code}. Valide pendant {minutes} min." - English (
en, slug:verify-otp-en):"Your {brand} verification code is {code}. Valid for {minutes} min." - Lingala (
ln, slug:verify-otp-ln):"Kodi ya bopimi ya {brand} ezali {code}. Ekozala na ntina miniti {minutes}." - Swahili (
sw, slug:verify-otp-sw):"Nambari yako ya uthibitisho ya {brand} ni {code}. Ni halali kwa dakika {minutes}." - Kikongo (
kg, slug:verify-otp-kg):"Kodi na nge ya lukwikilu {brand} kele {code}. Yau kele na mfunu miniti {minutes}." - Tshiluba (
lu/lua, slug:verify-otp-lu):"Kodi webe wa dishindika {brand} udi {code}. Udi ne mushinga minite {minutes}."
Example Request & Response (200 OK)
// Request: POST /api/v1/verify/send{"number": "+243812345678","brand": "AcmePay","length": 6,"ttlSeconds": 300,"lang": "fr"}// Response (200 OK) — raw OTP code is never returned{"verificationId": "b8c4e1a2-7b2f-4d9a-9f12-3e4a5b6c7d8e","to": "+243812345678","status": "PENDING","expiresAt": "2026-10-25T12:15:00.000Z","ttlSeconds": 300}
POST/api/v1/verify/check
Validates a user-supplied OTP code using timing-safe comparison (crypto.timingSafeEqual). You can identify the verification session by either verificationId or number.
Parameters (Body)
| Name | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | The OTP code entered by the user. |
| verificationId | string | Conditional | UUID returned by /verify/send. Required if number is omitted. |
| number | string | Conditional | Phone number to check against the latest pending verification. Required if verificationId is omitted. |
Responses
// 200 OK — Valid Code (Single-use: status transitions to APPROVED){"valid": true,"status": "APPROVED","verificationId": "b8c4e1a2-7b2f-4d9a-9f12-3e4a5b6c7d8e","verifiedAt": "2026-10-25T12:11:20.000Z"}// 422 Unprocessable Entity — Wrong Code (Attempts < 5){"valid": false,"status": "INVALID_CODE","remainingAttempts": 4}// 422 Unprocessable Entity — Locked Out After 5 Wrong Attempts{"valid": false,"status": "MAX_ATTEMPTS","remainingAttempts": 0}// 422 Unprocessable Entity — Expired, Not Found, or Already Consumed{"valid": false,"status": "EXPIRED"}
GET/api/v1/verify/:id
Retrieves the current status and remaining attempt count of a verification session belonging to your application. If a PENDING verification has passed its expiresAt timestamp, it is reported as EXPIRED. The hashed OTP code (codeHash) is never exposed.
// Response (200 OK){"verificationId": "b8c4e1a2-7b2f-4d9a-9f12-3e4a5b6c7d8e","to": "0812345678","status": "PENDING","attempts": 1,"remainingAttempts": 4,"expiresAt": "2026-10-25T12:15:00.000Z","verifiedAt": null,"createdAt": "2026-10-25T12:10:00.000Z"}
POST/api/v1/verify/:id/retry
Retry an OTP verification by invalidating the previous verification (CANCELED) and generating and dispatching a new OTP code to the same recipient over the critical queue. If the previous verification's SMS failed delivery (FAILED, EXPIRED, or NOT_SENT), the 60-second cooldown is automatically bypassed so legitimate users are not locked out. If the verification was already APPROVED, Emess does not send a new code and replies with "retried": false.
{"message": "Verification retried with a new code","verificationId": "d1f8a9c4-3e2b-4a8d-9b01-7c6e5d4f3a2b","previousVerificationId": "b8c4e1a2-7b2f-4d9a-9f12-3e4a5b6c7d8e","to": "0812345678","status": "PENDING","expiresAt": "2026-10-25T12:20:00.000Z","ttlSeconds": 300,"retried": true}
{"message": "Verification has already been approved","verificationId": "b8c4e1a2-7b2f-4d9a-9f12-3e4a5b6c7d8e","to": "0812345678","status": "APPROVED","verifiedAt": "2026-10-25T12:11:10.000Z","retried": false}
Rate Limits & Security
To protect your organization's token balance from SMS pumping and brute-force abuse, POST /api/v1/verify/send enforces per-number rate limits scoped to [appId, phone]:
- 60-Second Resend Cooldown: Rejects with
429 Too Many Requests(and returnsretryAfterSeconds) if a code was already requested for the same phone number within the last 60 seconds. - Sliding 15-Minute Cap: Rejects with
429 Too Many Requestsif the phone number has already requested 5 codes within the last 15 minutes. - Zero Plaintext Storage: OTP codes are hashed immediately with HMAC-SHA256 before persistence and compared in constant time.
Webhooks & Automatic Refunds
- Automatic Failure Refunds: If the underlying SMS fails to send (
sms:failed), Emess automatically refunds the deducted tokens back to your organization's active token bucket. - Webhook Events: Active webhook endpoints subscribed to
verify:approvedorverify:expiredreceive signed event payloads whenever a verification is approved or expires.