Notification payloads
Payload fields, custom data, localization, and platform overrides.
A notification is a payload and an audience, posted to POST /v1/messages. The payload is the
part your recipients see.
Anatomy
{
"audience": { "device_ids": ["01937b1e-0000-7000-8000-000000000000"] },
"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 l'application." }
}
}
}| Field | Shape | Notes |
|---|---|---|
title | 1 to 256 characters | Optional on its own |
body | 1 to 2048 characters | Optional on its own |
ttl | Integer from 0 to 2419200 seconds | Default 259200 (72 hours), starting at the first send attempt |
priority | high or normal | Default high; APNs 10/5 and FCM HIGH/NORMAL |
collapse_id | 1 to 64 UTF-8 bytes | APNs collapse header and FCM collapse key |
image | HTTPS URL, up to 2048 characters | Localizable; iOS requires a notification service extension |
subtitle | 1 to 256 characters | iOS alert subtitle; localizable |
thread_id | 1 to 64 characters | iOS notification grouping; included in the SDK stamp for Android foreground grouping |
sound | 1 to 64 characters | Native sound name; default when omitted. Android channels may override it |
badge | Nonnegative integer | Absolute badge count, not an increment |
android | Raw Android configuration object | Recursively merged over generated FCM Android options |
data | Flat map of strings, numbers, booleans and nulls | String values up to 1024 characters; no nested objects or arrays |
localizations | Map keyed by BCP 47 tag | Each entry overrides only the fields it carries |
apns | Raw APNs object | Recursively merged over generated fields |
At least one of title, body or a raw apns override must be present. A payload with none of the
three is refused with 422 empty_payload.
Custom data, and the reserved key
data travels alongside the alert and is read by your app when the notification is opened. Keep it
small: send a resource id your app can fetch the rest from, rather than the content itself.
Values are scalars only. A nested object or an array is refused by validation with 422, for
both platforms, before anything is attempted. To carry structured data, JSON-encode it into a
string on your side and decode it in the app.
`carillon` is reserved
Every payload carries a carillon key holding the delivery identifier that
open tracking needs. A payload that sets that key itself is refused
with 422 reserved_data_key rather than silently overwritten.
Localizations
A device reports its locale when it registers. At send time each device is matched against
localizations in three steps:
Its exact tag, so fr-CA finds a fr-CA entry.
Its language alone, so fr-CA falls back to a fr entry.
The base text, title and body, when neither matched.
An entry overrides only what it carries. A localizations.fr with a title and no body keeps the
base body.
Per-platform behaviour
iOS. title and body become the APNs alert. data becomes top-level keys of the payload
beside aps. Apple caps a notification at 4096 bytes, and a payload over that is refused before
anything is attempted.
Android. title and body become an FCM notification. data becomes the FCM data map, whose
values are strings on the wire: numbers and booleans are converted. Decode them in the app.
Both. Display is the operating system's decision and then your app's. Carillon does not set the operating system’s channel importance. Sound and badge fields are requests; OS settings and Android notification channels can override them. Foreground presentation requires the native SDK callbacks.
Sound. Every notification carries sound: "default" on both platforms unless the payload sets
sound. Name a sound bundled with the app to play it instead; on Android 8 and later the channel's
sound wins over either.
Lifetime and retries
ttl starts at the first send attempt, after scheduling and pacing. Its absolute
expiry is retained across retries. An expired delivery fails with
NotificationExpired without contacting the provider. ttl: 0 requests immediate
delivery without provider storage. An Android ttl override uses FCM's duration
format (for example "3600s") and also keeps a fixed deadline across retries.
NotificationExpired is not Abandoned. The first means the ttl you set ran out before an
attempt could be made. The second means a delivery stayed queued for 24 hours without ever being
handed to a provider, and maintenance wrote it off; it can be sent again. See
troubleshooting.
collapse_id lets a provider replace older pending notifications with the same
identifier. It is not business deduplication: use dedup_key when you need that.
Images and presentation
image adds aps.mutable-content = 1 and carillon.image on iOS, and
android.notification.image on Android. Apple requires a notification service
extension to download and attach the image. Without that extension, the text can
arrive without the image. Android background images are handled by FCM.
Formats and size limits differ by platform:
| Platform | Formats | Limit |
|---|---|---|
| iOS attachment | jpg, png, gif | The extension downloads up to 10 MiB. webp cannot be attached: the notification falls back to text |
| Android, rendered by the system | What FCM accepts, including jpg, png, webp | FCM downloads up to 1 MiB; a larger image is dropped and the text is shown |
| Android, app in the foreground | What the platform decodes | The SDK downloads up to 10 MiB |
Serve one image under 1 MiB in jpg or png to satisfy every path with a single URL.
{
"title": "Order shipped",
"image": "https://example.com/order.jpg",
"subtitle": "Delivery tomorrow",
"thread_id": "orders",
"badge": 1,
"sound": "default",
"localizations": { "fr": { "subtitle": "Livraison demain" } },
"android": { "notification": { "channel_id": "orders" } }
}Raw android options win conflicts within the generated Android configuration.
Carillon restores its reserved tracking stamp afterward, including in Android-specific
data. thread_id does not create an Android system notification group: FCM has
no corresponding notification field.
The API checks every localized APNs and FCM payload against 4096 UTF-8 bytes,
including the tracking stamp and raw overrides. Oversized payloads return
422 payload_too_large. Invalid image URLs return 422 invalid_image_url.
The raw APNs override
payload.apns is an escape hatch for anything Apple documents and Carillon does not model yet:
interruption-level, a critical alert or other advanced APNs options.
{
"payload": {
"title": "Gate change",
"body": "Boarding now at B22.",
"apns": {
"aps": {
"interruption-level": "time-sensitive",
"thread-id": "flight-AF1234"
}
}
}
}It is merged over the generated APNs payload. Objects are merged recursively: aps.alert.title overrides the generated title but preserves
a generated body when the override omits it. A string or other non-object aps.alert replaces
the generated alert entirely. The reserved carillon delivery identifier is added afterwards.