Cloudflare API

Cloudflare API via curl + jq for deployment status, DNS management, and cache operations. Raw API when netapi’s rich tables aren’t enough.

Token & Identity

Verify token — status, expiry, identity
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/user/tokens/verify" | jq '{
  status: .result.status,
  expires: (.result.expires_on // "never"),
  not_before: (.result.not_before // "immediate"),
  issued: .result.issued_on[:10],
  id: .result.id[:12]
}'
Health check — silent, exit-code only (for scripts/aliases)
curl -sf -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  | jq -e '.result.status == "active"' > /dev/null \
  && echo "CF token: OK" || echo "CF token: FAILED"
Policy audit — name, scopes, permissions (requires API Tokens:Read)
TOKEN_ID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/user/tokens/verify" | jq -r '.result.id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/user/tokens/${TOKEN_ID}" | jq '{
  name: .result.name,
  status: .result.status,
  expires: (.result.expires_on // "never"),
  policies: [.result.policies[] | {
    effect: .effect,
    resources: (.resources | keys),
    permissions: [.permission_groups[] | .name]
  }]
}'
Returns 9109 Unauthorized without API Tokens:Read scope. Add via: CF Dashboard → API Tokens → Edit → User > API Tokens > Read.
Scope probe — test which APIs the token can access (no extra permissions needed)
{
  verify=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
    "https://api.cloudflare.com/client/v4/user/tokens/verify")

  declare -A endpoints=(
    [tokens]="user/tokens/verify"
    [accounts]="accounts"
    [pages]="accounts/${CF_ACCOUNT_ID}/pages/projects"
    [zones]="zones"
    [dns]="zones?name=domusdigitalis.dev"
    [workers]="accounts/${CF_ACCOUNT_ID}/workers/scripts"
    [r2]="accounts/${CF_ACCOUNT_ID}/r2/buckets"
    [token-audit]="user/tokens"
  )

  scopes=$(for name in "${(k)endpoints[@]}"; do
    code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $CF_API_TOKEN" \
      "https://api.cloudflare.com/client/v4/${endpoints[$name]}")
    printf '"%s":%s,' "$name" "$code"
  done)

  echo "${verify}" | jq --argjson scopes "{${scopes%,}}" '{
    id: .result.id[:12],
    status: .result.status,
    expires: (.result.expires_on // "never"),
    not_before: (.result.not_before // "immediate"),
    scopes: ($scopes | to_entries | map({key, value: (if .value == 200 then "allowed" elif .value == 403 then "denied" else "error:\(.value)" end)}) | from_entries)
  }'
}
List accounts accessible by token
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts" | jq '[.result[] | {id, name}]'

Pages — Projects

List all Pages projects with domains
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects" \
  | jq '[.result[] | {name, subdomain, domains}]'
Get project build configuration
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs" \
  | jq '{name: .result.name, build_command: .result.build_config.build_command, output: .result.build_config.destination_dir, branch: .result.production_branch}'
Inspect environment variables (SECURITY: check for plain_text exposure)
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs" \
  | jq '{
  preview: [.result.deployment_configs.preview.env_vars | to_entries[] | {key, type: .value.type}],
  production: [.result.deployment_configs.production.env_vars | to_entries[] | {key, type: .value.type}]
}'
Env vars with "type": "plain_text" are returned in full by the API. Use Encrypt (secret_text) for credentials.

Pages — Deployments

One-shot status — is the latest deployment passing or failing right now?
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0] | "[\(.latest_stage.status | ascii_upcase)] \(.latest_stage.name) — \(.id[:12]) — \(.created_on[:19])"'
List last 5 deployments with status and commit
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq '[.result[:5][] | {id: .id[:12], status: .latest_stage.status, stage: .latest_stage.name, created: .created_on[:19], commit: .deployment_trigger.metadata.commit_message[:50]}]'
Stage breakdown — latest deployment (pulls full ID automatically)
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0].id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}" \
  | jq '{id: .result.id[:12], status: .result.latest_stage.status, stages: [.result.stages[] | {name, status, started_on: .started_on[:19], ended_on: .ended_on[:19]}]}'
Stage breakdown — Nth deployment (0=latest, 1=previous, etc.)
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[1].id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}" \
  | jq '{id: .result.id[:12], status: .result.latest_stage.status, stages: [.result.stages[] | {name, status, started_on: .started_on[:19], ended_on: .ended_on[:19]}]}'
Filter — failures only
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq '[.result[] | select(.latest_stage.status == "failure") | {id: .id[:12], stage: .latest_stage.name, created: .created_on[:19], commit: .deployment_trigger.metadata.commit_message[:60]}] | .[:5]'

