# Cloudflare

URL: https://docs.hanzo.ai/docs/openapi/cloudflare

> Your Cloudflare account, managed from Hanzo: zones, Pages, Workers, Workers AI, R2, KV and D1.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/cloudflare) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Your Cloudflare account, managed from Hanzo: zones, Pages, Workers, Workers AI, R2, KV and D1.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 32                                     |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1113 · Cloudflare — The Per-Org Asset Plane** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1113-cloudflare.md)

`/v1/cloudflare` is an org's own Cloudflare account, managed from Hanzo: zones
and their analytics, Pages, Workers, Workers AI, R2, KV and D1, all driven
through the API token that org connected. It is implemented in `hanzoai/cloud`
`apps/cloudflare`. This HIP states the two separations that define it — how you
connected is the integrations plane, what you manage is this plane; and every
call rides the org's own token, so the platform never reaches Cloudflare with a
global credential.

### Motivation

Connecting a provider and managing its resources are different concerns with
different lifetimes: a connection is made once and custodied, resources are
driven daily. Braiding them puts credential custody inside every resource
handler. The split gives each one owner: `/v1/integrations/cloudflare/*`
connects (HIP-0126), `/v1/cloudflare/*` manages
(`apps/cloudflare/cloudflare.go:7-13`).

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted
as in RFC 2119.

#### §1 The token, and the one door to it

The capability owns no store. Its only state is the per-org API token, sealed
in KMS at `/orgs/{org}/integrations/cloudflare/api_token` and read in-process
through the one custody seam `integrations.TokenFor`
(`apps/cloudflare/cloudflare.go:24-27`) — the same coordinate both the apikey
and OAuth connect paths seal to, and that DNS reads. The token MUST ride only
the Authorization header of the outbound request to
`https://api.cloudflare.com/client/v4` (`apps/cloudflare/cloudflare.go:92`);
it is never logged, echoed in an error, or stored by this subsystem.

Fail-closed: an org that has not connected, an unmounted integrations plane, or
a KMS that is not Ready each answer 503 — never another org's data and never a
silent success (`apps/cloudflare/cloudflare.go:39-41`).

#### §2 Tenancy

Every handler resolves the org from the validated principal (`principal.Org`,
HIP-0026), never a body or query field, and that org is the only input to token
custody — so a request can only ever address its own org's Cloudflare account.
No validated principal means 403; a non-SuperAdmin bearer has its org pinned by
the identity boundary, so it cannot name another
(`apps/cloudflare/cloudflare.go:22-34`).

#### §3 The addresses

Everything is under `/v1/cloudflare`: `zones` (list, detail, analytics, purge),
`pages/projects` (with deployments and domains), `workers/scripts` (with
subdomain and zone routes), `r2/buckets`, `kv/namespaces` (with values),
`d1/databases` (with query), and `ai/run/{model}`. The resource operations are
typed; `ai/run` is the one relay — the model's body passes through, bounded by
`maxAIBody`, and the wire test (`apps/cloudflare/relay_wire_test.go`) holds it.

#### §4 Money

The capability is metered (`plugin/cloudflare/main.go:21`), and Workers AI is
the one operation that debits: an `/ai/run` is inference, so it meters through
the same usage spine as every model call, at the thin BYO fee — the org's own
token already paid Cloudflare for the compute. The gate runs before any
Cloudflare contact, on a floored estimate (`BYOInferenceFeeMicros`, so
`gateCents ≥ 1` even for a modality whose token estimate is 0), and the exact
debit lands after the call on the tokens the model reported
(`apps/cloudflare/ai.go:112-160`). The payer is `principal.Ledger`;
`MeterUsage` records under provider `ai`, service `workers-ai`, so this spend
sums with LLM spend on the same axis. Everything else is passthrough on the
org's own account and costs nothing here.

#### §5 Events, telemetry, stage, upstreams

It publishes no events on the bus. Beyond the request span, `/ai/run` emits one
`gen_ai` span on the same plane as every model call, with system `cloudflare`
and per-model attribution (`apps/cloudflare/ai.go:129-131`). Its stage is `ga`:
it is platform infrastructure — the sibling of `/v1/dns` and `/v1/domain` — not
a vertical application. It derives from no upstream; it speaks Cloudflare's
public REST API v4 directly as a wire fact, with no vendored SDK.

### Rationale

