Carillon docs

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." }
    }
  }
}
FieldShapeNotes
title1 to 256 charactersOptional on its own
body1 to 2048 charactersOptional on its own
ttlInteger from 0 to 2419200 secondsDefault 259200 (72 hours), starting at the first send attempt
priorityhigh or normalDefault high; APNs 10/5 and FCM HIGH/NORMAL
collapse_id1 to 64 UTF-8 bytesAPNs collapse header and FCM collapse key
imageHTTPS URL, up to 2048 charactersLocalizable; iOS requires a notification service extension
subtitle1 to 256 charactersiOS alert subtitle; localizable
thread_id1 to 64 charactersiOS notification grouping; included in the SDK stamp for Android foreground grouping
sound1 to 64 charactersNative sound name; default when omitted. Android channels may override it
badgeNonnegative integerAbsolute badge count, not an increment
androidRaw Android configuration objectRecursively merged over generated FCM Android options
dataFlat map of strings, numbers, booleans and nullsString values up to 1024 characters; no nested objects or arrays
localizationsMap keyed by BCP 47 tagEach entry overrides only the fields it carries
apnsRaw APNs objectRecursively 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:

PlatformFormatsLimit
iOS attachmentjpg, png, gifThe extension downloads up to 10 MiB. webp cannot be attached: the notification falls back to text
Android, rendered by the systemWhat FCM accepts, including jpg, png, webpFCM downloads up to 1 MiB; a larger image is dropped and the text is shown
Android, app in the foregroundWhat the platform decodesThe 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.

Devices · Delivery · Send a notification

On this page