# Porkbun API v3 — Cloudflare

> Move a customer's domains into **their own Cloudflare account**: we create the zone, copy across the DNS records we hold, and repoint the registry nameservers at Cloudflare.

Two stages, and the first one needs a human:

1. **Connect the Cloudflare account (browser, one time).** Cloudflare's consent screen cannot be completed over the API. Send the account owner to `https://porkbun.com/account/connectCloudflare`, then poll `GET /cloudflare/getConnection` until `connected` is `true`.
2. **Move domains (API).** `GET /cloudflare/inventory` to see what's eligible, `POST /cloudflare/connect` to queue, then poll `/cloudflare/getQueue`. `POST /cloudflare/rollback/{domain}` undoes a move; `POST /cloudflare/retry/{domain}` re-queues a failure.

Queueing is asynchronous and per-domain: a call returns *queued*, never *connected*, and `skipped` (DNSSEC live, custom nameservers, already connected) is a normal outcome carrying a reason, not a failure.

Part of the Porkbun API v3.48. Topic index: https://porkbun.com/llms · Full reference: https://porkbun.com/llms-full.txt · Overview: https://porkbun.com/llms.txt · OpenAPI spec: https://porkbun.com/api/json/v3/spec

**Auth:** send `X-API-Key` / `X-Secret-API-Key` headers (preferred) or `apikey` / `secretapikey` in the JSON body. Create keys at https://porkbun.com/account/api

---

# Endpoints

## GET /api/json/v3/cloudflare/getConnection

**Check the Cloudflare account connection (poll target)**

Whether this account has an active Cloudflare grant, and which Cloudflare account it points at.

**This is the poll target for the connect flow.** Minting the grant is a human action: Cloudflare's consent screen has to be completed in a browser, and the authorization is bound to the Porkbun web session that started it, so it cannot be driven over the API. When `connected` is `false` the response carries a `connectUrl` — send the account owner there, then poll this endpoint until `connected` is `true`.

Also available via POST.

## GET /api/json/v3/cloudflare/inventory

**List every domain with its Cloudflare eligibility**

Every domain in the account with a `state` (`eligible`, `warn`, `blocked`, `connected`, `inprogress`) and a human-readable `reason`.

Read this **before** queueing to see what will be skipped and why. Works even with no Cloudflare connection yet, so an agent can plan while the owner is still authorizing. Also available via POST.

## POST /api/json/v3/cloudflare/connect

**Queue domains to move to the customer's Cloudflare account**

Queue one or many domains. For each one we create the zone in the customer's own Cloudflare account, copy across the DNS records we hold, and repoint the registry nameservers at Cloudflare.

**Asynchronous.** Work runs on a background job over the next few minutes, so a successful call means *queued*, never *connected* — poll `/cloudflare/getQueue` or `/cloudflare/get/{domain}`.

**`skipped` is a normal outcome, not an error.** DNSSEC live, custom nameservers, already connected, already in progress: each domain comes back under `queued`, `skipped` or `alreadyQueued` with its own reason. Read the reasons rather than treating a non-empty `skipped` as failure.

Eligibility, ownership and nameserver state are re-checked immediately before each domain is acted on, so a domain accepted here can still be skipped later.

Re-submitting a domain is safe: the queue row is the unit of truth and is updated in place.

Supports `dryRun: true`, which returns the same per-domain verdicts without queueing anything.

Requires an active Cloudflare connection (`CLOUDFLARE_NOT_CONNECTED` otherwise). Limits: 500 domains per call, 2000 domains per account per hour.

After queueing, poll `/cloudflare/get/{domain}`: the row moves `queued` → `working` → `setup` → `activating` → `connected`. `setup` means Cloudflare has the zone but has not provisioned it yet, so the nameservers have deliberately not been touched. `activating` means the nameservers are already repointed and Cloudflare is confirming the zone, which can take a while as DNS propagates.

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | yes | Domain names to move. A comma-separated string is also accepted. |
| `dryRun` | boolean | no | Validate and return per-domain verdicts without queueing. |

## GET /api/json/v3/cloudflare/getQueue

**List every Cloudflare move for the account**

