Carillon docs

Track receipts and opens

Report observed receipts and notification taps, and read their delivery timestamps.

Every notification Carillon sends carries a carillon key in its payload, and inside it a delivery_id. Reporting an open means sending that identifier back.

The delivery identifier

Use the delivery_id included in the received notification. A campaign ID or device ID is not accepted in its place. The identifier associates the event with a delivery; it is not independent proof that a person read the notification.

What the SDKs do

Complete the SDK setup for your platform, including its native callbacks. Forward notification taps as follows.

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completion: @escaping () -> Void) {
  Carillon.didOpen(response)
  completion()
}

didOpen reads carillon.delivery_id from the notification's userInfo and queues an opened event with the tap's instant.

Set UNUserNotificationCenter.current().delegate as early as you can in didFinishLaunching, so that an app launched by a tap has somewhere to deliver its open to.

The reserved key is an object on iOS and a JSON string on Android, because that is what APNs and FCM respectively carry. The SDKs surface deliveryId separately, so an app does not have to read either form.

Track receipts

With Swift, Kotlin or React Native SDK 0.5.0, receipt tracking runs automatically once the SDK and native notification callbacks are configured. It records a received event independently of foreground presentation: suppressing a banner in your callback still means the SDK received the message.

At configuration and each return to the foreground, the SDK also checks notifications still present in Notification Center (iOS) or the notification tray (Android). Opening the app directly can therefore report receipts without reporting any opens. Notifications already removed or replaced are absent from this list.

On iOS, configure a shared App Group for the app and notification service extension to retain receipts before the app launches. The extension stores the delivery ID and observed date; the host app reports them on its next launch or activation. Expo's appGroup option configures both targets. Carillon supplies mutable-content: 1 on alert payloads. Overriding it, silent notifications, or iOS declining to run the extension prevents that capture path.

On Android, background recovery uses Carillon's generated notification tag. Overriding android.notification.tag disables recovery from the tray for that notification. Forwarded receive callbacks still report receipts.

Read a receipt

In the dashboard, open the individual delivery trace. The confirmed row shows the receipt timestamp reported by the SDK. The notification may have arrived earlier, especially when recovered on app launch. Hover or tap an event label for its explanation. All trace timestamps use the browser's timezone and include the date, time and milliseconds.

Receipt and open events retain their original timestamp in the SDK's persistent queue when a network request fails. Retrying later, including after an app restart, does not replace that timestamp with the upload time, subject to the clock validation below.

The API calls the receipt event received. Its delivery objects also expose the server report time for diagnostics:

{
  "received_at": "2026-10-06T09:00:00.000Z",
  "received_reported_at": "2026-10-06T10:15:00.000Z",
  "opened_at": null
}

Both receipt fields remain null without a report. received_at is the device's observed timestamp, subject to the clock validation below. The server's received_reported_at can be later when the device is offline or the extension waits for the host app. The first report wins even if a later report supplies an earlier observed timestamp.

sent means provider acceptance. received means the SDK observed the notification. opened means a reported tap. Neither a receipt nor a tap proves that a person read the content. Missing receipts do not prove non-delivery: the app might not have resumed, the notification might be removed, or the installed SDK might not report receipts. Receipt fields share delivery-history retention.

Reporting an open yourself

If you are not using an SDK, POST /v1/events takes batches of up to 100, with a mobile key. Use type: "received" for an observed receipt or type: "opened" for a tap.

curl -X POST https://api.carillon.dev/v1/events \-H "Authorization: Bearer carillon_mk_live_your_mobile_key" \-H "Content-Type: application/json" \-d '{  "events": [    {      "type": "opened",      "delivery_id": "01937b21-0000-7000-8000-000000000000",      "at": "2026-08-20T09:16:44.120Z"    }  ]}'
{ "received": 1 }

received is how many events were in the request, never how many were recorded. Unknown, foreign and already-reported identifiers are dropped without being counted or reported back.

at is when it happened on the device. It is stored as sent unless the clock is impossible, in the future or more than thirty days old, in which case the moment we received it is stored instead.

Reading opens back

Read opened_at on a delivery returned by GET /v1/messages/{id}.

{
  "id": "01937b21-0000-7000-8000-000000000000",
  "device_id": "01937b1e-0000-7000-8000-000000000000",
  "status": "sent",
  "error_code": null,
  "opened_at": "2026-08-20T09:16:44.120Z"
}

What a null opened_at can mean

opened_at stays null for a notification nobody opened and for one opened by an app that never reported it. An app running an SDK version without the forwarding call, or one whose delegate is attached too late, produces the same result as an unopened notification. Compare open rates between campaigns of the same app version rather than reading one in isolation.

First report wins independently for received and opened: a duplicate event changes nothing, so the persistent queue is safe to retry. Receipt events do not change open counts or billable reach.

Campaign totals and platforms

GET /v1/messages/{id} returns outcomes.opened: the number of deliveries with at least one reported open across the whole campaign, including those outside the sampled delivery list. An unreported open is not counted.

platforms.ios and platforms.android each contain sent, failed, pending, opened and total. sent means provider acceptance, not display or receipt. Platform counts remain available after a device is deleted. platforms reports ios and android only; deliveries written before platform recording was introduced are counted in outcomes and in neither bucket. New deliveries always record their platform.

Delivery traces are retained for at least 30 days. The hourly curve below covers only the first 48 hours; it does not limit trace retention or lifetime open counts.

Hourly counts

With a secret key carrying deliveries:read, call GET /v1/messages/{id}/opens.

{ "hours": [{ "hour": "2026-08-20T09:00:00.000Z", "opened": 12 }] }

The curve covers the first 48 hours after campaign creation, through the current hour. It is empty if there are no recorded opens. This is a time window, not the campaign's lifetime open total. Campaigns from another app return 404; mobile keys cannot read this endpoint.

Notifications · SDK overview · Report events

On this page