Surge

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

stateDiagram-v2
  [*] --> pending
  pending --> queued
  queued --> sending
  sending --> sent
  sent --> delivered
  sent --> failed
StatusMeaning
pendingMessage created and accepted by the API
queuedPicked up by the send worker and queued for dispatch
sendingCurrently being transmitted to the carrier
sentCarrier accepted the message (message.sent webhook fires)
deliveredCarrier confirmed delivery to the handset (message.delivered fires)
failedDelivery 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.

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.

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

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

See Receiving Messages & Webhooks 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.

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

from surge import Surge

surge = Surge()
message = surge.messages.retrieve("{message_id}")
print(message.body)
{
  "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:

# Auto-paginating iterator — call list() and iterate
for message in surge.messages.list(account_id="{account_id}"):
    print(message.id, message.body)
{
  "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).