Every Cloudflare move this account has requested, with status and message. Queue rows are never deleted, so this doubles as the audit trail. Also available via POST.

**Status values** (poll until one of the terminal ones):

| status | meaning | terminal |
|--------|---------|----------|
| `queued` | accepted, waiting for the worker | no |
| `working` | a run is touching this row right now | no |
| `setup` | the zone exists in the customer's Cloudflare account but Cloudflare has not provisioned it yet (`initializing`). **The nameservers have not been touched** — the domain still resolves from Porkbun. Cloudflare can sit here for hours when an account has a backlog of zones it never activated | no |
| `activating` | nameservers repointed; waiting for Cloudflare to mark the zone active. Legitimately slow (registry + resolver propagation) — allow up to 72h, and a real move has been observed taking 59h | no |
| `connected` / `done` | the move finished | **yes** |
| `skipped` | not moved, and `message` says why (DNSSEC live, custom nameservers, no longer in the account) | **yes** |
| `failed` / `error` | the move did not complete; `message` says why. Re-queue with `/cloudflare/retry/{domain}` | **yes**, with one exception: a row that failed waiting on Cloudflare is re-checked for up to 30 days, so it can still close out as `connected` (or have its `message` updated to say it is now retryable) if Cloudflare activates the zone late |
| `undone` | the nameservers were put back on Porkbun — either the customer undid the move, or the domain stopped resolving while Cloudflare had not activated it yet and we restored it without waiting for the deadline | **yes** |

Poll on a sensible interval (a few seconds early on, then back off) — a zone typically leaves `queued` within seconds but can sit in `setup` or `activating` while Cloudflare provisions the zone and DNS propagates.

## GET /api/json/v3/cloudflare/get/{domain}

**Get the Cloudflare move status for one domain**

Status of a single domain's move, including the zone id once created and the nameservers we replaced (kept so the move can be undone). `NOT_QUEUED` if the domain has never been queued. Also available via POST.

**Status values** (poll until one of the terminal ones):

| status | meaning | terminal |
|--------|---------|----------|
| `queued` | accepted, waiting for the worker | no |
| `working` | a run is touching this row right now | no |
| `setup` | the zone exists in the customer's Cloudflare account but Cloudflare has not provisioned it yet (`initializing`). **The nameservers have not been touched** — the domain still resolves from Porkbun. Cloudflare can sit here for hours when an account has a backlog of zones it never activated | no |
| `activating` | nameservers repointed; waiting for Cloudflare to mark the zone active. Legitimately slow (registry + resolver propagation) — allow up to 72h, and a real move has been observed taking 59h | no |
| `connected` / `done` | the move finished | **yes** |
| `skipped` | not moved, and `message` says why (DNSSEC live, custom nameservers, no longer in the account) | **yes** |
| `failed` / `error` | the move did not complete; `message` says why. Re-queue with `/cloudflare/retry/{domain}` | **yes**, with one exception: a row that failed waiting on Cloudflare is re-checked for up to 30 days, so it can still close out as `connected` (or have its `message` updated to say it is now retryable) if Cloudflare activates the zone late |
| `undone` | the nameservers were put back on Porkbun — either the customer undid the move, or the domain stopped resolving while Cloudflare had not activated it yet and we restored it without waiting for the deadline | **yes** |

Poll on a sensible interval (a few seconds early on, then back off) — a zone typically leaves `queued` within seconds but can sit in `setup` or `activating` while Cloudflare provisions the zone and DNS propagates.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## POST /api/json/v3/cloudflare/retry/{domain}

**Retry a failed or skipped domain**

Put a domain that failed or was skipped back in the queue. Fails with `RETRY_FAILED` if it is already connected, already in progress, or no longer in the account. Supports `dryRun`.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## POST /api/json/v3/cloudflare/rollback/{domain}

**Undo a completed move (restore Porkbun nameservers)**

Point the domain's nameservers back at Porkbun, restoring the DNS we still hold.

The Cloudflare zone is deliberately left in place — deleting a zone in someone's own Cloudflare account is theirs to do. Fails with `ROLLBACK_FAILED` if we never moved the domain, it is already back on Porkbun nameservers, or it is being worked on right now. Supports `dryRun`.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## POST /api/json/v3/cloudflare/disconnect

