Carillon docs

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.

StatusMeaning
queuedWaiting for an attempt
in_flightAn attempt has started; the provider outcome has not been recorded
sentAPNs or FCM accepted the notification
failedDelivery 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 headerdedup_key body field
PurposeRetry the same HTTP request safelyPrevent separate requests from sending the same business notification
Same key, identical bodyOriginal response, byte for byte409 duplicate_dedup_key while the key is retained
Same key, different body422 idempotency_key_reused409 duplicate_dedup_key while the key is retained
Retention24 hours30 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_sent includes deliveries marked sent or in_flight. These cannot be recalled.
  • canceled_count counts delivery records marked failed with error_code: "Canceled".
  • A scheduled campaign cancelled before recipient resolution returns zero for both counts.

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.

On this page