Verify OTP
Verify the OTP code entered by the user
Endpoint
POST /api/otp/verifyRequest body
| Field | Type | Required | Description |
|---|---|---|---|
request_id | string (UUID) | Yes | The request_id from Initiate OTP |
code | string | Yes | The OTP code entered by the user (4 or 6 digits) |
Example
curl -X POST https://sms.lamah.com/api/otp/verify \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"request_id": "8c8a6f2f-9a3b-4d86-9b2c-1e3f8f9c2ab1",
"code": "123456"
}'Response
{
"message": "OTP verified successfully"
}| Field | Description |
|---|---|
message | Success confirmation message |
Error responses
401 Unauthorized — Invalid code
Returned while the request still has verification attempts left.
{ "message": "Invalid OTP" }429 Too Many Requests — Attempts exhausted
All 3 attempts have been used and the OTP has been invalidated. It cannot be verified again even with the correct code — call Initiate OTP for a fresh one.
The third failed attempt returns 429 rather than 401, so you learn on the failing call instead of the next one.
{ "message": "Too many invalid OTP attempts. Request a new OTP." }401 Unauthorized — Expired OTP
{ "message": "OTP has expired" }400 Bad Request — Already verified
{ "message": "OTP already verified" }404 Not Found — Invalid request ID
Returned both when the request_id is unknown and when it belongs to another project.
{ "message": "OTP not found" }422 Unprocessable Entity — Validation failed
{
"message": "The request id field must be a valid UUID.",
"errors": {
"request_id": ["The request id field must be a valid UUID."]
}
}401 Unauthorized — Bad token
{ "message": "Invalid or missing token." }A wrong code, an expired code, and a bad API token all return 401. Distinguish them by the message field — do not treat every 401 from this endpoint as an authentication failure.
OTP flow
User enters phone → POST /otp/initiate → save request_id
User receives SMS → enters code → POST /otp/verify → success/failVerification rules
- 3 attempts: each
request_idallows exactly three wrong guesses. The limit is fixed and not configurable. Spending it invalidates the OTP permanently - Single use: once successfully verified, the OTP cannot be used again — reverifying returns
400 - Expiration: the OTP expires after the duration set in
expirationduring initiation - One active OTP per number: while an unverified, unexpired, non-invalidated OTP exists, Initiate OTP rejects new requests for that number. An OTP that was invalidated by exhausted attempts no longer blocks a new one, so a locked-out user can request a fresh code immediately rather than waiting out the expiry
The remaining attempt count is not returned. If you want to show it, track failures against the request_id on your side.
An expired OTP reports 401 OTP has expired even if its attempts were already exhausted — expiry is checked first. Do not rely on 429 alone to detect lockout near the expiry boundary.
Attempt limiting caps guesses per OTP, not per phone number. Since a locked-out request can immediately be replaced, the practical ceiling is about 30 guesses per hour per number (3 attempts × the 10/hour cap on Initiate OTP), and each replacement costs an SMS. Prefer length: 6 over length: 4 for anything sensitive, and keep expiration short.