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.

AppYour API account. You get a key and a webhook secret from us.
CompanyOne of your accounts, with its phones. You identify it with your own id. If you only have one company, you create just one.
PhoneA 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.

GET /v1/companies

All your companies.

PUT /v1/companies/{company}

Creates the company or updates its details. Safe to call any number of times.

FieldTypeDescription
nametext, requiredThe company name, up to 200 characters.
fallbackbooleanIf the location's phone goes silent, the message can leave from another phone after 10 minutes. Default false.
GET /v1/companies/{company}

Phones

GET /v1/companies/{company}/devices

The 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 }
}] }
POST /v1/companies/{company}/devices
FieldTypeDescription
nametext, requiredUp to 80 characters.
location_reftextThe phone's location, using your own identifier.
is_defaultbooleanA company's first phone is the default.
PATCH /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.

SettingValuesDescription
sim1 or 2The SIM used for sending.
start, endHH:MMThe sending window, Romanian time.
per_minute1 - 30Texts per minute.
retries0 - 5Retries for a failed message.
inboundbooleanReports received texts.
callsbooleanReports the call log.
outgoingbooleanReports texts written by hand on the phone. Off by default.
historybooleanImports the phone's old texts once, received and sent. Off by default: without it, the log starts when the phone is connected.
POST /v1/companies/{company}/devices/{id}/pairing

A new connection code: { "code", "qr", "expires_at" }. Cancels the phone's older codes.

DELETE /v1/companies/{company}/devices/{id}

Removes the phone. Its history stays.

Messages

POST /v1/companies/{company}/messages

Queues a text. Responds 202 with { "id", "status": "queued" }.

FieldTypeDescription
totext, requiredThe number in international format, for example +40712345678.
texttext, requiredUp to 1600 characters. A text holds 160 characters, or 70 with special characters; a longer one goes out in several parts.
location_reftextSends from this location's phone.
expires_atISO 8601 dateIf 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

queuedWaiting for a phone, or for its sending window.
claimedPicked up by the phone.
sentLeft the phone.
deliveredThe carrier confirmed delivery.
failedCould not be sent or has expired; the reason is in error.
GET /v1/messages/{id}
{ "id": "1042", "status": "delivered", "error": null, "sent_at": "2026-11-04T10:00:03Z", "delivered_at": "2026-11-04T10:00:07Z" }
GET /v1/companies/{company}/messages

The 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.

GET /v1/companies/{company}/inbound
{ "inbound": [{ "id": "88", "device_id": "...", "from": "+40712345678", "text": "YES", "sim": "SIM 1", "received_at": "2026-11-04T10:05:00Z" }] }
GET /v1/companies/{company}/outgoing

Texts 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" }] }
GET /v1/companies/{company}/calls

direction 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" } }
EventFields in data
sms:sent, sms:deliveredcompany, messageId
sms:failedcompany, messageId, reason
sms:receivedcompany, deviceId, locationRef, message, phoneNumber, simTag, receivedAt, history (true for an old, imported message: it should not trigger anything)
sms:outgoingcompany, deviceId, locationRef, message, phoneNumber, sentAt
call:loggedcompany, deviceId, locationRef, ref, phoneNumber, direction, duration, simTag, occurredAt
device.offline, device.onlinecompany, 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.

CodeMeaning
400Invalid data; the reason is in error.
401Missing, wrong or revoked key.
404Does not exist, or does not belong to your app.
429Too many requests. Try again in a minute.
5xxTemporary error. Try again, with the same Idempotency-Key.
The API limit is 600 requests per minute. The real speed of your texts depends on the phone: 6 per minute by default, up to 30, and the carrier may also limit a regular SIM. For high volumes, add more phones.

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.