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
$VARin a command that pipes to another command, wrap the command containing$VARinbash -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,
cfmay only be on the mise login-shell PATH. Usezsh -lic 'cf ...'ifbashcannot 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_IDfrom 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 analyticsandcf schema --listmention 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 whoamishows OAuth auth with analytics scopes, GraphQL accepts that OAuth access token. If no suitable API token is exported, use a local script to loadaccess_tokenfrom~/.cf/config.tomland send the GraphQL request in memory. Nevercat,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_ltwith 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_ltwith YYYY-MM-DD) - Data retention: 90 days
- The
countfield in responses is the number of aggregated data points, not play events or unique viewers — onlysum.minutesViewedis 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 videoPlaybackEventsAdaptiveGroupsonly works with Cloudflare's own Stream Player embed. If using hls.js/vidstack/custom players, it returns empty. UsestreamMinutesViewedAdaptiveGroupsinstead.- The
countfield 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_geqanddatetime_ltare 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, notvideoPlaybackEventsAdaptiveGroups
"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
- GraphQL Analytics: https://developers.cloudflare.com/analytics/graphql-api/
- Stream Analytics: https://developers.cloudflare.com/stream/getting-analytics/
- Stream API: https://developers.cloudflare.com/stream/