The alternative to per-org tokens is a platform-level Cloudflare credential
with tenancy enforced by our own bookkeeping. That is one secret whose
compromise is every org's infrastructure, and it makes the platform the
customer of record for assets that are the org's. Deriving the token path from
the validated org makes cross-org reach structurally impossible rather than
policed — the coordinate for another tenant's token is never constructed.

### Security Considerations

The wrong implementation here hands an attacker another org's Cloudflare
account: DNS, live sites, storage, and edge code — enough to serve malware from
a victim's domain. The org-to-token derivation in §2 is the whole defense, and
the fee gate in §4 is the second: without it, a relay to a paid inference API
is a free-compute primitive billed to nobody. Both fail closed, and a frozen or
over-cap org is refused before Cloudflare is ever contacted — no discovery, no
run (`apps/cloudflare/ai.go:113-120`).

## Four surfaces

| Surface  | Reaches this capability as                         | Coverage                                                                   |
| -------- | -------------------------------------------------- | -------------------------------------------------------------------------- |
| **REST** | `cloudflare` at its own prefix                     | 32 operations                                                              |
| **CLI**  | `hanzo cloudflare …`                               | 32 of 32                                                                   |
| **SDK**  | `CloudflareApi` in every published client          | 32 methods                                                                 |
| **MCP**  | tool `cloudflare` on `https://api.hanzo.ai/v1/mcp` | 29 operations, 0 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/cloudflare/zones`, operation `get_cloudflare_zones`:

**CLI**

```bash
hanzo cloudflare zones list
```

**SDK**

**.ts**

```ts
import { Configuration, CloudflareApi } from 'hanzoai';

const api = new CloudflareApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getCloudflareZones();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import CloudflareApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = CloudflareApi(client).get_cloudflare_zones()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.CloudflareAPI.GetCloudflareZones(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, cloudflare_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = cloudflare_api::get_cloudflare_zones(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.CloudflareApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new CloudflareApi(client).getCloudflareZones();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/cloudflare/zones \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **cloudflare** through the `cloudflare` tool, which names its 29 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_cloudflare_d1_databases"
         }
       }
     }'
