Every outbound RCS message moves through a series of statuses. Arowana emits a webhook event at each transition so you can react in real time — no polling required.
Message lifecycle
queued → dispatched → delivered → read
↘ failed
Status Meaning queuedAccepted by Arowana and enqueued for delivery dispatchedForwarded to the carrier / Google network deliveredConfirmed on the recipient’s device readUser opened the message thread failedDelivery failed (see failure_reason) expiredMessage TTL elapsed before delivery
Track with webhooks (recommended)
Register a webhook endpoint in your dashboard and Arowana will POST events to your URL. No polling, no open connections.
POST https://your-server.example.com/webhooks/arowana
Content-Type: application/json
X-Arowana-Signature: sha256=...
Each event shares the same envelope:
{
"id" : "evt_a1b2c3" ,
"event" : "message.delivered" ,
"timestamp" : "2026-03-28T09:01:00Z" ,
"data" : {
"message_id" : "msg_a1b2c3d4" ,
"agent_id" : "ag_live_xxxx" ,
"to" : "+4917612345678" ,
"status" : "delivered" ,
"delivered_at" : "2026-03-28T09:01:00Z"
}
}
Delivery events
message.delivered
message.read
message.failed
message.expired
Fired when the carrier confirms the message arrived on the device. {
"id" : "evt_a1b2c3" ,
"event" : "message.delivered" ,
"timestamp" : "2026-03-28T09:01:00Z" ,
"data" : {
"message_id" : "msg_a1b2c3d4" ,
"agent_id" : "ag_live_xxxx" ,
"to" : "+4917612345678" ,
"status" : "delivered" ,
"delivered_at" : "2026-03-28T09:01:00Z"
}
}
Fired when the user opens the message thread. Only available on devices that send read receipts. {
"id" : "evt_b2c3d4" ,
"event" : "message.read" ,
"timestamp" : "2026-03-28T09:03:45Z" ,
"data" : {
"message_id" : "msg_a1b2c3d4" ,
"agent_id" : "ag_live_xxxx" ,
"to" : "+4917612345678" ,
"status" : "read" ,
"read_at" : "2026-03-28T09:03:45Z"
}
}
Fired when delivery is not possible. Check failure_reason for the cause. {
"id" : "evt_c3d4e5" ,
"event" : "message.failed" ,
"timestamp" : "2026-03-28T09:01:30Z" ,
"data" : {
"message_id" : "msg_a1b2c3d4" ,
"agent_id" : "ag_live_xxxx" ,
"to" : "+4917612345678" ,
"status" : "failed" ,
"failed_at" : "2026-03-28T09:01:30Z" ,
"failure_reason" : "UNKNOWN_RCS_USER"
}
}
Common failure_reason values: Value What happened UNKNOWN_RCS_USERPhone number is not RCS-capable or RCS is disabled CARRIER_NOT_LAUNCHEDYour agent is not yet live on the recipient’s carrier MESSAGE_EXPIREDTTL elapsed before delivery REJECTED_SPAMCarrier or Google flagged the message ACCOUNT_SUSPENDEDAgent or platform account is suspended
Fired when the message TTL elapses before delivery. Identical shape to message.failed with status: "expired". {
"id" : "evt_d4e5f6" ,
"event" : "message.expired" ,
"timestamp" : "2026-03-28T10:00:00Z" ,
"data" : {
"message_id" : "msg_a1b2c3d4" ,
"agent_id" : "ag_live_xxxx" ,
"to" : "+4917612345678" ,
"status" : "expired" ,
"expired_at" : "2026-03-28T10:00:00Z"
}
}
Verifying webhook signatures
Every request includes an X-Arowana-Signature header. Always verify it before processing. See Webhooks — Signature verification for the HMAC-SHA256 implementation and full event catalogue.
Poll for status (alternative)
If you cannot accept webhooks, poll GET /v1/messages/{id} until a terminal status is reached.
curl "https://api.arowana.app/v1/messages/msg_a1b2c3d4" \
-H "Authorization: Bearer $API_KEY "
{
"message_id" : "msg_a1b2c3d4" ,
"status" : "delivered" ,
"created_at" : "2026-03-28T09:00:00Z" ,
"delivered_at" : "2026-03-28T09:01:00Z"
}
Polling is rate-limited. Use webhooks for any volume above a handful of messages per minute.
Terminal statuses (stop polling): delivered, read, failed, expired.
Billing finalisation
Arowana places a balance hold at send time. The actual charge is settled when the message reaches a terminal status:
delivered / read → hold converts to a charge
failed / expired → hold is released and balance restored
Webhooks Configure endpoints, view all events, and signature spec.
Receive replies Handle inbound user replies and open conversation sessions.