# Cloudflare API — master token, minted narrow tokens, endpoint map

The Cloudflare REST API is a permitted surface on this install. It is how the agent does DNS edits, Pages deploys, D1 queries, Access policies, and apex-CNAME creation without driving the dashboard. This reference is the library: where the canonical docs live, how auth works (one operator-provisioned master token; the agent mints short-lived narrow tokens from it), where the master is stored, and the curated endpoints worth knowing.

Canonical reference (always the source of truth for request shapes, never mirror it wholesale here): **https://developers.cloudflare.com/api/**

---

## Auth model — one master, many minted narrow tokens

There are two distinct tokens in play. Keep them separate.

- **Master token** — fully-scoped, long-lived, provisioned **once** by the operator in the dashboard (see § Provisioning the master token, below). Broad enough to manage Pages, D1, DNS, **and** create API tokens. The agent reads it only to mint narrow tokens. It is never passed to `wrangler`, never put on a command line that gets echoed, never printed.
- **Minted narrow token** — scoped to exactly one operation class (a single-zone DNS edit, a one-project Pages deploy, a D1 query), short-lived, created by the agent via the API call below. Exported into the environment for the one `wrangler` / API call, then discarded. Never written to disk, never committed, never echoed into chat.

Why mint instead of hand-rolling: the operator provisions the master once; every per-operation token is minted by the agent on demand, so the operator never hand-crafts scoped tokens and no broad token is ever exported to a tool that could log it.

---

## Master token storage — account-scoped secrets file

Store the master at an account-scoped secrets file, **not** in any deployable project tree:

```
~/<brand>-code/data/accounts/<accountId>/secrets/cloudflare.env      # mode 600, umask 077
```

(worked example: `~/realagent-code/data/accounts/<accountId>/secrets/cloudflare.env`)

It holds exactly two values:

```
CLOUDFLARE_API_TOKEN=<master>
CLOUDFLARE_ACCOUNT_ID=<accountId>
```

Rationale (binding):

1. **Account-isolated** — under `data/accounts/<accountId>/`, consistent with brand isolation; one account's master never sits where another account's flow can read it.
2. **Outside every deployable/git project tree** — so the god-token can never be committed or shipped in a Pages upload. This is the failure mode a project-root `.env` invites: a `.env` next to a site's source gets swept into the `wrangler pages deploy` upload or a `git add .`.
3. **Sourced on demand for the master only** — `set -a; . <file>; set +a` to load `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` into the environment for the mint call; per-operation narrow tokens go into the ephemeral environment of their single command, never into this file.

Create it once (mode 600, parent dirs 700):

```bash
ACCOUNT_ID="<accountId>"
SECRETS_DIR="${HOME}/<brand>-code/data/accounts/${ACCOUNT_ID}/secrets"
( umask 077; mkdir -p "${SECRETS_DIR}" )
# The operator pastes the master token; the agent never echoes it back.
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "<master>" "${ACCOUNT_ID}" > "${SECRETS_DIR}/cloudflare.env" )
chmod 600 "${SECRETS_DIR}/cloudflare.env"
```

Load the master for a mint call:

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
```

---

## Provisioning the master token (ADVANCED — operator-guided, never automated)

This is the one manual, operator-driven step. The agent relays the click-path; it does **not** browser-automate it (no Playwright, no Chrome DevTools, no consent-page driver).

1. Open **https://dash.cloudflare.com/profile/api-tokens** in your browser.
2. Click **Create Token** → **Create Custom Token** → **Get started**.
3. Name it (e.g. `<brand> master`). Under **Permissions**, add these rows (Account-level unless noted):
   - **Account · Cloudflare Pages · Edit**
   - **Account · D1 · Edit**
   - **Account · API Tokens · Write**  ← this is what lets the agent mint narrow tokens
   - **Zone · DNS · Edit** (Zone Resources → the zones you route)
   - **Account · Cloudflare Tunnel · Edit** (only if you want the API to manage tunnels alongside `cloudflared`)
4. **Account Resources** → Include → your account. **Zone Resources** → Include → the zones in scope.
5. Click **Continue to summary**, then **Create Token**.
6. Copy the token **once** — Cloudflare shows it a single time. Paste it into the secrets file (above). The agent stores it without echoing it back.

Verify the master works (token value never printed — only the verification result):

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
curl -sS -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
  "https://api.cloudflare.com/client/v4/user/tokens/verify" | jq '.result.status'
# expect: "active"
```

---

