← Files Fastly Agent ToolkitARCHIVED FILE
skills/fastly-stats/references/api.md
12.8 KB · Oct 10, 2026 · 18:29 UTC
# Stats HTTP APIs
Two hosts, all `GET`, all authenticated with `Fastly-Key: <token>`.
- `https://api.fastly.com` for historical data.
- `https://rt.fastly.com` for live per-second data.
Three response shapes, and the parameter names differ between them. Sending the wrong vocabulary
is rejected.
| Shape | Endpoints | Envelope | Time params | Paging |
| --------- | -------------------------------------- | ------------------------------------------------------------------------ | ------------------------------ | ------------------ |
| Classic | `/stats*` | `{status, meta, msg, data}`, flat row per period | `from` / `to` / `by` | none |
| Inspector | `/metrics/{origins,domains}/services/` | `{status, meta, data}`, `dimensions` + `values[]` | `start` / `end` / `downsample` | `meta.next_cursor` |
| Real-time | `/v1/...` on `rt.fastly.com` | `{Data:[{recorded, aggregated, datacenter}], Timestamp, AggregateDelay}` | poll `ts/{timestamp}` | chain `Timestamp` |
## Classic historical, api.fastly.com
| Purpose | Path |
| -------------------------------- | ------------------------------------------- |
| All services, grouped by service | `/stats` |
| All services, one summed series | `/stats/aggregate` |
| One field across all services | `/stats/field/{field}` |
| One service | `/stats/service/{service_id}` |
| One service, one field | `/stats/service/{service_id}/field/{field}` |
| Account usage, by region | `/stats/usage` |
| Account usage, by service | `/stats/usage_by_service` |
| Month-to-date usage | `/stats/usage_by_month` |
| Per-POP summary, last 35 days | `/service/{service_id}/stats/summary` |
| Region codes | `/stats/regions` |
| POP catalog | `/datacenters` |
Query parameters on `/stats`, `/stats/aggregate`, `/stats/field/{field}` and the
`/stats/service/*` forms:
| Param | Notes |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `from` | Unix timestamp, ISO-8601, or a Chronic string (`1 day ago`). `yesterday` resolves to noon UTC. |
| `to` | Same formats. Defaults to now. |
| `by` | `minute`, `hour` or `day`. No `month`. Minute data is retained roughly one day. |
| `region` | One `stats_region` code. Silently overrides `datacenter` when both are sent. |
| `datacenter` | Comma-separated uppercase POP codes. A real filter; composes with any `by`. |
| `services` | `/stats` only: comma-separated service IDs. |
`data` grouping depends on the endpoint: `/stats` gives an object keyed by service ID,
`/stats/aggregate` a single array, `/stats/service/{id}` an array of period rows. Each row carries
`start_time` plus the measurement fields.
## Usage and billing endpoints
`/stats/usage` and `/stats/usage_by_service` take `from` / `to` / `by` / `region` and answer with
an object keyed by `stats_region`, each region carrying only `bandwidth`, `requests` and
`compute_requests`. Raw units, and `requests` excludes Compute traffic, so an account total must add
`compute_requests` per region.
`region` is accepted here and ignored. `?region=europe` returns all eleven regions, `data`
byte-identical to the unfiltered response, `meta.region` echoing `europe`. Same on
`/stats/usage_by_service`, whose `data` is keyed by region then by service ID. Only
`fastly stats usage --region` filters, client-side.
`/stats/usage_by_month` takes `year` (4-digit), `month` (2-digit) and `billable_units`. Its `data`
is `{customer_id, services, total}`, both `services` and `total` keyed by region.
`billable_units=true` rescales and nothing else: `bandwidth` / 1e9, `requests` and
`compute_requests` / 10,000, so a `requests` of `1.4452` means 14,452. For one month
`/stats/usage`, the per-service `/stats` sum and `/stats/usage_by_month` report the same byte total.
`bandwidth` already counts origin-bound request bytes, so the billable figure is not larger. See
<https://docs.fastly.com/products/how-we-calculate-your-delivery-bill>.
`/service/{id}/stats/summary` follows none of the conventions above. `start_time` and `end_time`
are required and must be epoch seconds; ISO-8601 returns `{"status":"error","msg":"invalid
start_time"}`. It is minutely-backed, so the window cannot start more than 35 days back, and it
answers `{"stats": {"<POP>": {...}}}` with no `data` / `meta` / `status` envelope. One aggregate
per POP, not a time series, so it does not replace `datacenter=`.
## POP catalog
`/datacenters` returns a flat array with `code`, `name`, `group`, `region`, `stats_region`,
`billing_region`, `coordinates`, and `shield` (the string a backend's `shield` setting takes).
`shield` is absent, not null, on POPs not offered as shields, so test with `.shield // "-"`.
Each POP carries four independent taxonomies and only one of them is what `region=` accepts:
| Field | Example values | Use |
| ---------------- | ---------------------------------------- | --------------------------------------------- |
| `stats_region` | `usa`, `europe`, `asia`, `anzac` | What `region=` takes |
| `region` | `US-East`, `EU-Central`, `North-America` | POP topology label, not accepted by `region=` |
| `group` | `United States`, `Europe`, `India` | Coarse reporting grouping |
| `billing_region` | `North America`, `Europe`, `Australia` | Invoice grouping |
`North-America` holds four Canadian POPs (`YYC`, `YUL`, `YYZ`, `YVR`) and no US POPs, and every
one of them reports `stats_region = usa`. Region-level stats cannot isolate Canada, so filter by
POP code instead.
The `stats_region` values accepted by `region=`, from `GET /stats/regions`: `africa_std`, `anzac`,
`asia`, `asia_india`, `asia_southkorea`, `europe`, `latam`, `mexico`, `saudi_arabia`,
`southamerica_std`, `usa`.
## Inspector, api.fastly.com
```text
GET /metrics/origins/services/{service_id}
GET /metrics/domains/services/{service_id}
```
| Param | Notes |
| ------------ | --------------------------------------------------------------------------------- |
| `start` | Inclusive. ISO-8601 with `Z`, or Unix timestamp. No relative strings. |
| `end` | Exclusive. Same formats. |
| `downsample` | `minute`, `hour` or `day`. This is the Inspector's `by`. |
| `metric` | Comma-separated metric names. This is the Inspector's `field`. |
| `group_by` | Origin: `host`, `region`, `datacenter`. Domain: `domain`, `region`, `datacenter`. |
| `region` | Comma-separated region filter. |
| `datacenter` | Comma-separated uppercase POP codes. |
| `host` | Origin Inspector only. |
| `domain` | Domain Inspector only. |
| `limit` | Rows per page, max 200. |
| `cursor` | From the previous response's `meta.next_cursor`. |
`data[]` is one entry per dimension combination; `data[].values[]` is sparse, aligned to the time
buckets, with zero metrics omitted from an element. Loop on `next_cursor` until it is empty or
you silently truncate the result.
Both Inspectors are paid add-ons enabled per service. When the product is off the endpoint still
answers HTTP 200 with `"status":"success"` and `"data":[]`, indistinguishable from a service that
had no traffic. Check `fastly products -s ID` first.
The two paths do not share an envelope. Success is `{data, meta, status:"success"}`, `status` a
string, `meta` echoing the defaults `limit: 100`, `sort`, `group_by`, `filters: {}`,
`next_cursor: ""`. Rejection is HTTP 400 with an RFC 7807 body, `status` a number, no `msg` either
way:
```json
{"type":"https://fastly.help/metrics/validation-error","title":"Request parameters were invalid.",
"status":400,"errors":[{"property":"start","reason":"failed to parse 'start' time"}],
"detail":"Parameters with invalid values: 'start'"}
```
Branch on `.status == "success"`, not on `.status`. Read causes from `errors[].reason`. Content-Type
is `application/json` on both paths.
Metric names take a source prefix (`all_`, `compute_`, `waf_`) or none: `metric=responses` and
`metric=all_responses` both validate. Strip the prefix to look a name up in the Field names and
aggregation shape reference. An unknown name is a 400, `Unrecognized metric names: '...'`, and names
are validated even where the product is off, so a 400 says nothing about entitlement. No cap on how
many: 20 in one call come back echoed in `meta.metric`, despite the CLI help's 10.
Origin Inspector adds latency histogram buckets (`all_latency_0_to_1ms` through
`all_latency_60000ms`), which is where origin response time lives. Edge processing time is a
different measurement and comes from classic `hits_time` / `miss_time` / `pass_time`.
Real-time per-domain and per-origin data lives on `rt.fastly.com` instead:
`/v1/domains/{id}/ts/{ts}` and `/v1/origins/{id}/ts/{ts}`, both taking the `ts/h` and
`ts/h/limit/{n}` forms below.
## Real-time, rt.fastly.com
```text
GET /v1/channel/{service_id}/ts/{timestamp} # 0 for the most recent complete second
GET /v1/channel/{service_id}/ts/h # up to the last 120 seconds in one call
GET /v1/channel/{service_id}/ts/h/limit/{n} # last n entries, n <= 120
```
`/v1/origins/{id}/...` and `/v1/domains/{id}/...` take the same forms.
It is a long poll, not a one-shot query. Start at `ts/0`, then pass the response's `Timestamp`
back as the next path value; do not compute `ts+1` yourself. The response is cached with a
1-second TTL, so keep one request outstanding at a time. `AggregateDelay` says how many seconds
behind real time the newest data is. On a quiet service the payload is
`{"Data":[],"Timestamp":...,"Error":"No data available, please retry"}` with HTTP 200: an `Error`
field, not an HTTP error.
`Data[]` holds one record per elapsed second: `recorded`, `aggregated` (all-POP totals keyed by
metric name), and `datacenter` (the same metrics keyed by POP code). `ts/h` returns only seconds
that had traffic, so `Data | length` is a sample count, not a window in seconds, and the `recorded`
span is not one either: it stops at the last second that had traffic. Divide a rate by 120, the
lookback `ts/h` covers. The span is still worth reading, as the tell for how bursty the traffic is:
```bash
curl -sS -H "Fastly-Key: $(fastly auth token)" \
"https://rt.fastly.com/v1/channel/$SID/ts/h" \
| jq 'if (.Data|length) == 0 then {samples: 0, window_s: 120, requests: 0, rps: 0, error: .Error}
else {samples: (.Data|length), window_s: 120,
span_s: (([.Data[].recorded]|max) - ([.Data[].recorded]|min) + 1),
requests: ([.Data[] | (.aggregated.requests // 0)
+ (.aggregated.compute_requests // 0)]|add)}
| . + {rps: (.requests / .window_s)} end'
```
Guard the empty case first: on a quiet service `max` and `min` over an empty array both return
`null` and the subtraction aborts the filter with `null (null) cannot be subtracted`. The newest
`recorded` runs about 8 seconds behind the wall clock, which is `AggregateDelay`, so the window is
the 120 seconds ending then rather than ending now.
Real-time offers no `by` or region filtering: you get every second for the whole service and
filter client-side from the `datacenter` map. For flexible windows, regions or fields, including
per-POP history, use classic historical with `datacenter=`.
`fastly stats realtime --json` reshapes this. It emits one flat object per second,
`{recorded, aggregated, datacenter}`, with no `Data` array and no `Timestamp`, and it chains the
poll for you. A `jq` filter written for the raw payload (`.Data[]`) matches nothing against CLI
output, and with `.Data[]?` it fails silently.
SHA-256: 04cd565202042ec5810ab4f165605dc0ea19f601f05fb3abbb63f5c645453368