# Cloudflare API — master token, reused per-scope 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 **one narrow token per scope**, persists it to the secrets file, and reuses it), where the master is stored, and the curated endpoints worth knowing — including how to list and revoke tokens, and the standing reconcile pass that keeps the account from accumulating strays.

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

---

## Auth model — one master, reused per-scope 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 per-scope tokens. It is never passed to `wrangler`, never put on a command line that gets echoed, never printed.
- **Per-scope narrow token** — scoped to exactly one operation class (a single-zone DNS edit, a one-project Pages deploy, a D1 query), with a **deterministic name** (`<brand>-pages-d1`, `<brand>-dns`, `<brand>-access`, and the house-code data scopes `<brand>-storage` = D1+R2 and `<brand>-calendar-d1` = D1). There is **one** such token per scope. The agent loads it from the secrets file; **if absent, it mints it once** from the master — correctly scoped, **no expiry — permanent and reused** — and persists it back to the secrets file. Thereafter it is loaded and reused, never re-minted. Exported into the environment for the one `wrangler` / API call; never echoed into chat, never written into a project tree.

Why one-per-scope instead of one-per-operation: the operator provisions the master once; the agent provisions each narrow token once and reuses it, so the account never fills with throwaway tokens, no broad token is ever exported to a tool that could log it, and decay is handled structurally by the reconcile pass (§ Reconcile) rather than by per-token TTL. The narrow token persists alongside the master in the same `600`-mode secrets file but is **strictly narrower** than the master — so the binding "never export the master to `wrangler`" is preserved while the narrow token gains reuse.

---

## 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** — `set -a; . <file>; set +a` to load `CLOUDFLARE_API_TOKEN` (master) + `CLOUDFLARE_ACCOUNT_ID` into the environment. The reused per-scope narrow tokens (`<brand>-pages-d1`, etc.) persist **in this same file** as additional keys (e.g. `CF_PAGES_D1_TOKEN=`), minted once if absent and loaded thereafter; each is strictly narrower than the master that already lives here, so persisting it adds no exposure the master did not already carry. A per-scope token is exported only into the single command that uses it, never echoed.

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"
```

### Multi-tenant installs — the master lives house-only

The account-scoped storage above is correct for a single-tenant install (one Cloudflare account, one client). On a **multi-client install** (many client sub-accounts sharing one Cloudflare account, for example SiteDesk), an account-wide token in a sub-account's `cloudflare.env` lets that sub-account reach every other client's D1 and R2. There, the master lives house-only at `${PLATFORM_ROOT}/config/cloudflare-house.env` (a location no sub-account spawn sources), per-account `cloudflare.env` files carry no account-wide token, and a sub-account reaches D1/R2 only through the storage broker, which scopes every operation to the caller's account. See [`../../../../.docs/cloudflare-storage-isolation.md`](../../../../.docs/cloudflare-storage-isolation.md). The per-scope DNS/Pages/Access minting below stays admin-run, and its mint-or-reuse mechanism is unchanged; the one difference on a multi-tenant install is the source file. Because the per-account `cloudflare.env` no longer carries the master, `cf-token.sh` falls back to the house credential (`${PLATFORM_ROOT}/config/cloudflare-house.env`) for these scopes, mints and persists the scope token there, and reports `src=house` on its `[cf-token]` lifeline. `cf-token.sh` gates that fallback on the caller being the house account (its `account.json` carries `role:"house"`, or it is the sole account), so the sanctioned path never mints a hosting token for a client sub-account; a denied caller gets an `action=deny` lifeline naming the reason. This gate is a boundary of intent, not of permission: every account's session runs as the same unix user and the house env path is deterministic, so an agent holding `Bash` can read the master directly or export a false `ACCOUNT_ID`. Do not cite the gate as proof a sub-account cannot reach the master. This is permanent, not pending: OS user separation was costed and declined as disproportionate, so nothing tracks closing it.

Load the master for a mint call:

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

### Load-credentials pre-flight (assert before any `wrangler` / `curl`)

Sourcing the file is not proof the token loaded — a missing file, a typo in the path, or an empty value all leave `CLOUDFLARE_API_TOKEN` unset, and the **next** `wrangler` / `curl` then fails with `CLOUDFLARE_API_TOKEN not set` *after* you have already issued the command. Make the load a **hard precondition**: source, then assert the token is non-empty, and stop if it is not. This block precedes every `wrangler d1`, `wrangler pages`, and Cloudflare-API `curl` in this reference and the ones that compose it:

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
: "${CLOUDFLARE_API_TOKEN:?credentials not loaded — source the secrets file before any wrangler/curl}"
: "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded — source the secrets file before any wrangler/curl}"
```

