| name | cloudflare-analytics |
| description | Cloudflare analytics via GraphQL API. Query HTTP request stats (status codes, 404s, top paths), video streaming analytics (minutes watched, top videos), geographic breakdowns, and time series data. Use when asked about Cloudflare analytics, HTTP errors, video stats, or traffic data. |
Cloudflare Analytics
Query HTTP traffic and video streaming analytics from Cloudflare's GraphQL API.
When to Use
- Investigate HTTP errors (404s, 5xx, etc.) across zones
- Analyze traffic patterns by path, host, country, or time
- Get video view counts and minutes watched
- Find top-watched videos
- Get daily/weekly/monthly trends
- Look up metadata for specific video UIDs
Prerequisites
export CLOUDFLARE_STREAM_API_TOKEN=your-api-token
export CLOUDFLARE_STREAM_ACCOUNT_ID=your-account-id
Token Permissions
The API token needs these permissions:
- Account โ Account Analytics โ Read (for Stream video analytics)
- Account โ Stream โ Read (for video metadata)
- Zone โ Analytics โ Read (for HTTP/zone analytics)
Create or edit tokens at: https://dash.cloudflare.com/profile/api-tokens
Verify Token
bash -c 'curl -s -H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" "https://api.cloudflare.com/client/v4/user/tokens/verify"' | jq .
Important: When using $VAR in a command that pipes to another command, wrap the command containing $VAR in bash -c '...'. Environment variables are silently cleared when pipes are used directly.
Cloudflare cf CLI Notes
The official Cloudflare cf CLI is useful for auth, zone lookup, and DNS analytics, but as of cf v0.0.5 it does not expose the GraphQL HTTP/Stream analytics queries used below.
- In pi/non-login shells,
cf may only be on the mise login-shell PATH. Use zsh -lic 'cf ...' if bash cannot find it.
- Check auth with
zsh -lic 'cf auth whoami'. Do not print or inspect stored token values.
- Find the Labs zone with:
zsh -lic 'cf zones list --name wearedevs.ai --fields id,name,status,account'
- DNS analytics works through the CLI, but the free plan only allows a 6-hour query window. The CLI currently resolves
CLOUDFLARE_ACCOUNT_ID from the environment more reliably than --account-id:
zsh -lic 'CLOUDFLARE_ACCOUNT_ID=... cf dns analytics report --zone wearedevs.ai --since 2026-05-27T00:00:00Z --until 2026-05-27T06:00:00Z --metrics queryCount --dimensions queryName,responseCode --limit 20 --sort=-queryCount'
cf agent-context analytics and cf schema --list mention deprecated zone analytics commands, but those commands are not registered in the top-level CLI in v0.0.5. Keep using the GraphQL API for HTTP status/path analytics and Stream analytics.
- If
cf auth whoami shows OAuth auth with analytics scopes, GraphQL accepts that OAuth access token. If no suitable API token is exported, use a local script to load access_token from ~/.cf/config.toml and send the GraphQL request in memory. Never cat, read, echo, log, or paste the token value.
List Zones
Find zone IDs (needed for HTTP analytics):
bash -c 'curl -s -H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones?account.id=$CLOUDFLARE_STREAM_ACCOUNT_ID"' | jq '.result[] | {id, name, status}'
Zone / HTTP Analytics
Endpoint: POST https://api.cloudflare.com/client/v4/graphql
Uses httpRequestsAdaptiveGroups under zones. Requires zone tags.
Constraints
- Max query interval: 24 hours (use
datetime_geq/datetime_lt with ISO 8601 timestamps)
- For longer ranges, split into multiple 24h queries and merge results
Requests by Status Code (e.g., 404s)
Write to /tmp/cf_request.json (replace $ZONE_TAG):
{
"query": "{ viewer { zones(filter: {zoneTag: \"$ZONE_TAG\"}) { httpRequestsAdaptiveGroups(filter: {datetime_geq: \"$START_DATETIME\", datetime_lt: \"$END_DATETIME\", edgeResponseStatus: 404}, limit: 50, orderBy: [count_DESC]) { count dimensions { clientRequestHTTPHost clientRequestPath } } } } }"
}
Replace $ZONE_TAG, $START_DATETIME (e.g., 2026-03-03T00:00:00Z), $END_DATETIME. Then run:
bash -c 'curl -s -X POST \
-H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/graphql" \
-d @/tmp/cf_request.json' | jq '.data.viewer.zones[].httpRequestsAdaptiveGroups'
Multiple Zones in One Query
Use zoneTag_in to query multiple zones at once:
{
"query": "{ viewer { zones(filter: {zoneTag_in: [\"$ZONE_TAG_1\", \"$ZONE_TAG_2\"]}) { httpRequestsAdaptiveGroups(filter: {datetime_geq: \"$START_DATETIME\", datetime_lt: \"$END_DATETIME\", edgeResponseStatus: 404}, limit: 50, orderBy: [count_DESC]) { count dimensions { clientRequestHTTPHost clientRequestPath } } } } }"
}
Filter by Path Pattern
Use clientRequestPath_like with SQL LIKE wildcards:
{
"query": "{ viewer { zones(filter: {zoneTag: \"$ZONE_TAG\"}) { httpRequestsAdaptiveGroups(filter: {datetime_geq: \"$START_DATETIME\", datetime_lt: \"$END_DATETIME\", clientRequestPath_like: \"%rss%\"}, limit: 20, orderBy: [count_DESC]) { count dimensions { clientRequestHTTPHost clientRequestPath clientRequestHTTPMethodName } } } } }"
}
Top Paths by Request Count
{
"query": "{ viewer { zones(filter: {zoneTag: \"$ZONE_TAG\"}) { httpRequestsAdaptiveGroups(filter: {datetime_geq: \"$START_DATETIME\", datetime_lt: \"$END_DATETIME\"}, limit: 50, orderBy: [count_DESC]) { count dimensions { clientRequestHTTPHost clientRequestPath edgeResponseStatus } } } } }"
}
HTTP Dimensions
| Dimension | Description |
|---|
clientRequestHTTPHost | Hostname |
clientRequestPath | Request path |
clientRequestHTTPMethodName | HTTP method (GET, POST, etc.) |
edgeResponseStatus | HTTP status code |
clientCountryName | Visitor's country |
clientRequestHTTPProtocol | Protocol (HTTP/1.1, HTTP/2, etc.) |
HTTP Filters
| Filter | Description |
|---|
zoneTag / zoneTag_in | Required. Zone ID(s) |
datetime_geq | Start datetime (inclusive, ISO 8601) |
datetime_lt | End datetime (exclusive, ISO 8601) |
edgeResponseStatus | Filter by status code (e.g., 404, 500) |
clientRequestPath_like | Path pattern (SQL LIKE with % wildcards) |
clientRequestHTTPHost | Filter by hostname |
clientRequestHTTPMethodName | Filter by method |
Stream Video Analytics
Uses streamMinutesViewedAdaptiveGroups under accounts.
Constraints
- Max query interval: 31 days per query (use
date_geq/date_lt with YYYY-MM-DD)
- Data retention: 90 days
- The
count field in responses is the number of aggregated data points, not play events or unique viewers โ only sum.minutesViewed is meaningful
Top Videos
Write to /tmp/cf_request.json:
{
"query": "{ viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { streamMinutesViewedAdaptiveGroups(filter: {date_geq: \"$START_DATE\", date_lt: \"$END_DATE\"}, orderBy: [sum_minutesViewed_DESC], limit: 20) { sum { minutesViewed } dimensions { uid } count } } } }"
}
Replace $ACCOUNT_ID with CLOUDFLARE_STREAM_ACCOUNT_ID, set date range (max 31 days). Then run:
bash -c 'curl -s -X POST \
-H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/graphql" \
-d @/tmp/cf_request.json' | jq .
Minutes Watched Per Video
{
"query": "{ viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { streamMinutesViewedAdaptiveGroups(filter: {uid: \"$VIDEO_UID\", date_geq: \"$START_DATE\", date_lt: \"$END_DATE\"}, orderBy: [sum_minutesViewed_DESC], limit: 100) { sum { minutesViewed } dimensions { uid date } count } } } }"
}
Geographic Breakdown (Videos)
{
"query": "{ viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { streamMinutesViewedAdaptiveGroups(filter: {date_geq: \"$START_DATE\", date_lt: \"$END_DATE\"}, orderBy: [sum_minutesViewed_DESC], limit: 50) { sum { minutesViewed } dimensions { clientCountryName } count } } } }"
}
Daily Time Series (Videos)
{
"query": "{ viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { streamMinutesViewedAdaptiveGroups(filter: {date_geq: \"$START_DATE\", date_lt: \"$END_DATE\"}, orderBy: [date_ASC], limit: 100) { sum { minutesViewed } dimensions { date } count } } } }"
}
Combined Dimensions (Video + Country + Date)
{
"query": "{ viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { streamMinutesViewedAdaptiveGroups(filter: {date_geq: \"$START_DATE\", date_lt: \"$END_DATE\"}, orderBy: [sum_minutesViewed_DESC], limit: 100) { sum { minutesViewed } dimensions { uid clientCountryName date } count } } } }"
}
Stream Dimensions
| Dimension | Description |
|---|
uid | Video UID |
clientCountryName | Viewer's country |
date | Date (YYYY-MM-DD) |
Dimensions can be combined in a single query.
Stream Order By
| Order | Description |
|---|
sum_minutesViewed_DESC | Most watched first |
sum_minutesViewed_ASC | Least watched first |
date_ASC | Oldest date first |
date_DESC | Newest date first |
Stream Filters
| Filter | Description |
|---|
accountTag | Required. Your Cloudflare account ID |
uid | Filter to a specific video UID |
date_geq | Start date (inclusive, YYYY-MM-DD) |
date_lt | End date (exclusive, YYYY-MM-DD) |
Video Metadata API
Look up video details by UID:
bash -c 'curl -s \
-H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_STREAM_ACCOUNT_ID/stream/$VIDEO_UID"' | jq '.result | {uid, meta, thumbnail, duration, created}'
List All Videos
bash -c 'curl -s \
-H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_STREAM_ACCOUNT_ID/stream?per_page=50"' | jq '.result | length'
Storage Usage
bash -c 'curl -s \
-H "Authorization: Bearer $CLOUDFLARE_STREAM_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_STREAM_ACCOUNT_ID/stream/storage-usage"' | jq .
Important Notes
- Zone analytics use
datetime_geq/datetime_lt (ISO 8601) with a 24-hour max window
- Stream analytics use
date_geq/date_lt (YYYY-MM-DD) with a 31-day max window
videoPlaybackEventsAdaptiveGroups only works with Cloudflare's own Stream Player embed. If using hls.js/vidstack/custom players, it returns empty. Use streamMinutesViewedAdaptiveGroups instead.
- The
count field in Stream results counts aggregated data segments, not unique viewers or play events.
Troubleshooting
Empty Results
- Verify the token has the required permissions (see Prerequisites)
- For zone analytics: ensure
datetime_geq and datetime_lt are at most 24 hours apart
- For stream analytics: ensure dates are within the 90-day retention window and at most 31 days apart
- Use
streamMinutesViewedAdaptiveGroups, not videoPlaybackEventsAdaptiveGroups
"zones not authorized" Error
The token needs Zone โ Analytics โ Read permission. Update at https://dash.cloudflare.com/profile/api-tokens
"account does not have access" Error
The token needs Account โ Account Analytics โ Read permission.
"does not have permission 'com.cloudflare.api.account.zone.analytics.read'"
The token cannot query zone HTTP analytics for that zone. Use a token with Zone โ Analytics โ Read for the zone, or use an authenticated cf OAuth token with analytics scopes without printing the token value.
API Reference