# Porkbun API v3 — API Key Management

> Request API keys programmatically (`/apikey/request` + `/apikey/retrieve`, approved by the account holder in the browser), and revoke the key making a call (`/apikey/revoke`).

**Site sharing:** let someone else, and their AI agent, run one domain's website without the account. `/apikey/share` asks (the account holder approves on porkbun.com and gets a link, QR code, share code and handoff prompt); the other person's agent calls `/apikey/claim` once, or they enter the share code when connecting the Claude app or ChatGPT; `/apikey/shares` and `/apikey/revokeShare` manage them. The key it makes has the `site` permission alone, limited to that domain. Guide: https://porkbun.com/llms/guides/share-site-access Listing, editing and revoking other keys is done on the website (https://porkbun.com/account/api).

Part of the Porkbun API v3.75. 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

## POST /api/json/v3/apikey/request

**Initiate an API key authorization request**

Initiate an API key authorization flow. No credentials are required. Returns a `requestToken` and `authUrl` that the account holder must visit (while logged in to Porkbun in that browser) to approve the request. The `authUrl` is valid for **30 minutes** (long enough for a brand-new user to create an account and verify their email); if it lapses, call this endpoint again for a fresh token. After approval, call `/apikey/retrieve` to get the public API key. By default (legacy flow) the secret API key is shown only once, in the user's browser, and must be pasted into the application manually. To let an agent receive the secret without a browser copy, supply a PKCE `codeChallenge` (see the parameter below): the key is then minted lazily and BOTH keys are returned once from `/apikey/retrieve` to the caller presenting the matching `codeVerifier`.

**Rate limit:** 20 requests per IP per 3600 seconds.

SANDBOX: pass `sandbox: true` to skip approval and get a sandbox key pair back immediately (both `apikey` and `secretapikey` in the response, plus `sandbox: true`).

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Human-readable name for the application/integration requesting access. Optional but strongly recommended: it is shown to the account holder on the approval screen so they know what they're granting access to. If omitted, the approval page shows "No application name was provided." |
| `codeChallenge` | string | no | Optional PKCE (RFC 7636) binding. base64url(SHA-256(codeVerifier)), 43 unpadded chars. When present, the request uses lazy-mint delivery: after approval the account holder copies nothing, and /apikey/retrieve returns BOTH the public and secret keys once to the caller that presents the matching codeVerifier. Omit for the legacy flow (secret shown only in the browser). |
| `codeChallengeMethod` | string | no | PKCE challenge method. Only S256 is supported. |
| `sandbox` | boolean | no | If true, skip the approval flow and immediately return a throwaway SANDBOX key pair (public `pk1_sb_`, secret `sk1_sb_`) for a fresh test account seeded with $1000 fake credit. Use it as apikey/secretapikey against the same base URL to run the whole API in the isolated sandbox — no real registry actions, DNS changes, or charges. Pass `scopes` to get a restricted sandbox key. Rate-limited (20/IP/hour). |
| `returnUrl` | string | no | Optional app-return redirect for native/mobile apps. Requires codeChallenge (PKCE). Must be an HTTPS URL your app claims as a Universal Link / App Link (custom URI schemes are rejected — another app could hijack them). After the account holder approves in the system browser, Porkbun redirects here with `?status=approved&requestToken=<token>` (or `status=denied`) so control returns to your app; it then calls /apikey/retrieve with its codeVerifier to receive the keys. The key is NEVER placed in the redirect — only the request token and status. |
| `scopes` | string[] | no | Optional permissions for the new key; `read` is always included. Shown to the account holder on the approval page, and the key gets exactly these. Leave out for a full-access key. Ask for the least you need, e.g. ["dns"] for a DNS updater. The account holder may grant less than you ask for (never more), so check `/ping` (`scopes`) once you have the key. Sandbox keys (`sandbox: true`) get exactly the scopes asked for, echoed in the response as `scopes`, so you can test a restricted key before asking for a real one. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/apikey/request \
  -H 'Content-Type: application/json' \
  -d '[]'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `requestToken` | string | Token used to poll `/apikey/retrieve` to check approval status. |
| `authUrl` | string | URL the account holder must visit to approve the request |
| `expiration` | string | ISO datetime when this request expires (30 minutes from creation) |
| `deliveryMode` | string | Which delivery flow this request uses. 'pkce' when a codeChallenge was supplied (the secret is returned once via /apikey/retrieve to the verifier holder); 'legacy' otherwise (the secret is shown only in the browser). |
| `message` | string | Human-readable instructions |

## POST /api/json/v3/apikey/retrieve

**Poll for API key approval**

Poll to check whether the account holder has approved an API key authorization request. Returns `status: PENDING` while awaiting approval. On approval, returns the public API key. For a **PKCE** request (one created with a codeChallenge), pass the matching codeVerifier here to receive BOTH keys once (secretapikey is included in that single response and never again); the secret must be claimed within 10 minutes of approval or the request expires (`REQUEST_EXPIRED`) and a new one is needed. For a **legacy** request the secret API key is never transmitted via this endpoint — it is displayed in the user's browser and must be pasted into the application.

