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| 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) |
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.
Webhook-based tracking (recommended)
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)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
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)import Surge from "@surgeapi/node";
const surge = new Surge();
const message = await surge.messages.retrieve("{message_id}");
console.log(message.body);require "surge_api"
surge = SurgeAPI::Client.new
message = surge.messages.retrieve("{message_id}")
puts message.bodyclient = Surge.Client.new(System.get_env("SURGE_API_KEY"))
{:ok, message} = Surge.Messages.get(client, "{message_id}")
IO.puts(message.body)curl https://api.surge.app/messages/{message_id} \
-H "Authorization: Bearer YOUR_API_KEY"{
"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)// Auto-paginating async iterator
for await (const message of await surge.messages.list("{account_id}")) {
console.log(message.id, message.body);
}# 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}" }# 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)curl "https://api.surge.app/accounts/{account_id}/messages" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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).