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

# Track Message Delivery

> How message delivery statuses work, webhook-based tracking, and how to list messages with cursor pagination.

Message delivery status comes through webhook events, not the REST API response. The create endpoint (`POST /accounts/{account_id}/messages`) returns the message object immediately — the status at that point is "created", and what happens next depends on the carrier.

## How delivery works

```mermaid theme={null}
stateDiagram-v2
  [*] --> created
  created --> sent
  sent --> delivered
  sent --> failed
```

| Status      | Meaning                                                                     |
| ----------- | --------------------------------------------------------------------------- |
| `created`   | Message created and accepted by the API                                     |
| `sent`      | Carrier accepted the message (`message.sent` webhook fires)                 |
| `delivered` | Carrier confirmed delivery to the handset (`message.delivered` fires)       |
| `failed`    | Delivery failed at any stage (`message.failed` fires with `failure_reason`) |

<Note>
  Status advances in one direction only. If Surge receives an out-of-order carrier status report (a rare but real occurrence on some networks), it is discarded rather than rolling the status back. A message in `delivered` state will never revert to `sent`.
</Note>

Not all carriers send delivery receipts. If you never receive `message.delivered` after `message.sent`, it doesn't mean the message wasn't delivered. The carrier simply didn't confirm it. This can be seen in messaging to the US and Canada, but it's especially prevalent outside of North America.

## Webhook-based tracking (recommended)

Configure a webhook endpoint (Settings → Webhooks in the dashboard) and handle the events you care about:

```python theme={null}
@app.post("/webhooks/surge")
async def surge_webhook(request: Request):
    payload = wh.verify(await request.body(), request.headers)

    match payload["type"]:
        case "message.delivered":
            msg = payload["data"]
            await db.messages.update(msg["id"], {"status": "delivered"})
        case "message.failed":
            msg = payload["data"]
            await db.messages.update(msg["id"], {
                "status": "failed",
                "failure_reason": msg.get("failure_reason")
            })

    return Response(status_code=200)
```

<Tip>
  Return `200` quickly. If your processing takes time, enqueue the work and return immediately — Surge retries if your handler times out.
</Tip>

See [Receiving Messages & Webhooks](../receiving/index) for endpoint setup and signature verification.

## Retrieving a message by ID

<Info>
  The retrieve-message response does **not** include a `status` field. It returns the message body, attachments, conversation thread, and metadata. For status, rely on the webhook events.
</Info>

You can retrieve a sent message to check its current fields:

<CodeGroup>
  ```python Python theme={null}
  from surge import Surge

  surge = Surge()
  message = surge.messages.retrieve("{message_id}")
  print(message.body)
  ```

  ```typescript TypeScript theme={null}
  import Surge from "@surgeapi/node";

  const surge = new Surge();
  const message = await surge.messages.retrieve("{message_id}");
  console.log(message.body);
  ```

  ```ruby Ruby theme={null}
  require "surge_api"

  surge = SurgeAPI::Client.new
  message = surge.messages.retrieve("{message_id}")
  puts message.body
  ```

  ```elixir Elixir theme={null}
  client = Surge.Client.new(System.get_env("SURGE_API_KEY"))
  {:ok, message} = Surge.Messages.get(client, "{message_id}")
  IO.puts(message.body)
  ```

  ```bash cURL theme={null}
  curl https://api.surge.app/messages/{message_id} \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</CodeGroup>

```json theme={null}
{
  "id": "msg_01jrzhe8d9enptypyx360pcmxj",
  "blast_id": null,
  "body": "Your appointment is confirmed for Friday at 2pm.",
  "attachments": [],
  "conversation": {
    "id": "cnv_01jrzhe8d9enptypyx360pcmxk"
  },
  "metadata": {}
}
```

Use this endpoint to fetch the message body or attachment URLs if you didn't store them at send time. For status, rely on the webhook events.

## Listing messages

To retrieve all messages for an account, use the list endpoint with cursor-based pagination:

<CodeGroup>
  ```python Python theme={null}
  # Auto-paginating iterator — call list() and iterate
  for message in surge.messages.list(account_id="{account_id}"):
      print(message.id, message.body)
  ```

  ```typescript TypeScript theme={null}
  // Auto-paginating async iterator
  for await (const message of await surge.messages.list("{account_id}")) {
    console.log(message.id, message.body);
  }
  ```

  ```ruby Ruby theme={null}
  # Cursor — page.data is the current page's array; auto_paging_each iterates everything
  page = surge.messages.list("{account_id}")
  page.data.each { |m| puts "#{m.id} #{m.body}" }
  ```

  ```elixir Elixir theme={null}
  # Surge.Messages.list isn't typed in the Elixir SDK yet — use the raw client.
  {:ok, page} =
    Surge.Client.request(client, :get, "/accounts/{account_id}/messages")

  Enum.each(page["data"], fn m -> IO.puts("#{m["id"]} #{m["body"]}") end)
  ```

  ```bash cURL theme={null}
  curl "https://api.surge.app/accounts/{account_id}/messages" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</CodeGroup>

```json theme={null}
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6Ii4uLiJ9",
    "previous_cursor": null
  }
}
```

Pass `after=CURSOR` or `before=CURSOR` as query parameters to page through results. The list returns messages in reverse chronological order (most recent first).
