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

> ## Agent Instructions
> For chatbots and coding agents, start at /guides/ai-agents.
> Hosted MCP: POST https://api.robase.dev/mcp (Streamable HTTP). Auth: Authorization: Bearer robe_… or robe_agent_….
> Phase 1 tools: get_pricing, send_otp, verify_otp, get_otp, send_sms, get_sms. No payment or top-up tools.
> Call get_pricing; treat listed:true as marketed markets (NG, GH, KE, ZA, GB, BJ, CI, US). Do not invent countries or delivery percentages.
> Prefer OTP-shaped tests such as "Your Robase test OTP is 123456". Do not send Hello or Good morning as tests. Match errors on error.type.

# Delivery timeline

> How to read the timeline on GET /v1/sms/{id} and GET /v1/otp/{id}: each hop, why it ended, and whether credits came back.

`GET /v1/sms/{id}` and `GET /v1/otp/{id}` return `status` (the current state) and `timeline` (how it got there). When a user says "I never got the SMS", the timeline is what you paste into the ticket.

## Shape

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "credit_cost": 1,
  "timeline": [
    { "at": "2026-09-13T09:00:00Z", "status": "queued" },
    { "at": "2026-09-13T09:00:02Z", "status": "sent" },
    { "at": "2026-09-13T09:00:41Z", "status": "failed", "reason": "delivery_failed", "refunded": true }
  ]
}
```

Items are oldest first. Every item has `at` (RFC 3339) and `status`. The rest appear only when they apply.

| Hop         | Meaning                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------ |
| `queued`    | The message is stored, charged, and waiting for the worker. `status` reads `pending` here. |
| `sent`      | Robase sent it to the network.                                                             |
| `delivered` | The carrier sent a delivery receipt.                                                       |
| `failed`    | Robase could not send it, or the network reported it undelivered.                          |
| `blocked`   | The anti-spam screen stopped it before it was sent.                                        |
| `verified`  | OTP only: the end user entered the right code.                                             |
| `expired`   | OTP only: the TTL ran out. `at` is the TTL time.                                           |

`delivered` shows only when the carrier sends a receipt. Some routes never send one, so a message can stay at `sent` and still have reached the phone. The timeline never names an upstream; Robase is your processor. Robase does not publish a delivery-rate percentage, on this endpoint or anywhere else.

## Reasons

`reason` is a stable machine token. Branch on it; do not parse it.

| Token                   | Where              | Meaning                                                                            |
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------- |
| `send_failed`           | `failed`           | Robase could not send the message.                                                 |
| `delivery_failed`       | `failed`           | The network reported the message as failed, rejected, or expired.                  |
| `antispam_blocked`      | `blocked`          | The anti-spam screen refused the content. `metadata.antispam` holds the verdict.   |
| `antispam_flagged`      | `sent`             | The screen flagged the content but the workspace action is `flag`, so it was sent. |
| `max_attempts_exceeded` | `failed`, OTP only | The end user entered too many wrong codes.                                         |

When the reason is not known, `reason` is absent. New tokens may be added; existing ones are never renamed.

## Refunds

A terminal `failed` or `blocked` item carries `refunded`. It reflects the credit ledger, not a rule:

* `true`: the charge is back on your balance.
* `false`: it is not. Today that is one case: an OTP burned by wrong verify attempts, which was delivered and is not refunded.

`credit_cost` on the parent object stays the message's price, so a refunded message still shows what it would have cost.

## Older messages

A hop is listed only when its timestamp is stored. Messages sent before this timeline existed may end short of their `status`, for example a `blocked` message with only a `queued` hop. `status` is always authoritative.

## Webhooks

`sms.failed`, `sms.blocked`, and `otp.failed` carry the same `reason` and `refunded` fields in `data` when they are known. Event names are unchanged. See [SMS webhooks](/sms/webhooks).
