Skip to main content
selectCloudflarelast verified 2026-07-30
Servicesaccounts
CredentialsCLOUDFLARE_API_TOKEN
Contributed byJeffrey Aven, StackQL Studios
Fan-out: account - one call per account.

Cloudflare API token creation and scope management

Looks up Cloudflare permission groups by name so that a token can be built from them. Permission groups are identified by UUID in the API but only meaningful by name, so resolving names to ids is the first step in creating any least-privilege token - and doing it in SQL means the ids are never hardcoded.

Query

SELECT id, name, scopes
FROM cloudflare.accounts.permission_groups
WHERE account_id = '{{account_id}}'
AND name IN (
'Workers KV Storage Write',
'Account Settings Read',
'Workers Scripts Read',
'Pages Read',
'Account Analytics Read',
'Analytics Read',
'Zone WAF Write',
'Zone Read'
)
ORDER BY name;

Creating a token from those groups

The insert selects straight from the catalogue, aggregating the matched ids into the policy document with json_group_array - so the token is defined by the names you want, not by UUIDs copied from a console:

INSERT INTO cloudflare.accounts.tokens (
account_id,
name,
policies
)
SELECT
'{{account_id}}',
'my-token-name',
json_array(
json_object(
'effect', 'allow',
'permission_groups', json_group_array(json_object('id', id)),
'resources', json_object(
'com.cloudflare.api.account.{{account_id}}',
json_object('com.cloudflare.api.account.zone.*', '*')
)
)
)
FROM cloudflare.accounts.permission_groups
WHERE account_id = '{{account_id}}'
AND name IN (
'Workers KV Storage Write',
'Account Settings Read',
'Workers Scripts Read',
'Pages Read',
'Account Analytics Read',
'Analytics Read',
'Zone WAF Write',
'Zone Read'
)
RETURNING result;

Changing the scopes on an existing token

Build the new policy document with the full desired set of names - existing ones plus the additions - then replace the token definition. The token id and its secret are unchanged by this:

REPLACE cloudflare.accounts.tokens
SET name = 'my-token-name',
policies = '<json document from the query above>'
WHERE account_id = '{{account_id}}'
AND token_id = 'f0a1975b612da0eb8b17f4bef13e09dc'
RETURNING result;

Reading a token

There is no list method - a token is read by id:

SELECT id, name, status, issued_on, last_used_on
FROM cloudflare.accounts.tokens
WHERE account_id = '{{account_id}}'
AND token_id = 'f0a1975b612da0eb8b17f4bef13e09dc';

Notes

The secret is returned exactly once, as the value field inside the create response, and can never be read back afterwards - capture it at creation or rotate the token with EXEC cloudflare.accounts.tokens.roll_token, which issues a new secret while keeping the same token id. Never persist that value into a query, a log or a repository. The scope replacement is a full replace: policies omitted from the document are removed, which is why the rescope query lists every group the token should end up with rather than only the new one. resources maps a scope URN to the objects it covers - the account URN with com.cloudflare.api.account.zone.* grants across all zones in the account, so narrow it to specific zone URNs for a tighter token. Reading a token that does not exist returns HTTP 404 with code 1003 'token not found'. The permission_groups lookup was verified live; the create and replace statements are reproduced from a verified run rather than executed during authoring, since running them mints or alters real credentials.

Related queries

  • Cloudflare zones - Lists all Cloudflare zones visible to the API token with status and pause state; the zone id keys every per-zone API.
  • Cloudflare KV namespace and value lifecycle - Creates a KV namespace, writes and reads a value, lists keys and deletes both; the full control-plane to data-plane cycle in one surface.