The `:?` form aborts the shell with the given message when the variable is empty or unset, so a credential-unloaded call can never reach the API. The token value is never printed — only the abort message on failure.

---

## 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). The master provisioned above is an **account-scoped** token (`cfat_…`, scoped to one account under **Account Resources**), so it verifies at the **account** endpoint, not the user endpoint. Verifying a `cfat_…` token at `/user/tokens/verify` returns `Invalid API Token` even when the token is valid:

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
: "${CLOUDFLARE_API_TOKEN:?credentials not loaded — source the secrets file first}"
: "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded — source the secrets file first}"
curl -sS -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
  "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/tokens/verify" | jq '.result.status'
# expect: "active"
```

---

## Token type — route endpoints by prefix (`cfat_` vs `cfut_`)

The master an operator provisions is usually **account-scoped** (`cfat_…`, created under **Account Resources** as above): it verifies and mints at the `/accounts/{id}/…` endpoints. But some accounts supply a **user-scoped** token (`cfut_…`) instead, and for a `cfut_…` token the **account-level endpoints return error code `9109`** — the working endpoints are the user-level ones. Detect the type by the token's prefix and route accordingly; never assume account scope.

| Operation | `cfat_…` (account-scoped) | `cfut_…` (user-scoped) |
|---|---|---|
| Verify | `GET /accounts/${ACC}/tokens/verify` | `GET /user/tokens/verify` |
| Resolve permission-group ids | `GET /accounts/${ACC}/tokens/permission_groups` | `GET /user/tokens/permission_groups` |
| Mint a narrow token | `POST /accounts/${ACC}/tokens` | `POST /user/tokens` |

Cross-type calls fail with a stable, recognizable signal: a `cfat_…` token at `/user/tokens/verify` returns `Invalid API Token`; a `cfut_…` token at any `/accounts/{id}/…` endpoint returns `9109`. The diagnostic is the verify call's JSON `errors[].code`.

The prefix also governs the **resource shape** of a zone-scoped mint body, not just the endpoint. A `cfut_…` master accepts a flat zone resource — `"resources": { "com.cloudflare.api.account.zone.*": "*" }`. A `cfat_…` (account-owned) master **rejects** that flat shape with code `1001` ("Must specify a zone for account owned tokens, or nest zone under specific account resource"); it requires the zone resource nested under the account resource — `"resources": { "com.cloudflare.api.account.${ACC}": { "com.cloudflare.api.account.zone.*": "*" } }`. Non-zone scopes (Pages/D1/Access) already scope to `com.cloudflare.api.account.${ACC}` and need no branch.

**Creating** a token stays an account-level, operator-owned concern — each account documents its own master token and its type (e.g. a per-account `secrets/cloudflare.README.md`). Where an account *can* mint an account-scoped token that is the cleaner default; but a skill that merely *consumes* a supplied token must detect the prefix and adapt, never assume it can mint at `/accounts/…`.

---

## Provisioning and reusing a stable per-scope token

Per operation class there is **one** deterministically-named narrow token, reused across runs. On each run the agent **loads it from the secrets file; if it is absent, it mints it once** (correctly scoped, **no expiry**) and persists it back to the secrets file. There is no per-operation throwaway and no `unset` — the token is permanent and reused. Mint via `POST /accounts/{account_id}/tokens` (`cfat_…` master) or `POST /user/tokens` (`cfut_…` master — see § Token type — route endpoints by prefix).

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a   # loads the master + account id (+ any persisted per-scope tokens)
: "${CLOUDFLARE_API_TOKEN:?credentials not loaded}"; : "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded}"

# Example: the stable Pages-deploy token. One deterministic name + secrets key per scope.
TOKEN_KEY="CF_PAGES_D1_TOKEN"      # the persisted key in cloudflare.env
SCOPE_NAME="<brand>-pages-d1"      # the deterministic, reused token name on the account
PAGES_D1=$(grep -m1 "^${TOKEN_KEY}=" "${SECRETS_DIR}/cloudflare.env" 2>/dev/null | cut -d= -f2-)

if [ -z "${PAGES_D1}" ]; then       # absent → mint once, scoped, no expiry, then persist
  PAGES_D1=$(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": "${SCOPE_NAME}",
  "policies": [{
    "effect": "allow",
    "resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": "*" },
    "permission_groups": [{ "id": "<pages-edit-permission-group-id>", "name": "Pages Write" }]
  }]
}
JSON
  )
  ( umask 077; printf '%s=%s\n' "${TOKEN_KEY}" "${PAGES_D1}" >> "${SECRETS_DIR}/cloudflare.env" )
fi

# Reused thereafter — exported only into the one command's environment, never echoed:
CLOUDFLARE_API_TOKEN="${PAGES_D1}" wrangler pages deploy ./dist --project-name <project> --branch=main
```