**Remove the stored Cloudflare connection**

Revoke and forget this account's Cloudflare grant. Domains already moved stay on Cloudflare and keep resolving; this only stops us making further changes on the customer's behalf. Reconnecting requires the browser authorization again. Supports `dryRun`.

## POST /api/json/v3/cloudflare/setProxy/{domain}

**Turn the Cloudflare proxy (orange cloud) on or off**

Set `proxied` on the domain's Cloudflare DNS records.

**The move itself always imports records DNS-only (grey cloud), on purpose** — changing how traffic is served at the same time as changing who serves DNS gives you two variables to debug at once. Proxying is therefore a separate, explicit step, best done after you've confirmed the site still works.

Defaults to every proxiable record; pass `records` to target specific names (`"@"` means the apex, a bare label like `"www"` is expanded). Only A, AAAA and CNAME can be proxied — anything else is reported under `skipped` with a reason rather than failing the call. Records already in the requested state are skipped too.

Proxying hides the origin IP, so if the zone's MX points at a hostname you are proxying, mail to it breaks; that case comes back in `warnings`. Supports `dryRun`.

Rate limit: 60 changes per account per hour (these calls go to Cloudflare under Porkbun's OAuth client).

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | yes | true = proxy through Cloudflare (orange cloud); false = DNS-only (grey cloud). |
| `records` | string[] | no | Optional. Limit to these names; "@" = apex, bare labels are expanded. |
| `dryRun` | boolean | no |  |

## GET /api/json/v3/cloudflare/getRecords/{domain}

**List the domain's live DNS records at Cloudflare**

The domain's DNS records **as Cloudflare currently holds them**, each with its `proxied` flag and whether it is `proxiable` at all.

Once a domain has moved, this is the authoritative record set — `/dns/retrieve` reads the Porkbun zone, which is no longer the one answering queries. Requires the move to have finished (`ZONE_NOT_READY` otherwise). Also available via POST.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## GET /api/json/v3/cloudflare/preview/{domain}

**Preview exactly which records a move would copy**

Which DNS records we would create in Cloudflare for this domain, and which we would drop, **without queueing anything**. The honest answer to "what will this do to my DNS" before committing.

Records are always created DNS-only (grey cloud); use `/cloudflare/setProxy` afterwards to turn the proxy on. Also available via POST.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## GET /api/json/v3/cloudflare/getZone/{domain}

**Get live zone state from Cloudflare (and detect nameserver drift)**

What **Cloudflare** says about the zone right now — status, paused, its nameservers, activation date — as opposed to what our queue row remembers.

These drift: if the nameservers are repointed elsewhere after the move, our row still reads `done` while Cloudflare has stopped answering for the domain. The response includes the live public nameservers and a `nameserversDrifted` boolean so you don't have to diff them. Also available via POST.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## GET /api/json/v3/cloudflare/getZoneSettings/{domain}

**Read the zone settings that matter after a move**

The Cloudflare zone settings worth caring about post-migration: `ssl`, `always_use_https`, `automatic_https_rewrites`, `min_tls_version`, `development_mode`, `cache_level`.

The important one is **`ssl`**: `flexible` means Cloudflare fetches your origin over plain HTTP while visitors see a padlock, so the response warns when it is `off` or `flexible`. Also available via POST.

If the account's Cloudflare authorization predates this feature it will not carry the `zone-settings.write` scope, and this returns `CLOUDFLARE_REAUTHORIZE_REQUIRED` with a `connectUrl`. Reconnecting is the same one-click browser flow and does not disturb domains already moved.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

## POST /api/json/v3/cloudflare/setZoneSettings/{domain}

**Change zone settings (allowlisted)**

Set one or more of the allowlisted zone settings. This is an allowlist rather than a passthrough — Cloudflare exposes hundreds of settings and WAF/firewall/security controls are deliberately out of scope for this API.

Prefer `ssl: "full"`; `flexible` is an invisible downgrade for visitors. Supports `dryRun`. Rate limit: shares the 60/hour Cloudflare-write budget.

If the account's Cloudflare authorization predates this feature it will not carry the `zone-settings.write` scope, and this returns `CLOUDFLARE_REAUTHORIZE_REQUIRED` with a `connectUrl`. Reconnecting is the same one-click browser flow and does not disturb domains already moved.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `ssl` | string | no |  |
| `always_use_https` | string | no |  |
| `automatic_https_rewrites` | string | no |  |
| `min_tls_version` | string | no |  |
| `development_mode` | string | no |  |
| `cache_level` | string | no |  |
| `dryRun` | boolean | no |  |

## POST /api/json/v3/cloudflare/createRecord/{domain}

**Create a DNS record in the domain's Cloudflare zone**

**This writes to the Cloudflare zone that actually answers for the domain**, unlike `/dns/*`, which manages Porkbun's nameservers and no longer affects resolution once a domain has moved.

`name` accepts `@` for the apex or a bare label. MX requires `priority`. `proxied` applies to A/AAAA/CNAME only. Supports `dryRun`.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | yes | A, AAAA, CNAME, TXT, MX, NS, PTR or SPF for a plain `content` value. Structured types (SRV, CAA, TLSA…) need `data` instead. |
| `name` | string | no | `@` for the apex, a bare label (`www`) is expanded, or a full hostname. |
| `content` | string | no | The value the record points at. |
| `ttl` | integer | no | 1 = automatic (Cloudflare's default), otherwise 60–86400. |
| `priority` | integer | no | Required for MX; lower is preferred. |
| `proxied` | boolean | no | Orange cloud. A/AAAA/CNAME only. |
| `comment` | string | no | Free-text note stored on the record (100 chars). |
| `data` | object | no | Structured value for record types Cloudflare models as an object (SRV, CAA…), passed through as given. |
| `dryRun` | boolean | no |  |

## POST /api/json/v3/cloudflare/editRecord/{domain}/{recordId}

**Update a DNS record in the domain's Cloudflare zone**

**This writes to the Cloudflare zone that actually answers for the domain**, unlike `/dns/*`, which manages Porkbun's nameservers and no longer affects resolution once a domain has moved.

Partial update: fields you omit keep their current value. The response carries both the new record and the `previous` one. Supports `dryRun`.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |
| `recordId` | path | yes | Cloudflare record id from /cloudflare/getRecords. |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | no | A, AAAA, CNAME, TXT, MX, NS, PTR or SPF for a plain `content` value. Structured types (SRV, CAA, TLSA…) need `data` instead. |
| `name` | string | no | `@` for the apex, a bare label (`www`) is expanded, or a full hostname. |
| `content` | string | no | The value the record points at. |
| `ttl` | integer | no | 1 = automatic (Cloudflare's default), otherwise 60–86400. |
| `priority` | integer | no | Required for MX; lower is preferred. |
| `proxied` | boolean | no | Orange cloud. A/AAAA/CNAME only. |
| `comment` | string | no | Free-text note stored on the record (100 chars). |
| `data` | object | no | Structured value for record types Cloudflare models as an object (SRV, CAA…), passed through as given. |
| `dryRun` | boolean | no |  |

## POST /api/json/v3/cloudflare/deleteRecord/{domain}/{recordId}

**Delete a DNS record from the domain's Cloudflare zone**

**This writes to the Cloudflare zone that actually answers for the domain**, unlike `/dns/*`, which manages Porkbun's nameservers and no longer affects resolution once a domain has moved.

The record is read before deletion, so the response reports exactly what was removed and a bad id fails before anything is destroyed. Supports `dryRun`.

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |
| `recordId` | path | yes | Cloudflare record id from /cloudflare/getRecords. |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `dryRun` | boolean | no |  |

---

## More

- Guides (how-tos): https://porkbun.com/llms/guides
- Topic index: https://porkbun.com/llms
- Full reference (one file): https://porkbun.com/llms-full.txt
- OpenAPI spec (full schemas): https://porkbun.com/api/json/v3/spec
- Short overview: https://porkbun.com/llms.txt
- Official MCP server: https://porkbun.com/mcp (`npx -y @porkbunllc/mcp-server`)
- Create API keys: https://porkbun.com/account/api
