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.