API Documentation

The Sahla SMS API is a plain REST API over HTTPS. Every request is authenticated with an API key and returns JSON. Base URL:

https://api.sahlasms.dev/api/v1

Quick start

Pair an Android device in your dashboard, create an API key, then send your first message. The device ID is optional — Sahla SMS picks an online device automatically when you omit it.

bash
1curl -X POST https://api.sahlasms.dev/api/v1/gateway/send-sms \
2 -H "Content-Type: application/json" \
3 -H "x-api-key: $SAHLA_API_KEY" \
4 -d '{
5 "recipients": ["+1234567890"],
6 "message": "Hello from Sahla SMS!"
7 }'

Authentication

Pass your API key in the x-api-key header on every request. Keys are shown once at creation — store them in your server environment and never expose them in client-side code.

http
x-api-key: sk_live_3f9a...c21d

Keys can be scoped to sms:send, sms:read, devices:read, and webhooks:manage. Revoking a key takes effect immediately.

Send a single SMS

POST/gateway/send-sms

request
1{
2 "recipients": ["+1234567890"],
3 "message": "Your verification code is 481932",
4 "deviceId": "dev_8c21f0" // optional
5}
response 200
1{
2 "success": true,
3 "data": {
4 "messageId": "msg_7b12ce",
5 "status": "queued",
6 "segments": 1,
7 "recipients": 1,
8 "queuedAt": "2026-01-14T09:21:44Z"
9 }
10}

Send bulk SMS

POST/gateway/send-bulk-sms

Pass up to 1,000 recipients per request, or a saved contact group. Messages are queued and sent steadily by your device with a short pause between each.

request
1{
2 "recipients": ["+1234567890", "+212612345678"],
3 "groupId": "grp_4a91bd", // optional, merged with recipients
4 "message": "Hi {{name}}, your order has shipped.",
5 "variables": { "name": "Sam" }
6}
response 200
1{
2 "success": true,
3 "data": {
4 "batchId": "bat_19fd03",
5 "queued": 2,
6 "rejected": 0,
7 "estimatedCompletion": "2026-01-14T09:24:10Z"
8 }
9}

List devices

GET/gateway/devices

response 200
1{
2 "success": true,
3 "data": [
4 {
5 "id": "dev_8c21f0",
6 "name": "Pixel 6a",
7 "phoneNumber": "+1234567890",
8 "status": "online",
9 "batteryLevel": 87,
10 "signalStrength": 4,
11 "lastSeenAt": "2026-01-14T09:21:02Z",
12 "messagesSent": 1842
13 }
14 ]
15}

Message status

GET/gateway/messages/:id

A message moves through queuedsentdelivered. A failure returns failed with an errorReason.

response 200
1{
2 "success": true,
3 "data": {
4 "id": "msg_7b12ce",
5 "recipient": "+1234567890",
6 "status": "delivered",
7 "segments": 1,
8 "queuedAt": "2026-01-14T09:21:44Z",
9 "sentAt": "2026-01-14T09:21:47Z",
10 "deliveredAt": "2026-01-14T09:21:52Z"
11 }
12}

Receive SMS

GET/gateway/messages?direction=inbound

Enable SMS receiving in the Android app, then poll inbound messages or — better — subscribe to the sms.received webhook event for real-time delivery.

Webhooks

Register a webhook URL in the dashboard and subscribe to sms.sent, sms.delivered, sms.failed, sms.received, and device.status. Every request carries an x-sahla-signature header: an HMAC-SHA256 of the raw body using your signing secret.

payload
1{
2 "event": "sms.delivered",
3 "timestamp": "2026-01-14T09:21:52Z",
4 "data": {
5 "messageId": "msg_7b12ce",
6 "deviceId": "dev_8c21f0",
7 "recipient": "+1234567890",
8 "status": "delivered"
9 }
10}
verify (node)
1import { createHmac, timingSafeEqual } from 'crypto'
2
3const expected = createHmac('sha256', process.env.SIGNING_SECRET)
4 .update(rawBody)
5 .digest('hex')
6
7const ok = timingSafeEqual(
8 Buffer.from(signatureHeader),
9 Buffer.from(expected),
10)

Failed deliveries are retried with exponential backoff for up to 24 hours. Respond with any 2xx within 10 seconds to acknowledge.

Errors & rate limits

Errors return a non-2xx status with a JSON body. The API allows 60 requests per minute per key; exceeding it returns 429 with a Retry-After header.

response 400
1{
2 "success": false,
3 "error": {
4 "code": "invalid_recipient",
5 "message": "Recipient +12345 is not a valid E.164 phone number"
6 }
7}
  • 401 unauthorized — missing, revoked, or malformed API key
  • 403 insufficient_scope — the key lacks the required scope
  • 409 no_device_available — no paired device is currently online
  • 422 quota_exceeded — monthly plan quota reached