No `expires_on`: the per-scope token is permanent and reused, so decay is the job of the **reconcile pass** (§ Reconcile), not a per-token TTL — the old short-TTL guidance was never honored in practice and produced only never-expiring strays. 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. Before reusing or before reconciling, `GET …/tokens` (§ Curated endpoint map) shows what already exists, so a scope is never minted twice under different names.

**Per-scope token resources.** Each reused per-scope token carries the resource that matches its operation class. The Pages-and-D1 token (`<brand>-pages-d1`) and the Access token (`<brand>-access`) are account-scoped: `"resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": "*" }`. The DNS token (`<brand>-dns`) is zone-scoped, and since it is a single stable token reused across every zone the install routes, its resource covers the account's zones — but the exact shape depends on the master's prefix (see § Token type — route endpoints by prefix): a `cfut_…` master takes the flat `"resources": { "com.cloudflare.api.account.zone.*": "*" }`, while a `cfat_…` master requires the zone nested under the account, `"resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": { "com.cloudflare.api.account.zone.*": "*" } }` (the flat shape returns `1001` on an account-owned master). A per-zone DNS token would defeat the one-stable-token-per-scope model (one token, reused, not one per zone); the minted token is still strictly narrower than the master, which already carries Zone DNS Edit over the zones in scope.

**House-code data scopes.** The house credential in `cloudflare-house.env` is a **minter**, not a data token, on **every** install, not only multi-tenant ones: the booking calendar and storage broker read it house-only fleet-wide (no per-account fallback), and mint their own account-scoped data tokens from that minter rather than use it directly (a minter returns `10000` on D1/R2). The multi-tenant install adds isolation on top of this same file (§ Multi-tenant installs); the single-tenant install simply reads its calendar/broker credential here. The scopes are — `<brand>-storage` (`D1 Write` + `Workers R2 Storage Write`, key `CF_STORAGE_TOKEN`) and `<brand>-calendar-d1` (`D1 Write`, key `CF_CALENDAR_D1_TOKEN`) — persisted house-side in `cloudflare-house.env` and reused. This is the same mint-or-reuse `cf-token.sh` machinery the hosting scopes use; the only difference is the consumer is trusted house code rather than the agent.

**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.

### A freshly-minted token is not instantly usable — verify with backoff

This applies only on the **mint-once path** (the absent branch above) — a reused token loaded from secrets is already active and needs no wait. A just-minted token returns `10000 Authentication error` for a few seconds before it becomes active. Do not fire a fresh token's first real call blind; **verify it first**, retrying on a transient `10000` with backoff and giving up after ~30s. Route `${VERIFY_URL}` by the master's prefix (account- vs user-scoped — see § Token type):

```bash
# Run this inside the mint-once (absent) branch, before persisting/using the token.
# ${PAGES_D1} = the per-scope token just minted; ${VERIFY_URL} = the prefix-correct verify endpoint.
for i in $(seq 1 10); do
  s=$(curl -sS -H "Authorization: Bearer ${PAGES_D1}" "${VERIFY_URL}" \
        | jq -r '.result.status // (.errors[0].code | tostring)')
  [ "$s" = "active" ] && break
  [ "$s" = "10000" ] && { sleep 3; continue; }   # transient post-mint window — expected
  echo "verify failed: ${s}" >&2; break          # any other code is a real failure
done
```

Proceed to the operation only once verify returns `active`. The `10000` window is transient and expected after a fresh mint; any other code is a real failure, not something to retry blindly.

---

## 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 **reused per-scope narrow** token.

