Relayion

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

curl "https://api.relayion.com/api/v1/devices" \
  -H "Authorization: Bearer rlyn_your_api_key"
{
  "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

FieldTypeRequiredNotes
recipientNumberstringYesE.164 format, e.g. +639171234567
bodystringYesMessage 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

curl -X POST https://api.relayion.com/api/v1/outbound \
  -H "Authorization: Bearer rlyn_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientNumber": "+639171234567",
    "body": "Your appointment is confirmed."
  }'

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

curl "https://api.relayion.com/api/v1/outbound?status=DELIVERED&from=2026-04-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer rlyn_your_api_key"

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

curl "https://api.relayion.com/api/v1/outbound/g1h2i3j4-5b6c-7d8e-9f0a-1b2c3d4e5f6a" \
  -H "Authorization: Bearer rlyn_your_api_key"

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

curl "https://api.relayion.com/api/v1/inbound?senderNumber=%2B639181234567" \
  -H "Authorization: Bearer rlyn_your_api_key"
{
  "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

curl "https://api.relayion.com/api/v1/inbound/h1i2j3k4-9a8b-7c6d-5e4f-3a2b1c0d9e8f" \
  -H "Authorization: Bearer rlyn_your_api_key"

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

# Fetch only inbound messages from April 1 onwards
curl "https://api.relayion.com/api/v1/conversations/%2B639171234567?direction=inbound&from=2026-04-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer rlyn_your_api_key"
{
  "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

EventWhen it fires
inbound.receivedAn inbound SMS was received by the device and stored
outbound.queuedAn outbound request was accepted while the device was offline
outbound.dispatchedThe send instruction was delivered to the device via WebSocket
outbound.sentThe device confirmed the SMS was submitted to the carrier
outbound.deliveredThe device received a delivery receipt from the carrier
outbound.failedThe device reported the SMS failed to send
Same-category constraint: All events on a single webhook must be either all 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.

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express receiver
app.post('/webhooks/sms', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-relayion-signature'];
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(400).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // process event.event, event.data
  res.status(200).send('OK');
});

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.

PlanBurst capacityRefill rateMonthly cap
Free5 messages1 / min500 messages
Starter50 messages20 / minNone
Pro100 messages50 / minNone
EnterpriseCustomCustomNone

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.

Questions? Email hello@relayion.com.
https://api.relayion.com