**Rate limit:** 120 requests per IP per 3600 seconds.

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `requestToken` | string | yes | The token returned by /apikey/request. Must be a 64-character lowercase hex string. |
| `codeVerifier` | string | no | PKCE verifier (RFC 7636). Required only when the request was created with a codeChallenge. The high-entropy secret whose base64url(SHA-256(codeVerifier)) equals that challenge. When it matches, this response includes the secret key (secretapikey) once. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/apikey/retrieve \
  -H 'Content-Type: application/json' \
  -d '[]'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `apikey` | string | The approved public API key. Present only when status is SUCCESS. |
| `secretapikey` | string | The secret API key. Returned ONLY for a PKCE request, exactly once, on the retrieve that presents a valid codeVerifier. Never returned for legacy requests, and never again after the first successful claim (later calls return apikey only, with code SECRET_ALREADY_CLAIMED). Store it immediately. |
| `message` | string |  |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/apikey/revoke

**Revoke the calling API key**

Permanently revokes **the key making this call**, nothing else: a key can only revoke itself, so this cannot be used to change any other key. Use it when an agent or script is finished, or suspects its key has leaked. Any key can call it, including a read-only one.

Requires `"confirm": "revoke"` (without it the response says which key would be revoked). Supports `dryRun`. The key stops working within a few seconds and cannot be restored; if it belongs to an app connected over OAuth, that app is disconnected. Works for sandbox keys too. A key made by site sharing can call it to give its access back; the share is then recorded as revoked and the account holder is emailed.

**Errors:** `CONFIRMATION_REQUIRED`, `NOT_SUPPORTED_FOR_PRODUCT` (the request was not made with an API key).

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | header | no | Optional, recommended on every purchase and write. Send a unique value (a UUID) per intended operation. Repeating the request with the same key and an identical body within 24 hours returns the original response instead of doing it again, so a retry after a timeout cannot double-charge or double-apply. The same key with a different body returns 409 IDEMPOTENCY_KEY_MISMATCH; while the first request is still running, 409 IDEMPOTENCY_KEY_IN_USE. |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `confirm` | string | yes | Must be `revoke`. |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `revoked` | object |  |
| `message` | string |  |

## POST /api/json/v3/apikey/share

**Share one domain's website with someone (owner approves)**

Starts sharing one domain's website with another person, so they and the AI agent they use can run it without the account. **Nothing is granted by this call.** It returns an `approveUrl` that the account holder opens on porkbun.com: the page shows exactly what will be shared, and approving there (logged in, after the usual re-verify) creates a share link and QR code to send the other person; the link opens a page with a handoff prompt for their AI agent and the share code. After approval, `GET /apikey/shares` returns `shareUrl` (plus `handoffPrompt` and `shareCode`, for an agent that sends the message itself).

The other person claims it once, within `claimHours` of approval. Set `accessHours` to make the access itself end on its own (e.g. 48), counted from the claim; leave it out for access until revoked. They claim it: their agent calls `/apikey/claim`, or they connect the Claude app or ChatGPT with the share code. Either way a new key is made on this account with the `site` permission alone, limited to the domain: the other person (and their AI agent) can run that one website: WordPress content (`/hosting/wpRequest`, `/hosting/wpUploadMedia`), static files, Link in Bio edits, backups and restores, staging, PHP and WordPress versions, SSL and search visibility. They cannot buy anything, cancel the hosting or change auto-renew, change DNS, turn on a Link in Bio page, or see the rest of the account (no other domains, balance or invoices). The account holder is emailed when it is claimed, the key shows as "Share: <name>" in API Access, and `/apikey/revokeShare` or deleting that key ends it.

The domain must have hosting or a Link in Bio page. A key that was itself shared cannot share onward. Not available with sandbox keys. Supports `dryRun`.

**Errors:** `INVALID_INPUT`, `DOMAIN_NOT_FOUND`, `HOSTING_NOT_FOUND`, `DOMAIN_NOT_ALLOWED`, `INSUFFICIENT_SCOPE`, `RATE_LIMITED` (20 open shares), `SANDBOX_UNSUPPORTED`.

