Developer Documentation

Zoril SMS API

Send and receive SMS through your own paired Android phones with a simple REST API. Base URL: https://sms.zoril.app

Quickstart

  1. Create an account and sign in.
  2. Install the Zoril SMS Android app on the phone you want to use as a gateway and pair it from Devices → Add device. A simulator device is available for testing without a real phone.
  3. Generate an API key at API Keys → New key. Copy it — it will only be shown once. Keys start with pk_live_.
  4. Send your first SMS with the request below.
curl -X POST https://sms.zoril.app/api/public/v1/sms/send \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"to":"+15551234567","message":"Hello from Zoril SMS"}'

Authentication

Every request must include a bearer token in the Authorization header:

Authorization: Bearer pk_live_your_key_here

Revoked keys are permanently deleted and stop working immediately. Store keys in environment variables — never in client-side code or public repos.

Send an SMS

POST/api/public/v1/sms/send

Request body

FieldTypeDescription
tostringRecipient phone number in E.164 format (e.g. +15551234567).
messagestringSMS body, 1–1600 characters. Long messages are auto-segmented on the device.
device_iduuid?Optional. If omitted, we route to the most recently online paired device.

Response — 202 Accepted

{
  "id": "b1e5c8f3-...-2a",
  "status": "queued",
  "to": "+15551234567",
  "created_at": "2026-07-09T13:14:15.000Z"
}

Simulator devices return status: "delivered" immediately. Real devices start at queued and progress to sentdelivered (or failed).

Node.js example

const res = await fetch("https://sms.zoril.app/api/public/v1/sms/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ZORIL_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ to: "+15551234567", message: "Hi from Node" }),
});
const data = await res.json();
console.log(data.id, data.status);

Python example

import os, requests
r = requests.post(
    "https://sms.zoril.app/api/public/v1/sms/send",
    headers={"Authorization": f"Bearer {os.environ['ZORIL_API_KEY']}"},
    json={"to": "+15551234567", "message": "Hi from Python"},
    timeout=15,
)
r.raise_for_status()
print(r.json())

Message status

GET/api/public/v1/messages/{id}

curl https://sms.zoril.app/api/public/v1/messages/b1e5c8f3-...-2a \
  -H "Authorization: Bearer pk_live_..."
{
  "id": "b1e5c8f3-...-2a",
  "direction": "out",
  "to_number": "+15551234567",
  "from_number": null,
  "body": "Hello from Zoril SMS",
  "status": "delivered",
  "error": null,
  "created_at": "2026-07-09T13:14:15.000Z",
  "sent_at":    "2026-07-09T13:14:17.000Z",
  "delivered_at":"2026-07-09T13:14:19.000Z",
  "retry_count": 0
}

Status values: queued, sent, delivered, failed, received (inbound).

Inbound SMS

Paired Android devices post incoming SMS to Zoril automatically — you do not need to call this endpoint yourself. It is documented for custom integrations.

POST/api/public/v1/sms/inbound

{
  "from": "+15551234567",
  "message": "STOP",
  "device_id": "optional-uuid",
  "received_at": "2026-07-09T13:14:20.000Z"
}

Inbound messages trigger your enabled automations (keyword, regex, or catch-all → auto-reply, forward, or webhook) and fire the message.inbound webhook event.

Webhooks

Add a webhook URL under Webhooks to receive signed events for inbound and outbound message updates.

Event payload

POST https://your-app.example.com/hook
content-type: application/json
x-zoril-signature: sha256=<hex>
x-zoril-event: message.inbound
x-zoril-timestamp: 1783600455

{
  "event": "message.inbound",
  "timestamp": "2026-07-09T13:14:20.000Z",
  "data": {
    "id": "...",
    "from_number": "+15551234567",
    "body": "STOP",
    "device_id": "...",
    "created_at": "..."
  }
}

Verifying the signature (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(header ?? "");
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

We retry failed deliveries with exponential backoff. Respond with 2xx within 10 seconds to acknowledge.

Idempotency

Pass a unique Idempotency-Key header on POST /sms/send to safely retry on network failures. Duplicate requests with the same key return the original message with idempotent: true.

Idempotency-Key: 8f14e45f-ceea-467a-a7a1-d59d1cbb0f14

Rate limits & quotas

  • Free plan: 100 SMS / month.
  • Business plan: 10,000 SMS / month.
  • API burst limit: 60 requests / minute / key.
  • Requests over quota return 402 quota_exceeded.

Errors

Errors use standard HTTP status codes and a JSON body: { "error": "code", "details": "..." }

StatusCodeMeaning
400invalid_inputBody failed validation.
401missing_api_key / invalid_api_keyBearer token missing, wrong, or revoked.
402quota_exceededMonthly SMS quota reached.
404device_not_found / not_foundResource does not exist on your account.
409no_online_deviceNo paired device is online to send from.
429rate_limitedToo many requests — back off and retry.
5xxserver_errorRetry with idempotency key.

Android gateway app

Pair any Android phone (Android 8+) by installing the Zoril SMS gateway APK and scanning the pairing QR code shown in Devices → Add device. The app runs a foreground service that polls Zoril for outbound messages, sends them via the phone's SIM, acknowledges delivery, and forwards inbound SMS back to your account.

  • Grant Send SMS and Receive SMS permissions.
  • Disable battery optimization for the app so the service stays alive.
  • A paired device stays linked until you explicitly delete it — deletion is permanent.
Need help? Email support@zoril.app.