RIVIL SMS API
Send and receive texts from your Android phones, from any app.
API address: https://sms.rivil.app. All requests and responses are JSON.
How it works
Messages go out from the SIM of an Android phone running the RIVIL SMS app. Your app queues the message through the API, and the phone picks it up within seconds.
| App | Your API account. You get a key and a webhook secret from us. |
| Company | One of your accounts, with its phones. You identify it with your own id. If you only have one company, you create just one. |
| Phone | A company phone, optionally tied to a location. Each company has a default phone. |
Your first text
1. Create the company
curl -X PUT https://sms.rivil.app/v1/companies/my-company \
-H "Authorization: Bearer $RIVIL_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "My Company" }'
2. Add a phone
curl -X POST https://sms.rivil.app/v1/companies/my-company/devices \
-H "Authorization: Bearer $RIVIL_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Front desk" }'
The response contains id, the phone's id.
3. Connect the phone
curl -X POST https://sms.rivil.app/v1/companies/my-company/devices/$DEVICE_ID/pairing \
-H "Authorization: Bearer $RIVIL_SMS_KEY"
Install the app from the installation page, then type the code from the response into it, or scan a QR code made from qr. The code is valid for 15 minutes and works once.
4. Send
curl -X POST https://sms.rivil.app/v1/companies/my-company/messages \
-H "Authorization: Bearer $RIVIL_SMS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1024" \
-d '{ "to": "+40712345678", "text": "Your order has shipped." }'
{ "id": "1042", "status": "queued" }
Authentication
Send the key with every request, in the Authorization header:
Authorization: Bearer rs_...
The key is shown only once, when it is created. Keep it on your server only, never in a browser or mobile app. If someone else gets it, write to us and we will replace it. Check that it works with GET /v1/me.
Companies
You choose the {company} identifier: letters, digits, -, _ and ., up to 120 characters. You only see your own app's companies.
/v1/companiesAll your companies.
/v1/companies/{company}Creates the company or updates its details. Safe to call any number of times.
| Field | Type | Description |
|---|---|---|
name | text, required | The company name, up to 200 characters. |
fallback | boolean | If the location's phone goes silent, the message can leave from another phone after 10 minutes. Default false. |
/v1/companies/{company}Phones
/v1/companies/{company}/devicesThe company's phones, with their status:
{ "devices": [{
"id": "41879465-fc0e-4573-8a8c-8f71803f540a",
"name": "Front desk",
"location_ref": null,
"is_default": true,
"paired": true,
"online": true,
"last_seen_at": "2026-11-04T10:23:20Z",
"battery": 69,
"charging": false,
"signal": 3,
"model": "samsung SM-S921B",
"app_version": "1.0.4",
"settings": { "sim": 1, "start": "08:00", "end": "20:00", "per_minute": 6, "retries": 1, "inbound": true, "calls": false, "outgoing": false, "history": false }
}] }
/v1/companies/{company}/devices| Field | Type | Description |
|---|---|---|
name | text, required | Up to 80 characters. |
location_ref | text | The phone's location, using your own identifier. |
is_default | boolean | A company's first phone is the default. |
/v1/companies/{company}/devices/{id}Changes name, location_ref, is_default or settings. Send the whole settings object, with all the fields you received when reading it.
| Setting | Values | Description |
|---|---|---|
sim | 1 or 2 | The SIM used for sending. |
start, end | HH:MM | The sending window, Romanian time. |
per_minute | 1 - 30 | Texts per minute. |
retries | 0 - 5 | Retries for a failed message. |
inbound | boolean | Reports received texts. |
calls | boolean | Reports the call log. |
outgoing | boolean | Reports texts written by hand on the phone. Off by default. |
history | boolean | Imports the phone's old texts once, received and sent. Off by default: without it, the log starts when the phone is connected. |
/v1/companies/{company}/devices/{id}/pairingA new connection code: { "code", "qr", "expires_at" }. Cancels the phone's older codes.
/v1/companies/{company}/devices/{id}Removes the phone. Its history stays.
Messages
/v1/companies/{company}/messagesQueues a text. Responds 202 with { "id", "status": "queued" }.
| Field | Type | Description |
|---|---|---|
to | text, required | The number in international format, for example +40712345678. |
text | text, required | Up to 1600 characters. A text holds 160 characters, or 70 with special characters; a longer one goes out in several parts. |
location_ref | text | Sends from this location's phone. |
expires_at | ISO 8601 date | If the message has not been picked up by then, it closes as failed. Default: 24 hours. |
Choosing the phone: the location's phone; if the location has no phone or the message has no location, the default phone.
The Idempotency-Key header (optional, up to 200 characters): if you repeat the request with the same key, no second text is sent. We recommend it for any automatic retry.
Message statuses
queued | Waiting for a phone, or for its sending window. |
claimed | Picked up by the phone. |
sent | Left the phone. |
delivered | The carrier confirmed delivery. |
failed | Could not be sent or has expired; the reason is in error. |
/v1/messages/{id}{ "id": "1042", "status": "delivered", "error": null, "sent_at": "2026-11-04T10:00:03Z", "delivered_at": "2026-11-04T10:00:07Z" }
/v1/companies/{company}/messagesThe list of messages, newest first. Filters: status, limit (1 - 100, default 50) and before (for the next page, the last id you received).
Replies and calls
The phone reports this data only if it has inbound, outgoing or calls turned on, respectively. The lists accept limit and before, like messages.
/v1/companies/{company}/inbound{ "inbound": [{ "id": "88", "device_id": "...", "from": "+40712345678", "text": "YES", "sim": "SIM 1", "received_at": "2026-11-04T10:05:00Z" }] }
/v1/companies/{company}/outgoingTexts written by hand on the phone, from the moment you turned on outgoing. Texts sent through the API do not appear here.
{ "outgoing": [{ "id": "7", "device_id": "...", "to": "0712345678", "text": "Back in a minute", "sent_at": "2026-11-04T10:06:00Z" }] }
/v1/companies/{company}/callsdirection is incoming, outgoing, missed or rejected, and duration is in seconds.
{ "calls": [{ "id": "12", "device_id": "...", "number": "+40712345678", "direction": "outgoing", "duration": 95, "sim": "SIM 1", "occurred_at": "2026-11-04T09:00:00Z" }] }
Webhooks
Give us a public HTTPS address and you get events as they happen, with no need to poll the API:
POST https://your-app.com/webhook
X-Signature: sha256=5bdcc146...
X-Event: sms:delivered
X-Delivery-Id: 12
{ "event": "sms:delivered", "delivery_id": "12", "created_at": "2026-11-04T10:00:07Z", "data": { "company": "my-company", "messageId": "1042" } }
| Event | Fields in data |
|---|---|
sms:sent, sms:delivered | company, messageId |
sms:failed | company, messageId, reason |
sms:received | company, deviceId, locationRef, message, phoneNumber, simTag, receivedAt, history (true for an old, imported message: it should not trigger anything) |
sms:outgoing | company, deviceId, locationRef, message, phoneNumber, sentAt |
call:logged | company, deviceId, locationRef, ref, phoneNumber, direction, duration, simTag, occurredAt |
device.offline, device.online | company, deviceId, name, locationRef |
Respond with a 2xx code within 15 seconds. Otherwise we retry, with growing pauses, up to 20 times. Events can arrive twice or out of order: keep the delivery_id and ignore the ones you have already seen. We do not follow redirects.
Verifying the signature
X-Signature is HMAC-SHA256 over the exact request body, using your secret. Verify it before trusting the event.
import { createHmac, timingSafeEqual } from 'node:crypto'
function valid(rawBody, header, secret) {
const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'))
const got = Buffer.from(header ?? '')
return got.length === expected.length && timingSafeEqual(got, expected)
}
<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
Errors and limits
An error looks like { "error": "message" }. The message text is in Romanian, so build your logic on the status code.
| Code | Meaning |
|---|---|
400 | Invalid data; the reason is in error. |
401 | Missing, wrong or revoked key. |
404 | Does not exist, or does not belong to your app. |
429 | Too many requests. Try again in a minute. |
5xx | Temporary error. Try again, with the same Idempotency-Key. |
Data and consent
Message texts and numbers are deleted after 30 days; the status stays for statistics. You are responsible for your recipients: only text people who have agreed to receive messages, and stop for those who ask. Also follow the terms of your SIM's carrier.