GET /notifications/{id}/metrics returns the same numbers as JSON, bucketed over a window you choose, so you can chart them in your own dashboard or export them on a schedule.
Each bucket breaks the funnel out per provider and channel, which is what makes the endpoint worth calling instead of counting sends yourself: when a message fails over between providers, the series shows which one errored and which one eventually delivered.
Choosing the window
Ask for a window in one of two ways:- Relative —
lookback, an ISO 8601 duration counted back from now:?lookback=P90D,?lookback=P12W,?lookback=PT12H. - Absolute —
startandend, ISO 8601 timestamps:?start=2026-04-01T00:00:00Z&end=2026-05-01T00:00:00Z.
start and end are pair-or-nothing; sending one without the other is a 400, as is a start that is not earlier than end. When a request carries both an absolute pair and a lookback, the pair wins and the lookback is ignored. A request with none of the three defaults to lookback=P30D.
An end in the future is accepted and not clamped — the buckets past now simply come back empty.
Boundaries are snapped, and echoed back
Courier widens the window you asked for onto the granularity grid: the start is floored to its bucket, the end is ceiled to the next boundary. That way every bucket your window touches is returned whole rather than half-counted. The response echoes the snapped boundaries asstart and end. Chart against those, not against what you sent:
WEEK buckets start on Sunday, matching the analytics store Courier queries. A Monday-based week has to be assembled from DAY buckets.Granularity
granularity sets the bucket size: HOUR, DAY, WEEK, or MONTH. It defaults to DAY.
Fine granularities are capped by how much window they can cover, so a single response stays a reasonable size:
Asking for a finer granularity than the window allows is a
400 — the fix is a coarser granularity or a shorter window, not a retry:
WEEK and MONTH have no window cap, but no response carries more than 1000 buckets; beyond that you get a 400 too.
Reading the response
series holds one entry per bucket, oldest first. Each entry carries the bucket’s start (period) and a data array with one row per provider and channel that handled a message in that bucket.
data for a bucket’s total, or filter them by channel to chart one channel on its own.
A few things worth knowing before you plot it:
- Every bucket in the window comes back, including the quiet ones, with
dataset to[]. The series is directly plottable — you never have to reconstruct missing periods, which is exactly the bucket-boundary logic (Sunday weeks included) you don’t want to reimplement. errorscounts provider attempts, not lost messages. A message that SendGrid rejected and Mailgun then delivered contributes toerrorson one row anddeliveredon another.undeliverableis the one that means the message never landed on that channel.openedandclickedare0on channels with no tracking, such as SMS without link tracking. A zero there means “not measured”, not “nobody read it”.- Sends without a template never appear. A message sent with inline content has no template to attribute, so it is counted nowhere in this endpoint.
- An unknown template id returns
200with an all-empty series, not a404— indistinguishable from a real template that has sent nothing. Validate template ids againstGET /notificationsif you need to tell the two apart.
Plan limits
Lookback is capped by plan, measured from the snappedstart:
Reaching past the cap is a
402, not an empty result:
RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset; a 429 adds Retry-After. If you are backfilling a dashboard, walk the windows sequentially rather than fanning out.
A 503 with Retry-After means the analytics store is briefly unavailable — retry the same request after the stated delay.