Developer hub
Ship SMS before your coffee cools
Six REST endpoints, one auth header, one error shape. Send messages and one-time passwords from any language that can make an HTTPS request — Nepal is live today, with more countries on the way.
Base URL
https://api.smsray.com/api/sms/v1
Auth
x-api-key: <your key>
Errors
{ "error", "code", "details"? }
Quickstart
Three steps to your first message
- Step 1
Get your account
Talk to sales and the SMSRay team creates your workspace. Then sign in at app.smsray.com with a one-time code sent to your phone or email.
- Step 2
Create an API key in Clients
Open Clients, add one per app or environment, and copy the key and webhook secret — they're shown once and stored only as hashes. Your keys and live examples live in the portal.
- Step 3
Send
POST to
/sms/sendwith thex-api-keyheader. You get amessageIdback instantly and the status follows. Custom messages use templates approved by SMSRay; OTP uses our built-in template, so it works on day one.
Send an SMS
One POST. The reply carries the messageId, segment count and encoding.
curl https://api.smsray.com/api/sms/v1/sms/send \
-H "x-api-key: $SMSRAY_API_KEY" \
-H "content-type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{ "to": "+9779801234567", "text": "Your order #1042 has shipped." }'Verify a phone number
We generate the code, send it and check it. Valid 5 minutes, 5 attempts.
# 1. Send a code (6 digits, valid 5 minutes)
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" }'
# 2. Check what the user typed
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" }'| Method | Path | What it does |
|---|---|---|
| POST | /sms/send | Send a transactional, OTP or promotional (Enterprise) message |
| POST | /sms/otp/send | Generate and send a 6-digit code |
| POST | /sms/otp/verify | Check the code your user typed |
| GET | /sms/messages/:id | Look up a message and its status |
| GET | /sms/balance | Workspace balance, rates and your counters |
| GET | /sms/coverage | Where this key can send, and where SMSRay delivers |
API reference
Everything you need, nothing you don't
Short pages, real examples, every field and error code.
Quickstart
Your first SMS, step by step
Authentication
The x-api-key header and key hygiene
Test mode
Recorded, never sent, free
API reference
Every endpoint, OpenAPI and Postman
Send SMS
POST /sms/send — fields and responses
OTP
Send and verify six-digit codes
Message status
GET /sms/messages/:id and statuses
Balance
GET /sms/balance, rates and counters
Coverage
GET /sms/coverage — where you can send
Numbers & countries
E.164, national format, 422 explained
Webhooks
Signed message.status and message.inbound
Errors and codes
One error shape, every code explained
Rate limits
Per-client send rate (20/s default) and OTP limits
Idempotency
Safe retries with Idempotency-Key (24 h)
Encoding & segments
GSM-7, Unicode and how segments are counted
Changelog
What changed, and when
Webhooks
Verify every event in a few lines
Each delivery carries x-lacspace-signature: t=…,v1=… — an HMAC-SHA256 of the timestamp and raw body with your webhook secret.
- Events:
message.statusandmessage.inbound - Reject timestamps outside ±300 s to stop replays
- During secret rotation two
v1values are sent — accept either - 8 attempts with backoff: 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h
- Respond 2xx within 10 s; use
x-lacspace-deliveryto de-duplicate
import crypto from "node:crypto";
// x-lacspace-signature: t=<unix>,v1=<hex>[,v1=<hex during secret rotation>]
export function verifySmsrayWebhook(rawBody, header, secret, toleranceSec = 300) {
const pairs = header.split(",").map((p) => p.split("="));
const t = Number(pairs.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), "hex");
return pairs
.filter(([k]) => k === "v1")
.some(([, v]) => {
const sig = Buffer.from(v, "hex");
return sig.length === expected.length && crypto.timingSafeEqual(sig, expected);
});
}Testing tips
Build it right the first time
Use test mode, then your own number
Put a key in test mode while you integrate: messages are recorded, never sent, and free. Then send a real OTP to your own number in E.164 (+9779801234567) and watch it move in the dashboard.
Always send an Idempotency-Key
Use something stable like order-1042-shipped. If your job retries, the replay returns the original response with Idempotent-Replayed: true — no second SMS, no second charge.
Poll status while you build
Before your webhook endpoint is live, call GET /sms/messages/:id to follow a message through queued, sent and delivered. Switch to webhooks in production.
Treat a wrong OTP as data
A wrong code returns 200 with verified: false and a reason. Pass the end user's ip on send to turn on per-IP limits.
SDKs
REST-first. Official libraries coming soon.
Today SMSRay is a plain JSON-over-HTTPS API, so it works from any stack with no package to install. The docs show cURL, Node.js, Python, PHP and Go for every endpoint, and the OpenAPI 3.1 file lets you generate a client for anything else.
Specs and PDF
Generated from the docs, so they always match.
What's new
10 Oct 2026: templates library, GET /sms/coverage, E.164 numbers and 422 destination_not_supported.
Developer support
Send the messageId and error code, never your full key.
- Integration help: support@lacspace.com
- Security: security@lacspace.com
- Plans and limits: sales@lacspace.com
Get your API key
Talk to sales and we set up your workspace. Sign in, add a client and send your first message from the terminal. Plans from NPR 3,000 a month.
Already a customer? Log in