OTP
Let SMSRay generate, send and check verification codes. You never store the code, and abuse limits are enforced for you.
On this page
Send an OTP
POST
/sms/otp/sendhttps://api.smsray.com/api/sms/v1/sms/otp/sendGenerate a 6-digit code, send it as a branded one-segment SMS, and store only its SHA-256 hash. The code is valid for 5 minutes.
Request
Headers
| Name | Type | Required | Rules |
|---|---|---|---|
x-api-key | string | required | Your secret key, ls_live_ followed by 48 hex characters. |
content-type | string | required | application/json |
Idempotency-Key | string | optional | 1–200 printable ASCII characters. Safe retries for 24 h. See Idempotency. |
Body parameters (JSON)
| Name | Type | Required | Rules |
|---|---|---|---|
to | string | required | Recipient. E.164 is recommended (+9779801234567); the national format is accepted for your workspace's country (for a Nepal workspace 98XXXXXXXX, 97… or 96…, with or without 977). Must match ^\+?\d{7,15}$. See Numbers & countries. |
purpose | string | optional | Up to 50 characters. Defaults to default. Codes are scoped to (your client, to, purpose), so login and reset-password never collide. |
ip | string | optional | Your end user's IP address, up to 64 characters. Enables the per-IP limit (10 OTPs per 10 minutes). |
senderId | string | optional | Optional; same meaning as on /sms/send. |
Example request
curl https://api.smsray.com/api/sms/v1/sms/otp/send \
-H "x-api-key: $SMSRAY_API_KEY" \
-H "content-type: application/json" \
-d '{ "to": "+9779801234567", "purpose": "login", "ip": "203.0.113.7" }'Response
200
200 application/json
{
"success": true,
"otpId": "9d5c0b7e-3a1f-4e62-8b1d-6a4f2c9e0d17",
"messageId": "4f8a2c61-0b9e-4d3a-a7c5-1e6f9b2d8c40",
"expiresInSeconds": 300
}429Resend too soon
429 application/json
{
"error": "Please wait before requesting another code",
"code": "rate_limited"
}Response fields
| Name | Type | Required | Rules |
|---|---|---|---|
otpId | string (uuid) | required | Id of the code record. You never need it to verify. |
messageId | string (uuid) | required | The SMS that carries the code; track it like any message. |
expiresInSeconds | integer | required | Always 300 (5 minutes). |
Errors
Every error uses the same shape: { "error", "code", "details"? }. Branch on code, not on the message.
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | Validation failed (see details). |
| 400 | invalid_number | The number can't receive SMS — for example a landline, or a number that fails its country's mobile rules. |
| 402 | insufficient_balance | Not enough balance for one OTP-rate segment. |
| 401 | unauthorized | The x-api-key header is missing ("Missing x-api-key") or the key is unknown ("Invalid API key"). |
| 403 | client_disabled | The API client that owns this key has been disabled in the dashboard. |
| 403 | forbidden | OTP messages are not allowed on your plan or workspace. |
| 403 | workspace_pending | Your workspace has not been activated yet, so it cannot send. |
| 403 | workspace_suspended | Your workspace is suspended. |
| 409 | idempotency_conflict | The same Idempotency-Key was used with a different body, or the first request is still running (Retry-After: 1). |
| 422 | destination_not_supported | A valid number in a country your workspace can't send to yet. details is { country, allowed }; show "We don't deliver to <country> yet". |
| 429 | rate_limited | Resend cooldown (60 s per number and purpose), 3 OTPs per number per 10 minutes, 10 per end-user IP per 10 minutes, 60 requests per minute per calling IP, or your per-client rate. Retry-After is set whenever it is known. |
| 503 | unavailable | Delivery is temporarily unavailable. Nothing was charged; retry with backoff. |
| 500 | server_error | Something went wrong on our side. The body never contains a stack trace. |
Notes
- The SMS reads:
<App>: 482913 is your verification code. Valid for 5 minutes. Do not share it.<App>is your API client's brand name (GSM-7, up to 30 characters, set in the dashboard). - The message uses SMSRay's built-in OTP template, so no template approval is needed. It is billed at your OTP rate and always fits in one GSM-7 segment.
- The code is never stored or shown in plain text. In the dashboard and in
GET /sms/messages/:idthe text appears as(OTP hidden). - The per-number limit counts across every SMSRay customer, so one phone number cannot be flooded with codes from many apps.
- OTPs are sent immediately and are never deferred.
Verify an OTP
POST
/sms/otp/verifyhttps://api.smsray.com/api/sms/v1/sms/otp/verifyCheck the code your user typed against the latest active code for the same number and purpose. Codes are single use.
Request
Headers
| Name | Type | Required | Rules |
|---|---|---|---|
x-api-key | string | required | Your secret key, ls_live_ followed by 48 hex characters. |
content-type | string | required | application/json |
Idempotency-Key | string | optional | 1–200 printable ASCII characters. Safe retries for 24 h. See Idempotency. |
Body parameters (JSON)
| Name | Type | Required | Rules |
|---|---|---|---|
to | string | required | The same number you sent the code to. |
code | string | required | The 6 digits your user entered. |
purpose | string | optional | Must match the purpose used on send. Defaults to default. |
Example request
curl https://api.smsray.com/api/sms/v1/sms/otp/verify \
-H "x-api-key: $SMSRAY_API_KEY" \
-H "content-type: application/json" \
-d '{ "to": "+9779801234567", "purpose": "login", "code": "482913" }'Response
200Correct code
200 application/json
{ "success": true, "verified": true }200Wrong code
200 application/json
{ "success": true, "verified": false, "reason": "invalid_code" }Response fields
| Name | Type | Required | Rules |
|---|---|---|---|
verified | boolean | required | true once, for the correct code. The code is then consumed. |
reason | string | optional | Present when verified is false: invalid_code, too_many_attempts (5 wrong tries) or no_active_otp (none sent, expired or already used). |
Errors
Every error uses the same shape: { "error", "code", "details"? }. Branch on code, not on the message.
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 401 | unauthorized | The x-api-key header is missing ("Missing x-api-key") or the key is unknown ("Invalid API key"). |
| 403 | client_disabled | The API client that owns this key has been disabled in the dashboard. |
| 403 | workspace_pending | Your workspace has not been activated yet, so it cannot send. |
| 403 | workspace_suspended | Your workspace is suspended. |
| 409 | idempotency_conflict | The same Idempotency-Key was used with a different body, or the first request is still running (Retry-After: 1). |
| 429 | rate_limited | You exceeded your per-client request rate (default 20 requests per second). Honour Retry-After. |
| 500 | server_error | Something went wrong on our side. The body never contains a stack trace. |
Notes
- A wrong code is not an HTTP error. Always read
verified; never treat a 200 as success on its own. - Each wrong code counts as an attempt. After 5 wrong attempts the code is locked and every further check returns
too_many_attempts— send a new code. - Codes expire 5 minutes after they are sent and are deleted automatically afterwards.