API Reference
The Relayion API lets you send and receive SMS through Android devices registered to your account. All requests go to:
https://api.relayion.com
All endpoints are versioned under /api/v1/. All responses use the JSON envelope:
// Success
{ "success": true, "data": { ... } }
// Error
{ "success": false, "error": { "message": "...", "code": "MACHINE_CODE" } }Authentication
All API endpoints authenticate with an API key. Pass it as a Bearer token in the Authorization header on every request:
Authorization: Bearer rlyn_your_api_key
Getting an API key
Log in to app.relayion.com, go to Settings → API Keys, and click Create key. The full key is shown exactly once. Copy it immediately and store it securely (e.g. in an environment variable). Only the prefix rlyn_xxxxxxxx is retained in the Relayion Console for identification.
Key binding
Each API key is permanently bound to a specific device, SIM slot, and phone number at creation time. Every request made with that key sends from that exact line. To send from a different device or SIM, create a separate key bound to it. Bindings cannot be changed after the key is created.
For inbound message access, the key automatically scopes queries to the bound phone number only. If the SIM moves to a different device or slot, inbound history remains accessible without creating a new key.
Device discovery
Retrieve the devices registered to your account with their active SIM slots embedded. Use this to confirm device status, inspect SIM slot details, and select the right device when creating an API key in the Relayion Console.
GET /api/v1/devices
Returns all active devices for the account. Soft-deleted devices are excluded. Read-only.
EXAMPLES
{
"success": true,
"data": [
{
"id": "b1c2d3e4-...",
"name": "Office Phone 1",
"status": "ONLINE",
"isPaired": true,
"pendingSetup": false,
"lastSeenAt": "2026-04-25T08:00:00.000Z",
"sims": [
{
"id": "c1d2e3f4-...",
"slotIndex": 0,
"phoneNumber": "+639171234567",
"label": "Globe",
"isDefault": true
}
]
}
]
}Only devices where isPaired is true can receive outbound dispatch. Manage devices and label SIM slots from the Relayion Console at app.relayion.com.
Sending SMS
Submit outbound SMS requests via API key auth. The message is pushed to the device over WebSocket. If the device is offline, it is stored as queued and dispatched on reconnect.
POST /api/v1/outbound
| Field | Type | Required | Notes |
|---|---|---|---|
recipientNumber | string | Yes | E.164 format, e.g. +639171234567 |
body | string | Yes | Message text |
Routing
Device and SIM routing are resolved from the API key's permanent binding. There is nothing to specify per request. If the bound device is offline, the message is stored as QUEUED and dispatched automatically on reconnect.
EXAMPLES
Response (202)
{
"success": true,
"data": {
"id": "g1h2i3j4-5b6c-7d8e-9f0a-1b2c3d4e5f6a",
"accountId": "a1b2c3d4-...",
"deviceId": "b1c2d3e4-...",
"apiKeyId": "e1f2a3b4-...",
"simSlotIndex": 0,
"recipientNumber": "+639171234567",
"body": "Your appointment is confirmed.",
"status": "DISPATCHED",
"createdAt": "2026-04-25T08:00:00.000Z",
"dispatchedAt": "2026-04-25T08:00:00.001Z",
"sentAt": null,
"deliveredAt": null,
"failedAt": null,
"failureReason": null
}
}status is DISPATCHED if the device was online, QUEUED if offline. Status flow: QUEUED → DISPATCHED → SENT → DELIVERED (or FAILED at any stage after dispatch).
GET /api/v1/outbound
List outbound messages, newest first. Optional filters: deviceId, recipientNumber, simSlotIndex, status, from (ISO date), to (ISO date). Returns an array in the same shape as the POST response.
EXAMPLES
GET /api/v1/outbound/:id
Get a single message by ID with its current delivery status. Returns 404 NOT_FOUND if the message does not exist or belongs to a different account.
EXAMPLES
Receiving SMS
Inbound SMS received by a paired device are stored in real time and available for retrieval via the API. For real-time push delivery to your server, see the Webhooks section.
GET /api/v1/inbound
List inbound messages for the bound phone number, newest first. Results are automatically pre-scoped to the bound phone number. Only messages received on that number are returned, regardless of which device or SIM slot received them. Additional optional filters include deviceId, senderNumber, simSlotIndex, from (ISO date), to (ISO date).
EXAMPLES
{
"success": true,
"data": [
{
"id": "h1i2j3k4-...",
"accountId": "a1b2c3d4-...",
"deviceId": "b1c2d3e4-...",
"simSlotIndex": 0,
"senderNumber": "+639181234567",
"body": "Yes, please confirm my 3pm appointment.",
"status": "RECEIVED",
"receivedAt": "2026-04-25T08:05:00.000Z",
"createdAt": "2026-04-25T08:05:00.000Z"
}
]
}GET /api/v1/inbound/:id
Get a single inbound message by ID. Returns the same shape as a single item above. Returns 404 NOT_FOUND if not found.
EXAMPLES
Conversations
A unified view of all messages (inbound and outbound) exchanged with a specific phone number, sorted oldest first. Use this for conversation threading.
GET /api/v1/conversations/:phoneNumber
The phone number must be URL-encoded (e.g. +63 → %2B63). When called with an API key, results are automatically pre-scoped to the bound phone number. Only inbound messages received on that number and outbound messages sent from that number are returned. All filters are optional and combine as AND. Additional optional filters include deviceId, simSlotIndex, from (ISO date), to (ISO date), and direction (inbound | outbound). Pagination uses page and limit (default 20, max 100). Returns { total: 0, messages: [] } if the phone number has no messages.
EXAMPLES
{
"success": true,
"data": {
"phoneNumber": "+639171234567",
"total": 2,
"messages": [
{
"id": "g1h2i3j4-...",
"direction": "outbound",
"deviceId": "b1c2d3e4-...",
"simSlotIndex": 0,
"body": "Your appointment is confirmed.",
"status": "DELIVERED",
"sentAt": "2026-04-25T08:00:00.000Z",
"deliveredAt": "2026-04-25T08:00:03.000Z",
"createdAt": "2026-04-25T08:00:00.000Z",
"timestamp": "2026-04-25T08:00:00.000Z"
},
{
"id": "h1i2j3k4-...",
"direction": "inbound",
"deviceId": "b1c2d3e4-...",
"simSlotIndex": 0,
"body": "Yes, please confirm my 3pm appointment.",
"status": "RECEIVED",
"receivedAt": "2026-04-25T08:05:00.000Z",
"createdAt": "2026-04-25T08:05:00.000Z",
"timestamp": "2026-04-25T08:05:00.000Z"
}
]
}
}Webhooks
Relayion posts to your registered endpoint in real time when subscribed events occur. Create and manage webhooks from the Relayion Console at app.relayion.com. This section covers what your receiver needs to handle.
Event types
| Event | When it fires |
|---|---|
inbound.received | An inbound SMS was received by the device and stored |
outbound.queued | An outbound request was accepted while the device was offline |
outbound.dispatched | The send instruction was delivered to the device via WebSocket |
outbound.sent | The device confirmed the SMS was submitted to the carrier |
outbound.delivered | The device received a delivery receipt from the carrier |
outbound.failed | The device reported the SMS failed to send |
inbound.* or all outbound.*. Mixing categories is rejected with 422 MIXED_EVENT_CATEGORIES.Signature verification
Every webhook request includes an X-Relayion-Signature header: the HMAC-SHA256 of the raw request body using your webhook secret, encoded as a hex string. Always verify this before processing.
Important: Always verify against the raw body bytes before JSON parsing. Re-serializing parsed JSON can change whitespace or key order and will produce a different hash.
Payload shapes
The inbound.received payload has two forms depending on the includeBody setting on your webhook (configured in the Relayion Console). When includeBody is disabled (the default), the message body is omitted from the event. Use GET /api/v1/inbound/:id with the id from the payload to retrieve the full message on demand. Enable includeBody only if your endpoint and log infrastructure are appropriate for storing message content. The body is transmitted on every delivery attempt and retry.
// inbound.received, includeBody off (default)
{
"event": "inbound.received",
"timestamp": "2026-04-07T10:00:00Z",
"data": {
"id": "<inboundMessageId>",
"deviceId": "<deviceId>",
"senderNumber": "+639171234567",
"simSlotIndex": 0,
"simPhoneNumber": "+639171234567",
"receivedAt": "2026-04-07T10:00:00Z"
}
}
// inbound.received, includeBody on
{
"event": "inbound.received",
"timestamp": "2026-04-07T10:00:00Z",
"data": {
"id": "<inboundMessageId>",
"deviceId": "<deviceId>",
"senderNumber": "+639171234567",
"body": "Yes, confirmed.",
"simSlotIndex": 0,
"simPhoneNumber": "+639171234567",
"receivedAt": "2026-04-07T10:00:00Z"
}
}
// outbound.queued
{
"event": "outbound.queued",
"timestamp": "2026-04-07T10:00:00Z",
"data": {
"id": "<messageId>",
"deviceId": "<deviceId>",
"recipientNumber": "+639171234567",
"body": "Your appointment is confirmed.",
"status": "queued",
"createdAt": "2026-04-07T10:00:00Z"
}
}
// outbound.dispatched
{
"event": "outbound.dispatched",
"timestamp": "2026-04-07T10:00:01Z",
"data": {
"id": "<messageId>",
"recipientNumber": "+639171234567",
"status": "dispatched",
"dispatchedAt": "2026-04-07T10:00:01Z"
}
}
// outbound.sent
{
"event": "outbound.sent",
"timestamp": "2026-04-07T10:00:02Z",
"data": {
"id": "<messageId>",
"recipientNumber": "+639171234567",
"status": "sent",
"sentAt": "2026-04-07T10:00:02Z"
}
}
// outbound.delivered
{
"event": "outbound.delivered",
"timestamp": "2026-04-07T10:01:00Z",
"data": {
"id": "<messageId>",
"recipientNumber": "+639171234567",
"status": "delivered",
"deliveredAt": "2026-04-07T10:01:00Z"
}
}
// outbound.failed
{
"event": "outbound.failed",
"timestamp": "2026-04-07T10:00:05Z",
"data": {
"id": "<messageId>",
"recipientNumber": "+639171234567",
"status": "failed",
"failedAt": "2026-04-07T10:00:05Z",
"failureReason": "No signal"
}
}Rate limits
Outbound SMS delivery is controlled by a per-device message bucket. Each device has a burst capacity (messages it can dispatch in a short window) and a refill rate (how many messages are added back per minute for sustained use). When the bucket is depleted, requests queue automatically. No error is returned and no message is dropped. Messages are delivered as soon as the bucket refills.
| Plan | Burst capacity | Refill rate | Monthly cap |
|---|---|---|---|
| Free | 5 messages | 1 / min | 500 messages |
| Starter | 50 messages | 20 / min | None |
| Pro | 100 messages | 50 / min | None |
| Enterprise | Custom | Custom | None |
The Free plan enforces a hard monthly cap of 500 outbound messages per calendar month. When that cap is reached the API returns 429 Too Many Requests:
{
"success": false,
"error": {
"message": "Monthly message limit reached.",
"code": "MONTHLY_LIMIT_EXCEEDED"
}
}Paid plans have no monthly message cap. Enterprise plans have custom throughput limits. Contact sales@relayion.com to discuss your requirements.
