# Track Message Delivery
URL: /docs/sending/track-delivery
LLM index: /llms.txt
Description: How message delivery statuses work, webhook-based tracking, and how to list messages with cursor pagination.

# Track Message Delivery

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 "sent" (queued), and what happens next depends on the carrier.

## How delivery works

```mermaid
stateDiagram-v2
  [*] --> pending
  pending --> queued
  queued --> sending
  sending --> sent
  sent --> delivered
  sent --> failed
```

| Status | Meaning |
|---|---|
| `pending` | Message created and accepted by the API |
| `queued` | Picked up by the send worker and queued for dispatch |
| `sending` | Currently being transmitted to the carrier |
| `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. Verizon, in particular, batches delivery confirmations, which can arrive several minutes late.

## Webhook-based tracking (recommended)

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

```python
@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 items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
from surge import Surge

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

```typescript TypeScript
import Surge from "@surgeapi/node";

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

```ruby Ruby
require "surge_api"

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

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

```bash cURL
curl https://api.surge.app/messages/{message_id} \
  -H "Authorization: Bearer YOUR_API_KEY"
```

</CodeGroup>

```json
{
  "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 items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

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

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

```ruby Ruby
# 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
# 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
curl "https://api.surge.app/accounts/{account_id}/messages" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

</CodeGroup>

```json
{
  "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).
