Cloudflare API Token Permissions
When wrangler deploy (or wrangler d1 migrations apply) fails in CI with code: 10000, code: 7403, or code: 9106, the cause is almost always: the GitHub Secret CLOUDFLARE_API_TOKEN is missing a permission for a binding or operation that wrangler.jsonc references.
Adding a binding to wrangler.jsonc doesn't fail locally (your wrangler login session has full account access), but CI's narrow API token does. The error surfaces only when CI runs — typically after the PR is already merged.
When to use this skill
- A CI
wrangler deployjob that previously succeeded now fails withAuthentication error [code: 10000]/[code: 7403]/[code: 9106] - You're about to add a binding (
r2_buckets,kv_namespaces,queues,vectorize,hyperdrive,ai,browser) towrangler.jsoncand want to predict whether the token needs an extension - Token rotation: the secret value leaked or expired and needs replacement
- You used the
Edit Cloudflare Workerstemplate to create a token and need to verify what's actually included (it omits D1 / Queues / Vectorize — see below)
Error code → missing permission
| First error in deploy log | Cloudflare API path that failed | Missing permission |
|---|---|---|
Authentication error [code: 10000] |
/accounts/<id>/r2/buckets/<name> |
Account / Workers R2 Storage / Edit |
not authorized [code: 7403] |
/accounts/<id>/d1/database/<id>/query |
Account / D1 / Edit |
Authentication failed [code: 9106] |
/memberships |
Not a missing permission — see the 9106 note below this table |
Authentication error [code: 10000] |
/accounts/<id>/workers/scripts/<name> |
Account / Workers Scripts / Edit |
Authentication error [code: 10000] |
/accounts/<id>/storage/kv/namespaces/<id> |
Account / Workers KV Storage / Edit |
Authentication error [code: 10000] |
/accounts/<id>/queues/<id> |
Account / Queues / Edit |
The first error in the log is the actionable one; subsequent errors are usually consequences of the first failed auth call. Fix the first one and the rest disappear.
The 9106-on-/memberships special case (verified 2026-08-08): 9106 is not a permission-missing code. It means a user-level endpoint was called without a user identity — either the token is an Account owned token (which structurally cannot call /memberships; wrangler ≥ 4.90 auto-falls back to /accounts), or the credential env var itself is broken. A user token that merely lacks the scope fails with code: 10000 instead, and that one is fixed by User / Memberships / Read (User scope — Account Settings / Read covers /accounts, not /memberships). The durable fix either way: set account_id in wrangler.jsonc (or CLOUDFLARE_ACCOUNT_ID in CI) so wrangler skips the account-resolution endpoints entirely.
Permission matrix (binding / command → required permission)
| Binding or wrangler command | Required token permission |
|---|---|
wrangler deploy core (Worker upload) |
Account / Workers Scripts / Edit |
wrangler d1 migrations apply --remote |
Account / D1 / Edit |
wrangler d1 export --remote (backup) |
Account / D1 / Edit (Read may suffice; Edit is the safe pick) |
r2_buckets binding (existence check on deploy) |
Account / Workers R2 Storage / Edit |
kv_namespaces binding |
Account / Workers KV Storage / Edit |
queues.producers / queues.consumers binding |
Account / Queues / Edit |
vectorize binding |
Account / Vectorize / Edit |
hyperdrive binding |
Account / Hyperdrive / Edit |
ai (Workers AI) binding |
Account / Workers AI / Read |
browser (Browser Rendering) binding |
Account / Browser Rendering / Edit |
Memberships lookup (only fires when account_id is not set) |
User / Memberships / Read (User scope) — or set account_id so the call never happens |
For the typical small-app stack (SPA + API + D1 + R2 served from one Worker), the minimum token permission set is:
Account / Workers Scripts / EditAccount / D1 / EditAccount / Workers R2 Storage / Edit- Account Resources:
<your specific account>(don't grant All accounts unless you have a reason)
…plus account_id set in wrangler.jsonc (or CLOUDFLARE_ACCOUNT_ID in CI). With it, wrangler never calls the account-resolution endpoints (/accounts, /memberships) that would otherwise drag in Account Settings / Read / User / Memberships / Read.
The "Edit Cloudflare Workers" template trap
Cloudflare offers a one-click template named Edit Cloudflare Workers. It looks complete but silently omits D1, Queues, and Vectorize — products added after the template was originally set. (The template does get occasional refreshes — R2 is included nowadays — so re-verify against the template reference when in doubt.)
What the template includes (verified 2026-08-07):
Workers Scripts / EditWorkers KV Storage / EditWorkers R2 Storage / EditWorkers Tail / ReadWorkers Routes / EditAccount Settings / ReadUser Details / Read(User scope)Memberships / Read(User scope)
What you must add manually if your wrangler.jsonc uses them:
D1 / EditQueues / EditVectorize / EditHyperdrive / Edit
Recommendation: skip the template. Use Create Custom Token and pick exactly what you need from the matrix above. Faster than auditing the template's gaps.
In-place permission edit (keeps the token value)
A common misconception: editing a Cloudflare API Token's permissions regenerates its value, requiring a GitHub Secret update.
It doesn't. Cloudflare keeps the same token value when you edit permissions; only rotating (deleting + recreating) generates a new value.
Workflow:
- https://dash.cloudflare.com/profile/api-tokens
- Locate the token used by GitHub Actions (sort by Last Used — CI tokens are touched on every
mainpush) - Click Edit (not "Roll")
- Add the missing permission row → Continue to summary → Update Token
- Token value unchanged → no GitHub Secret update needed
- Re-run the failed CI:
gh run rerun <run-id> --failed
Identifying which token is the CI token
If you have multiple tokens in your account and aren't sure which value lives in CLOUDFLARE_API_TOKEN:
- The GitHub Secret value is one-way encrypted — not retrievable
gh secret list -R <owner>/<repo>only shows theupdatedAttimestamp; correlate with the Cloudflare token's Created / Last Used timestamps- Cloudflare dashboard sorts tokens by Last Used. The most-recently-used should be the CI token (Actions runs on every
mainpush) - If you can't tell, rotate: create a fresh token with the full matrix,
gh secret set CLOUDFLARE_API_TOKEN, delete the old one. Avoids a long bisect.
Diagnostic flow when CI deploy fails
gh run view <run-id> --log-failed→ find the first[ERROR] code: <number>line- Look up the code in the Error code → missing permission table above
- Edit the token's permissions in-place (value stays the same)
gh run rerun <run-id> --failed- If a new
code: <different number>appears, repeat from step 2 (cumulative permissions across the deploy stages — e.g. R2 binding check fails first → fix R2 → D1 migrations apply fails next → fix D1) - If the same code persists, check that the token you edited is actually the one in
CLOUDFLARE_API_TOKEN(see Identifying which token is the CI token above) — most common cause of "I added the permission but it still fails"
When you do need to rotate (regenerate)
- The secret value leaked publicly
- Cloudflare flagged the token as compromised
- The token expired (if you set a TTL when creating)
- You can't tell which token the CI uses (rotate to break the ambiguity)
In that case:
- Create a new token with the full permission matrix above (Custom Token, not the template)
- Copy the new value (shown once)
- Update the GitHub Secret:
gh secret set CLOUDFLARE_API_TOKEN -R <owner>/<repo>and paste the value - Delete the old token in the Cloudflare dashboard (avoids accumulation of stale tokens)
- Re-run the failed CI
What this skill does NOT cover
- Local
wrangler loginsession permissions —wrangler loginuses OAuth and grants full account access; it never has the narrow-permission problem. Only CI tokens hit this. - R2 bucket-scoped object access tokens (S3-compatible API): those are a different token type (created under R2 → Manage R2 API Tokens) and unrelated to CI deploy. Workers R2 Storage binding uses the account API token, not the bucket-scoped one.
- Deploying via
wrangler deploy --account-id <id>instead of via secret: same permission requirements, just authenticated differently.
Related skills
cloudflare-workers-deploy-skeleton— initial project setup. The token created during that skill should already follow this matrix from day 1; consult this skill when adding new bindings later.cloudflare-d1-drizzle-migration— D1 migration pitfalls (PRAGMA foreign_keys=OFFignored). NeedsD1 / Edit.cloudflare-d1-weekly-backup-via-pr— weekly D1 backup workflow. Reuses the sameCLOUDFLARE_API_TOKEN(no new token needed if the matrix is met).cloudflare-workers-builds-keyless-deploy— the alternative that removes this whole problem class: deploy via Workers Builds with no Cloudflare token in GitHub at all. Its default build token has the same flavor of trap (D1 Edit missing).