> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending SMS

> Every option on POST /v1/sms.

## Minimal send

<Snippet file="send-first-sms.mdx" />

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.robase.dev/v1/sms \
    -H "Authorization: Bearer rb_live_xxx" \
    -H "Content-Type: application/json" \
    -d '{"to":"+2348012345678","from":"SHUTTLERS","body":"Your ride arrives in 3 minutes."}'
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "id": "01H8XKQJ3Z9S7K1X2Y3Z4A5B",
    "status": "queued",
    "to": "+2348012345678",
    "from": "SHUTTLERS",
    "body": "Your ride arrives in 3 minutes.",
    "segments": 1,
    "encoding": "GSM7",
    "country": "NG",
    "cost": { "amount_kobo": 400, "currency": "NGN" },
    "provider": "beem",
    "test_mode": false,
    "created_at": "2026-04-17T10:30:00Z"
  }
  ```
</ResponseExample>

## Request parameters

<ParamField body="to" type="string" required>
  Recipient phone number in **E.164** format (`+` followed by 8–15 digits). We auto-detect the country from the dial code. Invalid numbers return `phone_invalid`.
</ParamField>

<ParamField body="from" type="string" required>
  Sender ID — alphanumeric, ≤ 11 characters. Must be registered in your project. See [Sender IDs](/sms/sender-ids).
</ParamField>

<ParamField body="body" type="string">
  Message body. Required unless `template` is provided. Max 1600 characters (concatenated SMS auto-splits into segments).
</ParamField>

<ParamField body="template" type="string">
  Slug or UUID of a registered SMS template. When set, `body` is ignored and rendered from the template using `variables`. See [Templates](/sms/templates).
</ParamField>

<ParamField body="variables" type="object">
  Map of variable → value for template interpolation. Only used when `template` is set.

  ```json theme={null}
  { "driver_name": "Chidi", "eta": 3 }
  ```
</ParamField>

<ParamField body="country" type="string">
  ISO-3166 alpha-2 code (`NG`, `GH`, `KE`, `CI`). Optional — we derive it from `to` by default. Use this to override for hybrid-numbered SIMs.
</ParamField>

<ParamField body="send_at" type="string (ISO 8601)">
  Schedule delivery for a future time. The message stays in `scheduled` state until the scheduled time, then transitions to `queued` and sends. Max lead time: 30 days.
</ParamField>

## Response

<ResponseField name="id" type="string">
  Globally unique message ID. Use this to [fetch](/api-reference/sms/get) the message later.
</ResponseField>

<ResponseField name="status" type="string">
  One of: `queued`, `scheduled`, `sent`, `delivered`, `failed`, `rejected`, `cancelled`. New messages start as `queued` (or `scheduled` if `send_at` is in the future).
</ResponseField>

<ResponseField name="segments" type="integer">
  Number of SMS segments the body occupies. GSM-7 body: 160/153 chars per segment. UCS-2 (emoji, non-Latin): 70/67.
</ResponseField>

<ResponseField name="encoding" type="string">
  `GSM7` or `UCS2`. We detect automatically based on body content.
</ResponseField>

<ResponseField name="cost" type="object">
  Per-segment cost committed for this send. `amount_kobo` is the smallest unit of `currency` (e.g. kobo for NGN, pesewas for GHS).
</ResponseField>

<ResponseField name="provider" type="string">
  Upstream we initially routed to. The actual winning provider after failover is on `GET /v1/sms/:id` in `provider_attempts`.
</ResponseField>

<ResponseField name="test_mode" type="boolean">
  `true` when sent with a `rb_test_*` key. Test messages never touch real carriers.
</ResponseField>

## With a template

```typescript theme={null}
await pm.sms.send({
  to:   '+2348012345678',
  from: 'SHUTTLERS',
  template: 'ride-arriving',
  variables: { driver_name: 'Chidi', eta: 3 },
});
```

## Scheduled send

```typescript theme={null}
await pm.sms.send({
  to:   '+2348012345678',
  from: 'SHUTTLERS',
  body: 'Campaign launches at noon!',
  send_at: new Date('2026-05-01T12:00:00+01:00').toISOString(),
});
```

Cancel a scheduled send before it fires via `POST /v1/sms/:id/cancel`. After `send_at` the message status becomes `queued` and cancellation becomes a no-op (returns `conflict`).

## With idempotency

Always pair sends with an idempotency key from your business data — see [Idempotency](/concepts/idempotency):

```typescript theme={null}
await pm.sms.send(
  { to, from, body },
  `order-${orderId}-dispatch-sms`
);
```

## Error cases

| Error                 | HTTP | Cause                                              |
| --------------------- | ---- | -------------------------------------------------- |
| `phone_invalid`       | 400  | `to` not in E.164 format                           |
| `sender_id_invalid`   | 400  | Sender too long, or rejected by carrier compliance |
| `validation_error`    | 400  | Required field missing, body too long              |
| `sms_quota_exceeded`  | 402  | Monthly quota + wallet both exhausted              |
| `rate_limit_exceeded` | 429  | Slow down and retry with backoff                   |

Full catalog on [Errors](/concepts/errors).
