Track opens
Connect notification tap callbacks and read recorded open events.
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.
Reporting an open yourself
If you are not using an SDK, POST /v1/events takes batches of up to 100, with a mobile key.
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.
opened is the only event type today. First report wins: a second report for the same delivery
changes nothing, so the queue is safe to retry.
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.