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

  1. Retry on 429 and 5xx only.
  2. Do not retry validation failures (400, 422) before fixing payload.
  3. Use bounded exponential backoff with jitter.

Client idempotency pattern

  • Generate an operation key in your app per send intent.
  • Save operation_key -> message_id mapping.
  • If the same operation is retried, return existing message_id from 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

1

Send message request

Call POST /v1/messages and persist request metadata.
2

Track status

Query GET /v1/messages/{messageID} or consume webhook events.
3

Resolve final state

Treat read and failed as terminal states for most workflows.
Server-side webhook processing is designed for idempotent ingestion. Your consumer should still deduplicate events in your own storage.