Cloudflare API Management
Manage Cloudflare resources via the REST API. Covers DNS records, tunnels, Pages, and zones.
Authentication
Account API Token (for account-level resources like tunnels):
# Stored in ~/.zshrc
export LASERFOCUSED_CLOUDFLARE_TOKEN="cfat_..."
Token verification uses the account endpoint (not user endpoint):
curl -s "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/tokens/verify" \
-H "Authorization: Bearer ${LASERFOCUSED_CLOUDFLARE_TOKEN}"
Token prefixes:
cfat_— Account API token (tunnels, pages, account-level)cfut_— User API token (cross-account, user-level)
Account & Zone IDs
Account ID: e9adf716c7d13735e158a045298fe26f
Account Name: Justin@laserfocused.ee's Account
Zones:
hyperglot.io → c7ef50fc3fe392cd08cc61ba3a2c7bf9
franklin.ee → a8e4bf6b02771e2eca0135b1e4ef0540
frnkln.ai → c430681e325613d3b66a5c980edb39ef
API Base URL
All endpoints: https://api.cloudflare.com/client/v4/
Always include: -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json"
DNS Records
List records
curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${TOKEN}"
Filter: ?type=CNAME&name=api.example.com
Create record
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"type": "CNAME",
"name": "api",
"content": "target.example.com",
"proxied": true
}'
Update record
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{"content": "new-target.example.com"}'
Delete record
curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${TOKEN}"
Cloudflare Tunnels
Create tunnel
TUNNEL_SECRET=$(openssl rand -base64 32)
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\":\"my-tunnel\",\"tunnel_secret\":\"${TUNNEL_SECRET}\"}"
Response includes result.id (tunnel ID) and result.token (tunnel token for k8s).
Configure tunnel ingress
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"config": {
"ingress": [
{"hostname": "api.example.com", "service": "http://my-service:3001"},
{"service": "http_status:404"}
]
}
}'
The last ingress rule must be a catch-all (no hostname).
After creating tunnel
- Add CNAME:
api.example.com→${TUNNEL_ID}.cfargotunnel.com(proxied) - Create k8s secret with tunnel token
- Deploy cloudflared pod with
TUNNEL_TOKENenv var
List tunnels
curl -s "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel?is_deleted=false" \
-H "Authorization: Bearer ${TOKEN}"
Delete tunnel
curl -s -X DELETE "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}" \
-H "Authorization: Bearer ${TOKEN}"
Cloudflare Pages
Create Pages project
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"name": "my-project",
"production_branch": "main",
"build_config": {
"build_command": "npm run build",
"destination_dir": "dist",
"root_dir": "frontend"
}
}'
List Pages projects
curl -s "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects" \
-H "Authorization: Bearer ${TOKEN}"
Add custom domain to Pages
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT_NAME}/domains" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{"name": "app.example.com"}'
Zones
List zones
curl -s "https://api.cloudflare.com/client/v4/zones?per_page=50" \
-H "Authorization: Bearer ${TOKEN}"
Filter: ?name=hyperglot.io
Error Handling
All responses follow the same structure:
{"success": true/false, "errors": [...], "result": {...}}
Always check success field before using result. Common errors:
81053— DNS record already exists (use PATCH to update)1000— Invalid API token (wrong token type or expired)7003— Could not route to zone (token lacks zone permission)
Existing Tunnels
| Tunnel | ID | Routes To | |--------|-----|-----------| | hyperglot-hetzner | f2aed06f-bcfa-4523-a719-d2a174cf8248 | api.hyperglot.io → hyperglot-backend:3001 |
Additional Resources
For tunnel setup with k8s, see the k3s-deploy skill.