Skip to main content
The Template Analytics page shows one template’s delivery funnel in the Courier UI. 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:
  • Relativelookback, an ISO 8601 duration counted back from now: ?lookback=P90D, ?lookback=P12W, ?lookback=PT12H.
  • Absolutestart and end, 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 as start and end. Chart against those, not against what you sent:
Every boundary is UTC. There is no timezone parameter, so a “daily” series is UTC days — convert on your side if your reporting day starts elsewhere.
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.
There are no bucket-level totals: sum the rows in 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 data set 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.
  • errors counts provider attempts, not lost messages. A message that SendGrid rejected and Mailgun then delivered contributes to errors on one row and delivered on another. undeliverable is the one that means the message never landed on that channel.
  • opened and clicked are 0 on 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 200 with an all-empty series, not a 404 — indistinguishable from a real template that has sent nothing. Validate template ids against GET /notifications if you need to tell the two apart.

Plan limits

Lookback is capped by plan, measured from the snapped start: Reaching past the cap is a 402, not an empty result:
Rate limits on metrics reads are per second, per workspace: 1/s on Developer, 2/s on Business, 5/s on Enterprise. Every response carries 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.
The endpoint currently serves the US region only. Workspaces in the EU region cannot query it yet.