Delivery and retries
Delivery statuses, safe request retries, deduplication, and cancellation.
POST /v1/messages returns 202 when the campaign is accepted for asynchronous processing.
Use GET /v1/messages/{id} to read its progress and delivery results.
The trace
A delivery records the result for one device. Campaign totals are in outcomes; deliveries
contains the first 50 delivery records.
| Status | Meaning |
|---|---|
queued | Waiting for an attempt |
in_flight | An attempt has started; the provider outcome has not been recorded |
sent | APNs or FCM accepted the notification |
failed | Delivery failed or was skipped; read error_code |
sent means accepted by Apple or Google. There is no delivered-to-handset receipt: neither provider
reports that a notification reached the device, so sent does not confirm that the operating
system displayed it or that a person read it. opened_at is set only when the app reports a tap.
See track opens.
Provider errors retain their codes, such as BadDeviceToken or SENDER_ID_MISMATCH.
Carillon errors describe skipped or unconfirmed deliveries, such as NoCredential, Canceled,
or OutcomeUnknown. See troubleshooting.
At most once, never twice
Carillon does not retry a delivery when an interrupted send leaves its outcome unknown. The notification may have reached the provider. Retrying could send it twice.
We trade a possible duplicate for a possible loss.
Such deliveries remain pending until maintenance marks them failed with OutcomeUnknown.
That code means the outcome could not be confirmed; it does not prove that the notification was lost.
Do not automatically create a replacement send for it.
This behavior does not guarantee exactly-once display on a device.
Idempotency-Key and dedup_key
Both are optional and apply to POST /v1/messages.
Idempotency-Key header | dedup_key body field | |
|---|---|---|
| Purpose | Retry the same HTTP request safely | Prevent separate requests from sending the same business notification |
| Same key, identical body | Original response, byte for byte | 409 duplicate_dedup_key while the key is retained |
| Same key, different body | 422 idempotency_key_reused | 409 duplicate_dedup_key while the key is retained |
| Retention | 24 hours | 30 days |
Create one Idempotency-Key per intended send. On a timeout, retry with that key and the
same request bytes, including JSON formatting. If the first request is still processing,
the API returns 409 idempotency_key_in_flight; wait for Retry-After before retrying.
Use a new key for a new send.
Use a stable business identifier for dedup_key, such as order-1234-shipped.
A duplicate response includes campaign_id so you can inspect the original campaign.
Deduplication is limited to its retention period; keep longer-lived business state in your application.
curl -X POST https://api.carillon.dev/v1/messages \-H "Authorization: Bearer carillon_sk_live_your_secret_key" \-H "Content-Type: application/json" \-H "Idempotency-Key: 018f2c8a-6f3f-7a2b-9f1e-2c9b0f4d5a11" \-d '{ "audience": { "device_ids": ["01937b1e-0000-7000-8000-000000000000"] }, "dedup_key": "order-1234-shipped", "payload": { "title": "Your order has shipped", "body": "Track it from the app." }}'Idempotency is checked before deduplication. Retrying this exact request with the same
Idempotency-Key returns its original response. A separate request using the same dedup_key
is rejected while the deduplication record is retained.
Pacing
delivery_rate is the maximum notifications per second for the campaign.
If omitted, the app's configured ceiling applies. A value above that ceiling returns
422 delivery_rate_too_high.
Automatic pause
If most of a chunk fails on a configuration error (at least 25 deliveries and half of the chunk),
Carillon pauses the campaign and keeps its remaining deliveries queued. Configuration errors are
the ones that fail every device the same way: a missing or rejected credential, a revoked APNs key,
a bundle ID the key cannot send to, or an FCM sender mismatch. Dead tokens do not pause a campaign.
The campaign's status becomes paused and paused_reason holds the error code. The
organization's owners and admins receive an email, at most one per app and error code every six
hours. Correct the credential, then resume the campaign from the dashboard; see
Resume a paused campaign.
Cancelling
Call POST /v1/messages/{id}/cancel to cancel pending work.
{ "canceled": true, "already_sent": 412, "canceled_count": 8871 }already_sentincludes deliveries markedsentorin_flight. These cannot be recalled.canceled_countcounts delivery records markedfailedwitherror_code: "Canceled".- A scheduled campaign cancelled before recipient resolution returns zero for both counts.
Related pages
Schedule a message · Track opens · API errors
Trace retention and platform counts
Campaign traces remain readable for at least 30 days after sending. The response includes
outcomes.opened and the same sent/failed/pending/opened/total counters in platforms.ios
and platforms.android. A platform without deliveries has zero counts. platforms reports
ios and android only: deliveries written before platform tracking existed are counted in
outcomes but in neither platform bucket, so the two platform totals can sum to less than
outcomes.total on an old campaign. New deliveries retain their platform even if the device is
later deleted.
An open is a reported tap, not a receipt confirming display. See tracking opens.
GET /v1/messages/{id}/opens returns opens per hour for the first 48 hours after the campaign was
created; outcomes.opened keeps counting after that window.
Notifications use a 72-hour TTL by default, starting at their first send attempt. Deferred
retries keep that deadline; see notification lifetime.