Documentation
Sign inGet started

SMS stats API

Every number on the Metrics dashboard comes from the SMS stats API, and you can read the same aggregates yourself to build your own dashboard, feed a warehouse, or wire a health check into your alerting. The endpoints are read-only and workspace-scoped, and they need an API key with read access to the sms scope. They read the same aggregation the dashboard does, so a value you fetch matches what the dashboard shows for the same range.
Typed methods ship in the TypeScript, Python, PHP, and Go SDKs under sms.stats, the bird CLI exposes them as bird sms stats, and an agent reaches them through the sms_stats_* MCP tools. Full request and response schemas are in the API reference.

The aggregate and the time series

Three endpoints cover the top of the dashboard:
  • GET /v1/sms/stats/summary returns one aggregate row for the whole window: the lifecycle counts (accepted, sent, delivered, undelivered, failed, rejected, expired), the derived delivery_rate and failure_rate, and the processing, delivery, and total latency percentiles (p50, p95, p99). Pass compare=previous_period and the response also includes the preceding equal-length window and the change from it.
  • GET /v1/sms/stats/daily and GET /v1/sms/stats/hourly return the lifecycle counts one row per day or per hour. Rates and latency are whole-window figures, so read those from /summary rather than per bucket.
Every rate comes back as a fraction between 0 and 1, so a delivery_rate of 0.9739 is 97.39%. A rate whose denominator is zero is null, which is how a period that accepted nothing reports delivery_rate rather than reading 0. And figures attribute by send time, not event time: a delivery confirmed today for a message accepted yesterday counts against yesterday. A recent window therefore under-reports delivered while its delivery reports are still arriving, and its counts only ever grow toward the truth, so treat the last few hours as provisional rather than final.

Choosing the window

from and to take either a calendar day (YYYY-MM-DD) or an RFC 3339 instant, and which forms an endpoint accepts differs:
EndpointBoundsMaximum window
/summaryBoth days, or both instants365 days, or 720 hours on instants
/dailyCalendar days365 days
/hourlyRFC 3339 instants720 hours (30 days)
Instant bounds are hour-grain, which is what makes a rolling "last 24 hours" a single request. On /summary, mixing a day with an instant returns a 422.
Set timezone to an IANA identifier such as America/New_York to have day and hour boundaries, and the defaults used when you omit from and to, computed in that zone instead of UTC. While timezone is set, from and to must not include a UTC offset of their own.

Breakdowns

Seven endpoints slice the same delivery numbers by one dimension:
  • Where it went: /countries (the destination country) and /carriers (the carrier that handled it).
  • What you sent: /originators (the sender address it went out from), /categories, and /tags.
  • How it ended: /statuses (one row per lifecycle status with activity) and /error-codes.
All of them live under /v1/sms/stats/. Rows come back ranked descending by a sort metric and capped at limit (default 50, maximum 200), alongside total, the number of distinct values the dimension held in the window, which is how you know a capped result is the top slice of something larger. Rows whose sort metric is a rate with a zero denominator go last.
sort defaults to accepted on the volume breakdowns and to failed on /error-codes, which is the metric that breakdown exists to rank by. /error-codes groups by Bird's own normalized failure reason rather than a raw carrier code, and that value is the same one the error_code filter on GET /v1/sms/messages takes, so a row joins straight to the messages behind it, which is the usual next step after spotting a spike.
/tags counts only tagged messages, and a message carrying several tags is counted once under each, so its rows do not sum to the period total. Reconcile it against itself, not against /summary.
/statuses is the odd one out: its rows carry a status and a count rather than the full delivery block, because a status is the outcome and there is nothing further to break down.

Received messages

Six more endpoints under /v1/sms/stats/inbound/ count what your numbers received rather than what you sent: /summary, /daily and /hourly for the totals and the series, and /countries, /operators and /numbers for the breakdowns. They take the same window and timezone parameters as their outbound counterparts.
Two things differ from the outbound family. Each row carries a plain received count, with no delivery block or rates: an inbound message either arrived or did not, so there is no lifecycle to aggregate. And /operators excludes messages whose sending operator the carrier did not report, so its rows can sum to less than /inbound/summary for the same period; reconcile against the summary, not against the operator rows.

Next steps

  • Metrics: the dashboard these numbers render
  • SMS log: the per-message view behind the aggregates
  • Events: the per-message event stream the aggregates are built from