SMS stats API
Every value on the Metrics dashboard comes from the SMS stats API. Use the same aggregates in a dashboard, data warehouse, or health check. The read-only, workspace-scoped endpoints require an API key with read access to the sms scope. A response matches the dashboard for the same range and filters.
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. Figures use the message's send 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:
| Endpoint | Bounds | Maximum window |
|---|---|---|
| /summary | Both days, or both instants | 365 days, or 720 hours on instants |
| /daily | Calendar days | 365 days |
| /hourly | RFC 3339 instants | 720 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 compute boundaries and omitted defaults in that zone instead of UTC. When timezone is set, Bird rejects numeric UTC offsets such as +05:45 in instant bounds. Use Z instants or calendar days instead.
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/. Country, carrier, originator, category, tag, and error-code rows are ranked by sort and capped by limit (default 50, maximum 200). Their responses include total, the number of distinct values in the window, so you can detect a capped result. Rows whose sort metric is a rate with a zero denominator appear last. The status breakdown has at most seven rows and has no sort or limit parameters.
The six ranked breakdowns can also return a short series per row. Set include_trend=true and choose trend_grain=daily or hourly. Trends require limit of 50 or less and a window of at most 90 days for daily buckets or 720 hours for hourly buckets.
sort defaults to accepted on volume breakdowns and to failed on /error-codes. The /error-codes endpoint groups by Bird's normalized failure reason rather than a raw carrier code. Its value also works with the error_code filter on GET /v1/sms/messages, linking a row to its messages.
/tags counts only tagged messages, and a message carrying several tags is counted once under each. Its rows therefore do not sum to the period total. Reconcile one /tags result with another instead of using /summary.
/statuses returns a status and count rather than the full delivery block because the status is the outcome.
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, because an inbound message has no delivery lifecycle to aggregate. The /operators endpoint excludes messages whose sending operator the carrier did not report, so its rows can sum to less than /inbound/summary for the same period. Use the summary as the workspace total; operator rows cover only messages with a reported operator.