Skip to main content
One template’s delivery funnel as JSON, bucketed over a window you choose.
shows the same funnel in the UI. returns it as JSON, so you can chart it in your own dashboard. Address the template by its nt_ ID or by an alias. Authenticate with a workspace as a bearer token. Each bucket breaks the funnel out per provider and channel. When a message fails over between providers, the series shows which one errored and which one 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: start 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. Pick one form or the other. A request carrying both a lookback and an absolute pair is a 400, not a silent preference for one of them, because the two spell the same thing and honouring both would discard whichever lost. A request with none of the three defaults to lookback=P30D. An end in the future is accepted and not clamped. Buckets past now 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: Both caps are measured on the snapped window, and each carries an allowance of a bucket at either edge. That is why a P90D window at DAY passes even though snapping widens it past 90 days. 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. No response carries more than 1000 buckets, and 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. Rates are yours to compute from those sums:
Guard every division. A quiet bucket has a sent of zero. 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, so you never reconstruct missing periods yourself.
  • 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. That is indistinguishable from a real template that has sent nothing. Validate template ids against if you need to tell the two apart.

Plan limits

Lookback is capped by plan, measured from the snapped start: On Enterprise the API itself imposes no lookback cap, so how far back you can reach is whatever the analytics store still holds. The console is narrower than the API here, because its custom-range calendar stops at January 1 of last year. 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, and a 429 adds Retry-After. If you are backfilling a dashboard, walk the windows sequentially rather than fanning out.

Errors

A 400 and a 402 both mean the request as written can never succeed, so retrying it unchanged just spends rate limit. Narrow the window or coarsen the granularity instead.

Covering a whole workspace

The endpoint is per template. For a workspace-wide view, list your templates with and call metrics for each one, sequentially. At 1 to 5 requests per second, fanning out concurrently just earns a 429.
The endpoint currently serves the US region only. Workspaces in the EU region cannot query it yet.