Message lifecycle and idempotency
Delivery status lifecycle, retry boundaries, and idempotency patterns for reliable messaging.
Use this guide to build retry-safe integrations.
Message lifecycle
Typical state flow:
queued -> sent -> delivered -> read
\-> failed
queued: accepted and waiting for dispatch.sent: handed off to provider.delivered: delivered to recipient device/provider state.read: read receipt observed.failed: dispatch failed or provider rejected.
Retry boundaries
- Retry on
429and5xxonly. - Do not retry validation failures (
400,422) before fixing payload. - Use bounded exponential backoff with jitter.
Client idempotency pattern
- Generate an operation key in your app per send intent.
- Save
operation_key -> message_idmapping. - If the same operation is retried, return existing
message_idfrom your store.
Webhook deduplication pattern
Use the delivery id from webhook headers (svix-id or webhook-id) to avoid duplicate processing:
if delivery_id already processed:
return 200
store delivery_id
enqueue processing
return 200
For message.new / message.status_update, also key business logic on message_id inside the JSON body.
Reconciliation flow
Send message request
Call POST /v1/messages and persist request metadata.
Track status
Query GET /v1/messages/{messageID} or consume webhook events.
Resolve final state
Treat read and failed as terminal states for most workflows.