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
dataJSON object, including nested objects and arraysTop-level string values up to 1024 characters; provider payload size limits still apply
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.

Campaign tags

Add an optional top-level tags array to classify a send:

{
  "audience": { "external_ids": ["user-123"] },
  "tags": ["new_comment", "social"],
  "payload": { "title": "New comment", "body": "Someone replied to your post." }
}

Use up to 10 strings of 1-64 characters. Matching is exact and case-sensitive; duplicates are removed. Omitting tags stores an empty list. Tags belong to the campaign, are shared by all its deliveries, and cannot be edited after creation. They are returned by GET /v1/messages/{id} and appear in the dashboard's message list and campaign detail. Use the Tag filter in Messages to find campaigns with a particular tag, or add tags in the dashboard's notification composer.

We recommend a small set of stable categories such as like, new_comment and chat_message. Avoid user IDs, post IDs, timestamps and other unique values. Put per-notification context in payload.data instead.

Campaign tags are separate from device and user tags: they do not select an audience and are never included in the APNs or FCM payload. They support filtering message history and comparing aggregate delivery statistics.

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 may be strings, numbers, booleans, nulls, objects or arrays:

{
  "data": {
    "route": "order",
    "order": { "id": "42", "items": [{ "sku": "rod", "quantity": 2 }] },
    "actions": ["view", "track"]
  }
}

APNs carries objects and arrays directly. For FCM, Carillon JSON-encodes them and marks which fields were encoded. Starting with Kotlin and React Native SDK 0.3.0, the SDKs decode only those fields; a string that looks like JSON stays a string. React Native exposes the decoded values in onReceived's data and onOpened's payload. Kotlin exposes them in structuredData, while data remains the original string map. Swift receives the original JSON values. Older Android SDKs receive JSON strings and require manual decoding.

Existing scalar behavior is unchanged: Android receives numbers and booleans as strings, and null as an empty string. iOS preserves their JSON types. The final provider payload, including encoded data and tracking metadata, must fit within 4096 UTF-8 bytes.

`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

List tags already used

Use a secret key with deliveries:read to discover existing notification labels:

curl https://api.carillon.dev/v1/messages/tags \
  -H "Authorization: Bearer $CARILLON_SECRET_KEY"
{ "tags": ["like", "new_comment"], "next_cursor": null }

Results contain distinct tags from stored campaigns in the key's app and mode: test keys see test campaigns, live keys see live campaigns. Each page contains up to 100 labels sorted by label. When next_cursor is not null, URL-encode it and pass it as cursor to retrieve the next page. An app with no tags returns an empty tags array and a null cursor.

Agents can use notification_tags_list to discover the same labels before sending.

Compare tag statistics

In the dashboard, open Overview → Statistics and choose the last 7, 30 or 90 days. The table covers live campaigns created in that period and follows the source filter inside that block. Click a tag to open the filtered message list. In Messages, use the Tags multi-select to match any of several labels. Without tag selects untagged campaigns.

Each row shows campaigns, recipients, sent, failed, pending and opened deliveries, plus the open rate (opened / sent). Recipients count deliveries, not unique users across campaigns. Counts use retained delivery history. A campaign with multiple tags contributes to each tag, so do not add the rows together as a global total. The All row and the KPI totals count each campaign once, including untagged campaigns.

Completed campaigns with no recipients are hidden from the message list by default. Enable Include messages with no recipients to see them. Scheduled and in-progress campaigns remain visible, and direct campaign links still work.

On this page