Pages — Build Analysis

Compare build times across last 10 deployments
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq '[.result[:10][] | {
    id: .id[:12],
    status: .latest_stage.status,
    build_sec: (if (.stages[] | select(.name == "build") | .ended_on) and (.stages[] | select(.name == "build") | .started_on) then (((.stages[] | select(.name == "build") | .ended_on[:19] + "Z") | fromdateiso8601) - ((.stages[] | select(.name == "build") | .started_on[:19] + "Z") | fromdateiso8601)) else null end),
    commit: .deployment_trigger.metadata.commit_message[:60]
  }]'
Per-stage timing with duration (diagnose timeouts)
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq '[.result[:5][] | {id: .id[:12], status: .latest_stage.status, stages: [.stages[] | {name, status, duration: (if .ended_on and .started_on then (((.ended_on[:19] + "Z") | fromdateiso8601) - ((.started_on[:19] + "Z") | fromdateiso8601) | tostring + "s") else "n/a" end)}]}]'
Build duration of latest successful deploy
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '[.result[] | select(.latest_stage.status == "success")] | .[0] | .stages[] | select(.name == "build") | "\(.started_on[:19]) → \(.ended_on[:19])"'
Success/failure ratio — last 20 deployments
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments?per_page=20" \
  | jq '{
    total: (.result | length),
    success: ([.result[] | select(.latest_stage.status == "success")] | length),
    failure: ([.result[] | select(.latest_stage.status == "failure")] | length),
    latest: .result[0] | {status: .latest_stage.status, stage: .latest_stage.name, commit: .deployment_trigger.metadata.commit_message | split("\n")[0][:60]}
  }'
Observation: domus-scripture (~6,800 pages) adds ~15 min to Cloudflare build
Without scripture:  3 min 39s  → success
With scripture:    18 min 12s  → failure (Cloudflare 20-min timeout)
Local hub build:    2 min 15s  → Cloudflare is ~8x slower

Pages — Build Logs

Stream full build log for latest deployment
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0].id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}/history/logs" \
  | jq -r '.result.data[] | "\(.ts[:19]) \(.line)"'
Tail — last 10 lines of build log
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0].id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}/history/logs" \
  | jq -r '.result.data[-10:][] | "\(.ts[:19]) \(.line)"'
Deployment dashboard URL — open in browser
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0].id')
printf "https://dash.cloudflare.com/${CF_ACCOUNT_ID}/pages/view/domus-docs/%s\n" "${DID}"

Pages — Actions

Retry a failed deployment
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[0].id')
curl -sX POST -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}/retry" \
  | jq '{id: .result.id[:12], status: .result.latest_stage.status}'
Rollback to a specific deployment
DID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
  | jq -r '.result[2].id')
curl -sX POST -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments/${DID}/rollback" \
  | jq '{id: .result.id[:12], status: .result.latest_stage.status}'

Pages — Monitoring

Watch deployment — poll every 30s until complete
while true; do
  result=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
    "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/domus-docs/deployments" \
    | jq -r '.result[0] | "\(.latest_stage.name):\(.latest_stage.status) | \(.id[:12]) | \(.deployment_trigger.metadata.commit_message | split("\n")[0][:50])"')
  printf "%s  %s\n" "$(date +%H:%M:%S)" "${result}"
  [[ "${result}" == *":success"* || "${result}" == *":failure"* ]] && break
  sleep 30
done

Zones & DNS

List all zones
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones" \
  | jq '[.result[] | {name, id: .id[:12], status, plan: .plan.name}]'
Get zone ID for a domain
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=domusdigitalis.dev" | jq '.result[0].id'
List DNS records for a zone
ZONE_ID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=domusdigitalis.dev" | jq -r '.result[0].id')
curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  | jq '[.result[] | {type, name, content, proxied, ttl}]'

Cache

Purge entire cache for a zone
ZONE_ID=$(curl -sH "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=domusdigitalis.dev" | jq -r '.result[0].id')
curl -sX POST -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"purge_everything": true}' \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" | jq '.success'

netapi Equivalents

netapi wraps these API calls — use when the rich table output is enough
netapi cloudflare pages list
netapi cloudflare pages get domus-docs
netapi cloudflare pages deployments domus-docs
netapi cloudflare zones
netapi cloudflare zone domusdigitalis.dev
netapi cloudflare purge domusdigitalis.dev
netapi cloudflare dns list domusdigitalis.dev

When you need raw JSON for jq pipelines, scripting, or debugging failures — use curl directly against the API.

See Also

  • curl Patterns — request construction and auth

  • REST — HTTP methods and status codes