### Token list / create / verify / revoke
The endpoints below are the **account-scoped** (`cfat_…`) family — the common case. For a **user-scoped** (`cfut_…`) master, swap to the `/user/tokens/…` family per § Token type — route endpoints by prefix; the account endpoints return `9109` for that token type.
- `GET  /accounts/${ACC}/tokens/verify` — confirm an **account-scoped** (`cfat_…`) token is active. This is the endpoint for the master and every per-scope narrow token here, all of which are account-scoped. **`/user/tokens/verify` is user-scoped only** and returns `Invalid API Token` for a `cfat_…` token even when it is valid — do not use it to verify an account token.
- `GET  /accounts/${ACC}/tokens` — list all tokens on the account (id, name, status, `expires_on`). Use it to find a per-scope token to reuse and to enumerate strays before a reconcile. User-scoped twin: `GET  /user/tokens`.
- `POST /accounts/${ACC}/tokens` — mint a per-scope token (see § Provisioning and reusing a stable per-scope token). Mint only when the list above shows the scope absent.
- `DELETE /accounts/${ACC}/tokens/{id}` — revoke a token by id (the reconcile/cleanup verb). User-scoped twin: `DELETE /user/tokens/{id}`. Route by the master's prefix exactly as verify/mint do — `cfat_…` → `/accounts/${ACC}/tokens…`, `cfut_…` → `/user/tokens…`; the cross-type call returns `9109`.
- `GET  /accounts/${ACC}/tokens/permission_groups` — resolve permission-group ids for scoping. **Caveat — some groups return duplicate ids.** A single named group (observed for **"Access: Apps and Policies Write"**) can appear **more than once** in this list under different ids. Minting with the wrong variant yields a token that returns `10000` on **every** call, **reads included** — a permanently poisoned token, not the transient post-mint `10000` window of § A freshly-minted token. **Verify a freshly-minted Access token with a read** (e.g. `GET /accounts/${ACC}/access/apps`) before relying on it: a `10000` on that read means the wrong permission-group id was used — re-resolve and mint with the other variant.

### 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.
- `PUT /accounts/${ACC}/access/apps/{app_id}` — update an existing Access application (e.g. to widen its destinations). The body is the **full app object**, not a partial patch — re-supply `name`, `destinations`/`domain`, `type`, `session_duration`, and the rest, not just the changed fields. **`PATCH` does not work under API-token auth**: it returns `10405 "Method not allowed for this authentication scheme"`. Use `PUT` with the complete body; there is no working partial-update verb for this auth scheme.

---

## Reconcile — the standing cleanup check

Token accumulation **emits no event and does not reproduce on demand**, so the decay mechanism is a standing reconcile pass, run **periodically and before a deploy** — not a one-off. It lists every token, keeps the known-good set, and revokes the rest. The known-good set is: the **master**, the stable per-scope operation tokens (`<brand>-pages-d1`, `<brand>-dns`, `<brand>-access`), and the **tunnel** tokens. Everything else — throwaway, duplicate-named, stray — is revoked.

```bash
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
: "${CLOUDFLARE_API_TOKEN:?credentials not loaded}"; : "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded}"
LIST_URL="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/tokens"  # /user/tokens for a cfut_ master

# Names kept verbatim — adjust to this account's stable set + tunnel token names.
KEEP="<brand>-pages-d1 <brand>-dns <brand>-access maxy-api-master maxy-pages Real Agent"

# 1. List; for each token whose name is NOT in KEEP, revoke it by id.
curl -sS -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" "${LIST_URL}?per_page=100" \
  | jq -r '.result[] | [.id, .name] | @tsv' \
  | while IFS=$'\t' read -r id name; do
      case " ${KEEP} " in *" ${name} "*) echo "keep  ${name}";;
        *) curl -sS -X DELETE -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" "${LIST_URL}/${id}" >/dev/null
           echo "revoke ${name}";; esac
    done

# 2. Post-condition — re-list and confirm the revoked names are gone. Do not trust the DELETE exit code.
curl -sS -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" "${LIST_URL}?per_page=100" | jq -r '.result[].name'
```

`?per_page=100` returns the first page only; an account that has accumulated **more than 100 tokens** — the exact failure this pass exists to undo — needs `&page=2`, `&page=3`, … until a short page returns. Reconcile is the accumulation-survival mechanism, so it must walk every page, not just the first.

The tally is **names only** — a token value is never printed (the redaction binding in § Provisioning applies). "Revoked N" is a claim only once the follow-up list (step 2) shows those names absent, never an inference from the DELETE call's exit code.

---

## When to use which path

- **Tunnel create/route** → `cloudflared` with the OAuth cert, per `manual-setup.md`. The API can manage tunnels too, but the cert path is the established one for create/route.
- **Tunnel run/resume** → `cloudflared tunnel run` authenticates with the per-tunnel credentials-file named in `config.yml`, not the cert, so the connector (and the `resume-tunnel.sh` supervisor) respawns a configured tunnel with no cert present. This is what lets a tunnel created via the API-token credentials-file path resume on reboot like an OAuth one.
- **Apex CNAME** → API (above) or dashboard (`dashboard-guide.md`). Either flattens correctly.
- **Pages deploy** → `wrangler` with the reused `<brand>-pages-d1` token, per `hosting-sites.md`.
- **D1 read/write** → `wrangler d1 execute --remote` with the reused D1-Edit token, per `d1-data-capture.md`.
- **Inspecting account state** (which zones, which tunnels) → API `GET` with a read-scoped token (reused), or the dashboard.