```

Answers `200` — ok.

## Endpoints

| Endpoint                                                                                                                                                                         | What it does                                                                                                                            |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/cloudflare/d1/databases/{database}/query`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-d1-databases-by-database-query)                              | Runs one SQL statement against a D1 database.                                                                                           |
| [`DELETE /v1/cloudflare/d1/databases/{database}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-d1-databases-by-database)                                      | Deletes a D1 database and everything stored in it.                                                                                      |
| [`GET /v1/cloudflare/d1/databases`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-d1-databases)                                                                   | Lists the D1 databases on the org's Cloudflare account.                                                                                 |
| [`POST /v1/cloudflare/d1/databases`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-d1-databases)                                                                 | Creates a D1 database on the org's Cloudflare account.                                                                                  |
| [`GET /v1/cloudflare/kv/namespaces/{namespace}/values/{key}`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-kv-namespaces-by-namespace-values-by-key)             | Read a Workers KV value as its stored bytes                                                                                             |
| [`PUT /v1/cloudflare/kv/namespaces/{namespace}/values/{key}`](https://docs.hanzo.ai/docs/openapi/cloudflare/put-cloudflare-kv-namespaces-by-namespace-values-by-key)             | Write a Workers KV value from the request body                                                                                          |
| [`DELETE /v1/cloudflare/kv/namespaces/{namespace}/values/{key}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-kv-namespaces-by-namespace-values-by-key)       | Removes one key from a Workers KV namespace.                                                                                            |
| [`DELETE /v1/cloudflare/kv/namespaces/{namespace}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-kv-namespaces-by-namespace)                                  | Deletes a Workers KV namespace and every key in it.                                                                                     |
| [`GET /v1/cloudflare/kv/namespaces`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-kv-namespaces)                                                                 | Lists the Workers KV namespaces on the org's Cloudflare account.                                                                        |
| [`POST /v1/cloudflare/kv/namespaces`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-kv-namespaces)                                                               | Creates a Workers KV namespace on the org's Cloudflare account.                                                                         |
| [`POST /v1/cloudflare/pages/projects/{project}/deployments`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-pages-projects-by-project-deployments)                | Trigger a new Pages deployment for a project                                                                                            |
| [`DELETE /v1/cloudflare/pages/projects/{project}/domains/{domain}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-pages-projects-by-project-domains-by-domain) | Detaches a custom domain from a Cloudflare Pages project.                                                                               |
| [`POST /v1/cloudflare/pages/projects/{project}/domains`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-pages-projects-by-project-domains)                        | Attaches a custom domain to a Cloudflare Pages project.                                                                                 |
| [`GET /v1/cloudflare/pages/projects/{project}`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-pages-projects-by-project)                                          | Reads one Cloudflare Pages project — its build config, deployment configs and latest deployment.                                        |
| [`DELETE /v1/cloudflare/pages/projects/{project}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-pages-projects-by-project)                                    | Deletes a Cloudflare Pages project, and with it every deployment it has ever made.                                                      |
| [`GET /v1/cloudflare/pages/projects`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-pages-projects)                                                               | Lists the org's Cloudflare Pages projects.                                                                                              |
| [`POST /v1/cloudflare/pages/projects`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-pages-projects)                                                             | Creates a Cloudflare Pages project on the org's account.                                                                                |
| [`DELETE /v1/cloudflare/r2/buckets/{bucket}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-r2-buckets-by-bucket)                                              | Deletes an R2 bucket.                                                                                                                   |
| [`GET /v1/cloudflare/r2/buckets`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-r2-buckets)                                                                       | Lists the R2 buckets on the org's Cloudflare account.                                                                                   |
| [`POST /v1/cloudflare/r2/buckets`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-r2-buckets)                                                                     | Creates an R2 bucket on the org's Cloudflare account.                                                                                   |
| [`POST /v1/cloudflare/workers/scripts/{script}/subdomain`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-workers-scripts-by-script-subdomain)                    | Publishes or withdraws one Worker script on the account's workers.dev subdomain.                                                        |
| [`PUT /v1/cloudflare/workers/scripts/{script}`](https://docs.hanzo.ai/docs/openapi/cloudflare/put-cloudflare-workers-scripts-by-script)                                          | Uploads or replaces a module Worker script.                                                                                             |
| [`DELETE /v1/cloudflare/workers/scripts/{script}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-workers-scripts-by-script)                                    | Removes a Worker script from the org's Cloudflare account.                                                                              |
| [`GET /v1/cloudflare/workers/scripts`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-workers-scripts)                                                             | Lists the Worker scripts on the org's Cloudflare account.                                                                               |
| [`GET /v1/cloudflare/workers/subdomain`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-workers-subdomain)                                                         | Reads the org account's workers.dev subdomain — the name under which every subdomain-enabled script is served.                          |
| [`DELETE /v1/cloudflare/workers/zones/{zone}/routes/{route}`](https://docs.hanzo.ai/docs/openapi/cloudflare/delete-cloudflare-workers-zones-by-zone-routes-by-route)             | Unbinds a Worker route, so its pattern stops dispatching to a script.                                                                   |
| [`GET /v1/cloudflare/workers/zones/{zone}/routes`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-workers-zones-by-zone-routes)                                    | Lists the Worker routes bound within one zone — the URL patterns that dispatch to a script.                                             |
| [`POST /v1/cloudflare/workers/zones/{zone}/routes`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-workers-zones-by-zone-routes)                                  | Binds a URL pattern in a zone to a Worker script.                                                                                       |
| [`GET /v1/cloudflare/zones/{zone}/analytics`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-zones-by-zone-analytics)                                              | Reads a zone's Cloudflare traffic dashboard — requests, bandwidth, threats and pageviews over the since/until window.                   |
| [`POST /v1/cloudflare/zones/{zone}/purge`](https://docs.hanzo.ai/docs/openapi/cloudflare/post-cloudflare-zones-by-zone-purge)                                                    | Drops a zone's Cloudflare edge cache — either the whole zone (purge\_everything) or exactly the listed file URLs.                       |
| [`GET /v1/cloudflare/zones/{zone}`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-zones-by-zone)                                                                  | Reads one Cloudflare zone the org's token can see.                                                                                      |
| [`GET /v1/cloudflare/zones`](https://docs.hanzo.ai/docs/openapi/cloudflare/get-cloudflare-zones)                                                                                 | Lists the Cloudflare zones the org's connected API token can see, paged and filtered by the query parameters Cloudflare itself accepts. |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