## Minting a narrow token

The agent mints a per-operation token from the master via `POST /accounts/{account_id}/tokens`. Scope it to the single operation, give it a short TTL, export it for the one call, discard it.

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a   # loads the master + account id

# Example: a one-project Pages-deploy token. Adjust the policy for the operation.
MINTED=$(curl -sS -X POST \
  -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/tokens" \
  --data @- <<'JSON' | jq -r '.result.value'
{
  "name": "ephemeral-pages-deploy",
  "policies": [{
    "effect": "allow",
    "resources": { "com.cloudflare.api.account.<accountId>": "*" },
    "permission_groups": [{ "id": "<pages-edit-permission-group-id>", "name": "Pages Write" }]
  }],
  "expires_on": "<iso8601-a-few-minutes-out>"
}
JSON
)

# Use it for exactly one command, in this command's environment only:
CLOUDFLARE_API_TOKEN="${MINTED}" wrangler pages deploy ./dist --project-name <project> --branch=main

unset MINTED   # discard; never written to disk, never echoed
```

Look up `<pages-edit-permission-group-id>` (and the ids for DNS Edit, D1 Edit, etc.) once via `GET /accounts/{account_id}/tokens/permission_groups`; they are stable per account.

**Redaction is binding.** Every example above pipes the token into a variable or an `Authorization` header and never to stdout. The agent surfaces the operation as verb + target — "minting a Pages-deploy token", "creating CNAME `chat.example.com`" — never the secret. A failure surfaces the API error body with any `Authorization` value masked.

---

## Curated endpoint map

Request/response shapes live at the canonical docs URL; this is the index of what to reach for. `${ACC}` = `CLOUDFLARE_ACCOUNT_ID`, `${ZONE}` = the zone id, `${TOKEN}` = a **minted narrow** token.

### Token create / verify
- `GET  /user/tokens/verify` — confirm a token is active.
- `POST /accounts/${ACC}/tokens` — mint a narrow token (see above).
- `GET  /accounts/${ACC}/tokens/permission_groups` — resolve permission-group ids for scoping.

### DNS records (zone-scoped; needs **Zone · DNS · Edit**)
- `GET  /zones/${ZONE}/dns_records` — list / find a record id.
- `POST /zones/${ZONE}/dns_records` — create. For an **apex CNAME**, set `"type":"CNAME"`, `"name":"@"` (or the bare zone name), `"content":"<UUID>.cfargotunnel.com"`, `"proxied":true` — Cloudflare flattens it automatically. This is the API equivalent of the dashboard apex edit in `dashboard-guide.md`.
- `PUT/PATCH /zones/${ZONE}/dns_records/{id}` — update. `DELETE …/{id}` — remove a stray record.

```bash
curl -sS -X POST -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/zones/${ZONE}/dns_records" \
  --data '{"type":"CNAME","name":"@","content":"<UUID>.cfargotunnel.com","proxied":true}' \
  | jq '{ok:.success, name:.result.name}'
```

### Zones
- `GET /zones?account.id=${ACC}` — list zones on the account; find a zone id by name (`?name=example.com`).

### Tunnels (Account · Cloudflare Tunnel)
- `GET  /accounts/${ACC}/cfd_tunnel` — list tunnels.
- `POST /accounts/${ACC}/cfd_tunnel` — create. `DELETE …/{tunnel_id}` — delete.
- The OAuth/`cloudflared` path in `manual-setup.md` remains the primary tunnel flow; use the API only when an end-to-end programmatic run is wanted.

### Access (Zero Trust) applications + policies
- `POST /accounts/${ACC}/access/apps` and `POST /accounts/${ACC}/access/apps/{app_id}/policies` — author an Access application + policy programmatically. The dashboard click-path in `dashboard-guide.md` § "Author an Access policy" is the alternative for operators who prefer to click.

---

## When to use which path

- **Tunnel create/route/run** → `cloudflared` (OAuth cert), per `manual-setup.md`. The API can manage tunnels too, but the cert path is the established one.
- **Apex CNAME** → API (above) or dashboard (`dashboard-guide.md`). Either flattens correctly.
- **Pages deploy** → `wrangler` with a minted Pages-Edit token, per `hosting-sites.md`.
- **D1 read/write** → `wrangler d1 execute --remote` with a minted D1-Edit token, per `d1-data-capture.md`.
- **Inspecting account state** (which zones, which tunnels) → API `GET` with a read-scoped minted token, or the dashboard.