**Key permission needed:** `account`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | header | no | Optional, recommended on every purchase and write. Send a unique value (a UUID) per intended operation. Repeating the request with the same key and an identical body within 24 hours returns the original response instead of doing it again, so a retry after a timeout cannot double-charge or double-apply. The same key with a different body returns 409 IDEMPOTENCY_KEY_MISMATCH; while the first request is still running, 409 IDEMPOTENCY_KEY_IN_USE. |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | yes | The domain whose website to share. |
| `sharedWith` | string | yes | Who it is for (1-60 characters), e.g. `Sarah`. Shown on the approval page and to the other person. |
| `claimHours` | integer | no | How long the link works once approved (the window to claim it). |
| `accessHours` | integer | no | Optional: how long the access itself lasts, in hours, counted from when they connect (claim). Leave out for access until it is revoked. When it runs out the key stops working and the share shows `EXPIRED`; calls get `SHARE_ACCESS_ENDED`. Shown to the account holder on the approval page. |
| `dryRun` | boolean | no |  |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `shareId` | integer |  |
| `domain` | string |  |
| `sharedWith` | string |  |
| `approveUrl` | string | For the account holder to open and approve on porkbun.com. Only returned here. |
| `expires` | string | Approval deadline (24 hours). |
| `message` | string |  |
| `accessHours` | integer |  |

## GET /api/json/v3/apikey/shares

**List site shares**

This account's site shares (`/apikey/share`), newest first: `status` is `PENDING` (waiting for the account holder's approval), `ACTIVE` (approved, link not used), `CLAIMED` (the other person connected), `REVOKED` or `EXPIRED`. For an `ACTIVE` share, keys with full access or the `account` permission also get `shareUrl`, `qrCodeUrl`, `shareCode` and `handoffPrompt` to pass on; other keys see the share without them. A domain-limited key sees only its domains' shares. Also available via GET.

**Key permission needed:** `read`

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `shares` | object[] |  |

## POST /api/json/v3/apikey/shares

**List site shares**

This account's site shares (`/apikey/share`), newest first: `status` is `PENDING` (waiting for the account holder's approval), `ACTIVE` (approved, link not used), `CLAIMED` (the other person connected), `REVOKED` or `EXPIRED`. For an `ACTIVE` share, keys with full access or the `account` permission also get `shareUrl`, `qrCodeUrl`, `shareCode` and `handoffPrompt` to pass on; other keys see the share without them. A domain-limited key sees only its domains' shares. Also available via GET.

**Key permission needed:** `read`

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `shares` | object[] |  |

## POST /api/json/v3/apikey/revokeShare

**Revoke a site share**

Ends a site share by `id` (from `/apikey/shares`). An unclaimed link and code stop working; if the other person already connected, their key is disabled and their app or agent loses access within seconds. Supports `dryRun`.

**Errors:** `NOT_FOUND`, `DOMAIN_NOT_ALLOWED`, `INSUFFICIENT_SCOPE`.

**Key permission needed:** `account`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | header | no | Optional, recommended on every purchase and write. Send a unique value (a UUID) per intended operation. Repeating the request with the same key and an identical body within 24 hours returns the original response instead of doing it again, so a retry after a timeout cannot double-charge or double-apply. The same key with a different body returns 409 IDEMPOTENCY_KEY_MISMATCH; while the first request is still running, 409 IDEMPOTENCY_KEY_IN_USE. |

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes |  |
| `dryRun` | boolean | no |  |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `id` | integer |  |
| `message` | string |  |

## POST /api/json/v3/apikey/claim

**Claim a site share (no API key needed)**

Called by the agent of the person a website was shared with, using the `shareToken` from the share link or handoff prompt. **No API key needed.** Works once, while the share is approved and unexpired: it returns a new key pair, made on the sharing account, with the `site` permission alone and limited to the shared domain: the other person (and their AI agent) can run that one website: WordPress content (`/hosting/wpRequest`, `/hosting/wpUploadMedia`), static files, Link in Bio edits, backups and restores, staging, PHP and WordPress versions, SSL and search visibility. They cannot buy anything, cancel the hosting or change auto-renew, change DNS, turn on a Link in Bio page, or see the rest of the account (no other domains, balance or invoices). Store the secret immediately; it is not shown again. Then start with `GET /hosting/get/{domain}`. To give the access back later, the same key calls `/apikey/revoke` (the account holder is emailed). Calls outside that return `INSUFFICIENT_SCOPE` or `DOMAIN_NOT_ALLOWED`.

People using the Claude app or ChatGPT don't call this: they enter the share code when connecting the Porkbun connector. Rate limited to 20 calls per IP per hour.

**Errors:** `INVALID_INPUT`, `SHARE_NOT_FOUND` (used, revoked, expired or wrong; ask for a new one), `RATE_LIMIT_EXCEEDED`.

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `shareToken` | string | yes | The token from the share link (the part after /share/) or the handoff prompt. |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `apikey` | string |  |
| `secretapikey` | string | Returned once. |
| `domain` | string |  |
| `permissions` | string |  |
| `product` | string | `cloudWordPress`, `secureStaticHosting` or `managedLinks`. |
| `message` | string |  |
| `accessUntil` | string | When this key stops working, if the share has an access length; null = until revoked. |

---

## More

- Guides (how-tos): https://porkbun.com/llms/guides
- Help articles (knowledge base) as Markdown: https://porkbun.com/llms/kb
- 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
