Send a notification
Send from your backend and read the response and delivery results.
Prerequisites
- A Carillon app with a valid provider credential. See quickstart.
- A secret key for that app.
- A device registered by the SDK, with
identify()called so it carries your own user identifier asexternal_id. The examples useuser-42; replace it with one of yours.
Samples on this page show placeholders. Sign in to the dashboard in this browser and they fill in with your own organization, app and mobile key.
1. Send
Target the user identifiers your backend already holds with audience.external_ids. Every
device that carries one of them receives the notification; unknown identifiers are ignored.
Use a new Idempotency-Key for each intended notification. Reuse it with the same request body
when retrying after a timeout. The endpoint returns 202 Accepted.
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": { "external_ids": ["user-42"] }, "payload": { "title": "Your order has shipped", "body": "Track it from the app.", "data": { "order_id": "1234" }, "localizations": { "fr": { "title": "Votre commande est partie", "body": "Suivez-la depuis votre application." } } }}'{
"id": "01937b20-0000-7000-8000-000000000000",
"status": "sending",
"mode": "live",
"created_at": "2026-08-20T09:14:03.221Z",
"deliveries": [],
"warnings": []
}deliveries is empty here: the devices behind external_ids are resolved by the worker after the
request is accepted. Read the trace for the per-device results.
Read the warnings
warnings is empty on an ordinary send. An entry means part of this campaign cannot go out and was
written off before anything was attempted because a provider credential is missing or unusable. A campaign where every platform is in that state is refused with
422 instead of accepted.
2. Read the trace
curl https://api.carillon.dev/v1/messages/01937b20-0000-7000-8000-000000000000 \-H "Authorization: Bearer carillon_sk_live_your_secret_key"{
"id": "01937b20-0000-7000-8000-000000000000",
"status": "sending",
"schedule_type": "immediate",
"delivery_rate": 100,
"remaining": 0,
"paused_reason": null,
"outcomes": { "sent": 1, "failed": 0, "pending": 0, "total": 1 },
"deliveries": [
{
"id": "01937b21-0000-7000-8000-000000000000",
"device_id": "01937b1e-0000-7000-8000-000000000000",
"status": "sent",
"error_code": null,
"opened_at": null
}
]
}outcomes covers the whole audience. deliveries is a bounded sample of the first fifty, in
creation order, so a campaign of a million does not answer with a million rows.
opened_at stays null until a handset reports opening the notification. A notification nobody
opened and one opened by an app that never reports look the same here.
Send to device IDs
When you hold Carillon device IDs, from deviceId() in the SDK, the dashboard or an
export, name them directly. The 202 response then lists the delivery
created for each device, including the ones that will fail.
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-2c9b0f4d5a12" \-d '{ "audience": { "device_ids": ["01937b1e-0000-7000-8000-000000000000"] }, "payload": { "title": "Your order has shipped", "body": "Track it from the app." }}'{
"id": "01937b22-0000-7000-8000-000000000000",
"status": "sending",
"mode": "live",
"created_at": "2026-08-20T09:15:11.004Z",
"deliveries": [
{ "id": "01937b23-0000-7000-8000-000000000000", "device_id": "01937b1e-0000-7000-8000-000000000000" }
],
"warnings": []
}The four audience shapes
| Audience | Shape | Limit |
|---|---|---|
| Your user identifiers | { "external_ids": ["…"] } | 1 to 2000, each at most once; unknown identifiers are ignored |
| Named devices | { "device_ids": ["…"] } | 1 to 100, each at most once |
| Everyone | { "all": true } | The whole app |
| A saved audience | { "audience_id": "…" } | See audiences |
Only a device_ids send returns its deliveries in the 202 response. For the three other shapes
the list is empty: the audience is resolved by the worker, so the identifiers are minted then.
Register a token without an SDK
The SDKs do this for you. Doing it by hand is useful when you are testing the API itself, or when you already hold tokens from somewhere else.
curl -X POST https://api.carillon.dev/v1/devices \-H "Authorization: Bearer carillon_mk_live_your_mobile_key" \-H "Content-Type: application/json" \-d '{ "token": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90", "platform": "ios", "environment": "sandbox", "external_id": "user-42", "tags": { "plan": "pro" }, "timezone_id": "Europe/Paris", "locale": "fr-FR", "opted_in": true}'200, with the device as it now stands. Registration is an upsert on the token, so running this
again with a changed external_id updates the same row rather than creating a second one.
Replace the example token with a real APNs or FCM token. Keep the returned id for the send request.