{"openapi":"3.0.0","info":{"title":"Porkbun API v3","version":"3.68","description":"The Porkbun API enables programmatic domain registration, DNS management, SSL certificate retrieval, and related operations. It is fully usable by AI agents, automation scripts, and developer tools.\n## Sandbox \/ test mode\n\nBuild and test an integration end-to-end with **no real registry actions, no DNS changes, no certificates, and no charges** by using a **sandbox API key** \u2014 a public key prefixed `pk1_sb_` (secret `sk1_sb_`), created at https:\/\/porkbun.com\/account\/api. Same base URL; just swap the key. In sandbox:\n- Every response includes `\"sandbox\": true` and an `X-Porkbun-Sandbox: true` header.\n- Registrations, renewals, transfers, DNS, contacts, nameservers, glue, and DNSSEC are simulated against an isolated datastore; your account starts with fake credit (top up\/reset via `\/sandbox\/topup` and `\/sandbox\/reset`). Availability and pricing reflect the real catalog so quotes match production.\n- Endpoints that can't be simulated \u2014 **hosting**, **email** and **closeouts** \u2014 return `SANDBOX_UNSUPPORTED`. Closeouts are excluded because the inventory and the purchase both live with a third party that has no test environment, so a simulated buy would claim a real domain.\n- **Webhooks are delivered in the sandbox.** Register an endpoint with `POST \/webhook\/create`, then either perform an operation (the matching signed event is delivered just like production) or fire any event on demand with `POST \/sandbox\/triggerWebhook` to test your handler and signature verification.\n\n## Mock server (no credentials)\n\nLearn any endpoint's exact response shape with **zero setup** \u2014 no key, no account. Every real path is mirrored under `\/mock`:\n- `GET \/mock` lists every mockable endpoint.\n- `GET|POST \/mock\/<path>` returns a schema-accurate example response for that operation (e.g. `\/mock\/domain\/listAll`). Append `?status=error` for the error shape.\n\nMock responses touch no datastore, are identical in shape to the live API, and are signalled by an `X-Porkbun-Mock: true` header. Use it to build client code before you have credentials; switch to a sandbox key when you want real behavior with fake money.\n\n## Quickstart\n\n**1. Get your API keys** \u2014 visit https:\/\/porkbun.com\/account\/api\n\n**2. Test connectivity**\n```bash\ncurl https:\/\/api.porkbun.com\/api\/json\/v3\/ip\n```\nReturns your public IP. No credentials required.\n\n**3. Check domain availability** (also verifies your credentials)\n```bash\ncurl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/checkDomain\/example.com \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"apikey\":\"your_api_key\",\"secretapikey\":\"your_secret_key\"}'\n```\nReturns `avail: \"yes\"\/\"no\"` and `price` in USD. Returns an error if credentials are invalid.\n\n**4. Register the domain** \u2014 convert `price` to pennies (e.g. $9.73 \u2192 973) for `cost`\n```bash\ncurl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/create\/example.com \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"apikey\":\"your_api_key\",\"secretapikey\":\"your_secret_key\",\"cost\":973,\"agreeToTerms\":\"yes\"}'\n```\n\n**5. Add a DNS record**\n```bash\ncurl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/create\/example.com \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"apikey\":\"your_api_key\",\"secretapikey\":\"your_secret_key\",\"type\":\"A\",\"content\":\"1.2.3.4\",\"ttl\":\"600\"}'\n```\n\n## AI agents (MCP)\n\nIf you're building with Claude Desktop, Cursor, Cline, or another [Model Context Protocol](https:\/\/modelcontextprotocol.io) client, install our official Porkbun MCP server ([setup and tool list](https:\/\/porkbun.com\/mcp)) \u2014 no integration code required, your AI client gets domain registration, DNS, SSL, marketplace, and account-management tools natively.\n\n```bash\nnpx -y @porkbunllc\/mcp-server\n```\n\nRepo and Claude Desktop config: [github.com\/oborseth\/Porkbun-MCP](https:\/\/github.com\/oborseth\/Porkbun-MCP). It covers everything documented below, plus documentation-search tools that work without credentials. See the repo README for the current tool list. All write operations automatically attach an `Idempotency-Key` so agent retries don't double-charge.\n\nOr use the hosted server with no install: `https:\/\/mcp.porkbun.com\/mcp`. Clients that support OAuth sign in through the browser (public clients with PKCE, or, for platforms that require one, a client ID and secret registered at `https:\/\/porkbun.com\/oauth2\/register`); agent platforms that can't do OAuth can send the account's API key pair instead (`X-API-Key` + `X-Secret-API-Key`, or `Authorization: Bearer pk1_\u2026:sk1_\u2026` as a single secret), with the key's permissions, allowed domains and spend limits applying as usual. Setup: [Connect ChatGPT, Claude or another MCP client](https:\/\/porkbun.com\/llms\/guides\/connect-chatgpt-or-claude).\n\n## What you can build\n\n- **Agentic domain registration** \u2014 Search availability and pricing across hundreds of TLDs, then register domains on behalf of users with a single API call\n- **Automated DNS management** \u2014 Provision, update, and tear down DNS records as part of infrastructure automation or app deployment pipelines\n- **Dynamic DNS clients** \u2014 Use `\/ping` or `\/ip` to detect IP address changes, then update A\/AAAA records automatically\n- **Domain portfolio tools** \u2014 List, monitor expiry, and configure auto-renewal settings across all domains in an account\n- **SSL automation** \u2014 Retrieve free SSL certificate bundles for domains registered at Porkbun\n- **Domain availability search** \u2014 Check availability and real-time pricing across all supported TLDs\n\n## Agent-friendly design\n\n- **Machine-readable error codes** \u2014 Every error response includes a `code` field (e.g. `INVALID_DOMAIN`, `INSUFFICIENT_FUNDS`) for programmatic branching\n- **Header authentication** \u2014 Pass `X-API-Key` \/ `X-Secret-API-Key` as request headers; no JSON body required for read operations\n- **Hosted MCP connector (ChatGPT, Claude)** \u2014 `https:\/\/mcp.porkbun.com\/mcp` serves this API as MCP tools with a browser sign-in instead of keys (OAuth 2.1 + PKCE; guide: https:\/\/porkbun.com\/llms\/guides\/connect-chatgpt-or-claude). Each connection is its own API key, listed at \/account\/api as \"(connected app)\", and its `pbo_at_` access tokens are accepted here as `Authorization: Bearer`, for MCP clients rather than as a general way to call the API. Variants for app-directory listings: `\/mcp\/no-topups` and `\/mcp\/no-purchases`\n- **GET support on read endpoints** \u2014 All read-only endpoints accept `GET` requests, making safe\/idempotent operations distinguishable by HTTP method\n- **Rate limit headers** \u2014 `Retry-After` (seconds to wait) on every 429, plus `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` on rate-limited endpoints, enable intelligent backoff\n- **Spec discovery** \u2014 Every API response includes `Link: <https:\/\/porkbun.com\/api\/json\/v3\/spec>; rel=\"describedby\"` so clients can self-discover this spec\n- **Idempotency keys** \u2014 Send `Idempotency-Key: <unique-string>` on POST endpoints; retries within 24h return the cached response so a network blip can't double-charge\n- **Official MCP server** \u2014 `npx -y @porkbunllc\/mcp-server` exposes this entire API as native tools for Claude Desktop, Cursor, and other Model Context Protocol clients\n- **Request IDs** \u2014 every response carries an `X-Request-Id` header and a `requestId` field in the JSON body; reference these in support tickets, retry-deduplication logic, or log correlation\n- **Version signalling** \u2014 every response carries an `X-API-Version` header (matching this spec's `info.version`). The URL path stays at `\/api\/json\/v3\/` across all minor versions; minor bumps are always backward-compatible, so you can pin to `v3` and watch the header (or the Changelog below) to know what's available\n- **Per-key scoping** \u2014 restrict each API key to specific source IPs (with CIDR support) and\/or specific target domains. Lets you hand an agent a key that can only operate on the domains you intend, from the network you expect\n- **Plain-text docs for LLMs** \u2014 a short overview at [\/llms.txt](https:\/\/porkbun.com\/llms.txt) and the full reference as flat Markdown at [\/llms-full.txt](https:\/\/porkbun.com\/llms-full.txt), and per-topic pages at [\/llms](https:\/\/porkbun.com\/llms) (no JavaScript required, generated from this spec)\n- **Per-TLD registration requirements (JSON Schema)** \u2014 `GET \/domain\/getRegistrationRequirements\/{tld}` returns whether a TLD is registerable via the API, the `\/domain\/create` body as a JSON Schema, and (for TLDs with registry eligibility rules like `.us`\/`.ca`) a second JSON Schema of the required fields and allowed values. Assemble and validate a registration payload before sending it, instead of discovering requirements through failed attempts.\n- **Dry run \/ validate-only** \u2014 pass `dryRun: true` to rehearse a write without performing it. On the billable ops (`\/domain\/create`, `\/domain\/renew`, `\/domain\/transfer`) it runs every pre-flight check and returns the would-be cost, balance, and `wouldSucceed` WITHOUT charging. It also works on **DNS record writes** (`\/dns\/create`, `\/dns\/edit`, `\/dns\/editByNameType`, `\/dns\/delete`, `\/dns\/deleteByNameType`) and **nameserver updates** (`\/domain\/updateNs`): validates ownership, the target record, and permissions, returns `wouldSucceed` WITHOUT mutating anything \u2014 so an agent can safely rehearse a destructive change before applying it\n- **Per-operation safety metadata** \u2014 every operation in this spec carries an `x-porkbun-agent` vendor extension `{safe, cost, destructive, reversible, requiresConfirmation}` so a tool generator or planner can tell a read from a billable or destructive write without heuristics (the official MCP exposes equivalent read-only\/destructive hints as standard MCP tool annotations)\n- **Retryability on errors** \u2014 each error's `next_action` includes a `retryable` boolean: `true` only for transient conditions where re-sending the same request can succeed (rate limits, in-flight idempotency, provisioning-not-ready); `false` when you must change something first. Branch on it instead of guessing from the message\n\n## Intended use\n\nThe Porkbun API is not a reseller service as defined under ICANN\u2019s Registrar Accreditation Agreement (RAA). All domain registrations are processed directly by Porkbun as the registrar of record. The API is intended for managing domains within your own account or on behalf of clients, and does not establish a reseller relationship.\n\n## Authentication\n\n**1. JSON body (primary)** \u2014 Include `apikey` and `secretapikey` in the JSON request body. This is the standard method.\n\n**2. Request headers** \u2014 Pass `X-API-Key: <apikey>` and `X-Secret-API-Key: <secretapikey>` as headers instead of in the body. Header auth takes effect only when no body credentials are present.\n\n## HTTP status codes\n\n- **400** \u2014 Request error (see `code` and `message` in response body)\n- **403** \u2014 Additional authentication required (e.g. two-factor code)\n- **429** \u2014 Rate limit exceeded (see the `Retry-After` header for seconds to wait; also `X-RateLimit-Reset`)\n\n## Error codes\n\nEvery error response includes a `code` string field alongside `status: \"ERROR\"` and `message`. Use `code` for programmatic error handling; use `message` for display to users.\n\n**Authentication and protocol**\n\n| Code | Meaning |\n|------|---------|\n| `INVALID_PROTOCOL` | Request was not made over HTTPS |\n| `METHOD_NOT_ALLOWED` | HTTP method not allowed for this endpoint |\n| `INVALID_OR_EMPTY_JSON` | Request body is missing or not valid JSON |\n| `API_KEY_REQUIRED` | No API key or token was provided |\n| `INVALID_API_KEYS_001` | API key and secret combination is invalid |\n| `INVALID_TOKEN` | Bearer token is invalid or expired |\n| `INVALID_USER` | Account associated with the API key was not found or is not active |\n| `IP_NOT_ALLOWED` | This API key has an IP allowlist configured and the request source IP is not in it. HTTP 403. |\n| `DOMAIN_NOT_ALLOWED` | This API key has a domain allowlist configured and the target domain is not in it. HTTP 403. |\n| `HOSTING_DIR_NOT_EMPTY` | `\/hosting\/deleteFile` on a directory that still has files; `contents` lists them. Delete those first or send `recursive: true`. |\n| `MPP_NOT_AVAILABLE` | `\/account\/topupMpp`: the account cannot add credit through an agent card payment (e.g. bank-transfer-only). |\n| `MPP_UNAVAILABLE` | The payment link could not be created; nothing was charged. Retry. |\n| `CRYPTO_NOT_AVAILABLE` | `\/account\/topupCrypto`: the account cannot add credit with crypto via the API (new account, bank-transfer-only, or not eligible); the message says which. |\n| `CRYPTO_TOPUP_LIMIT` | `\/account\/topupCrypto`: 10 checkouts opened today. |\n| `CRYPTO_CHECKOUT_FAILED` | Coinbase did not create the checkout; nothing was charged. Retry. |\n| `CHECKOUT_NOT_FOUND` | No crypto checkout with that ID on this account. HTTP 404. |\n| `INVOICE_NOT_FOUND` | No order with that ID belongs to this account (`\/account\/invoice*`). HTTP 404. |\n| `INVOICE_PDF_FAILED` | The invoice PDF could not be generated; retry, or use `GET \/account\/invoice\/{orderId}`. HTTP 500. |\n\n| `MISSING_PARAMETER` | A required field or path segment is absent; the message names it |\n| `INSUFFICIENT_SCOPE` | HTTP 403: the API key's permissions (`keyScopes`) do not include the one this call needs (`requiredScope`). Nothing was changed. The account holder can change the key's permissions in API Access |\n| `ID_VERIFICATION_REQUIRED` | `\/domain\/create`: the account holder must verify their identity at https:\/\/porkbun.com\/account\/idVerification before this registration (or pay it by card). Nothing was charged. Not currently enforced |\n| `NEW_ACCOUNT_LIMIT` | `\/domain\/create`: a new account reached the number of API registrations allowed in its first days (`retryAfter` seconds until it lifts). Nothing was charged. Not currently enforced |\n| `VERIFICATION_REQUIRED` | The account's email and phone must be verified first (both can be verified over the API: `\/account\/verifyEmail\/send` and `\/account\/verifyPhone\/send`, then `\/confirm`); nothing was charged |\n| `TERMS_NOT_AGREED` | Send `agreeToTerms: \"yes\"` |\n| `ORDER_FAILED` | The order could not be created; nothing was charged. Retry |\n| `INVALID_INPUT` | A parameter is missing, the wrong type, or out of range; the message says which |\n| `API_ACCESS_DISABLED` | The domain is not opted in to API access. Only the account holder can turn it on (per domain, or for all domains, in account settings); `list_domains` \/ `\/domain\/listAll` show `apiAccess` |\n| `DOMAIN_NOT_ACTIVE` | The domain is not in a state that can be managed right now (for example pending or expired) |\n| `DOMAIN_EXPIRED` | The domain has expired, so email hosting on it can't be renewed until the domain is (`\/email\/renewMailbox`); nothing was charged |\n| `DOMAIN_NOT_FOUND` | The domain is not in this account (check the spelling, or list them with `\/domain\/listAll`). The response names the account in `account`, so a key for the wrong account is obvious |\n\n**Rate limiting**\n\n| Code | Meaning |\n|------|---------|\n| `RATE_LIMIT_EXCEEDED` | Request rate limit reached; wait the seconds in the `Retry-After` header (also `ttlRemaining` field \/ `X-RateLimit-Reset` header) before retrying |\n\n**Domain operations**\n\n| Code | Meaning |\n|------|---------|\n| `INVALID_DOMAIN` | Domain parameter is invalid or not in your account (on `\/dns\/*` the response names the account in `account`) |\n| `DOMAIN_NOT_AVAILABLE` | Domain is not available for registration |\n| `DNS_CHANGE_CONFIRMATION_REQUIRED` | `\/email\/addForward` would delete other apex MX records or rewrite the SPF record; `dnsChanges` says what. Nothing was changed. Resend with `confirmDnsChanges: true` once the user agrees |\n| `FORWARD_CREATE_FAILED` | The forward could not be saved (the forwarding DNS records are already in place); retry |\n| `FORWARD_DELETE_FAILED` | The forward could not be deleted; check `\/email\/getForwards\/{domain}` and retry |\n| `DUPLICATE_FORWARD` | That address already forwards there; nothing needed doing |\n| `FORWARD_NOT_FOUND` | No forward with that `address` and `forwardTo` (list them with `\/email\/getForwards\/{domain}`) |\n| `FORWARD_LIMIT_REACHED` | The domain has its maximum number of email forwards (`limits`) |\n| `MAILBOX_EXISTS` | The address is already an email hosting mailbox (so it cannot be a forward, or a second mailbox) |\n| `FORWARD_EXISTS` | The address is an email forward, so it cannot also be a mailbox |\n| `NO_PENDING_MAILBOX` | `\/email\/setupMailbox`: no paid or trial mailbox is waiting to be set up |\n| `INVALID_PASSWORD` | The mailbox password breaks the rules in the message; or leave it out to have one generated |\n| `EMAIL_HOSTING_UNAVAILABLE` | Email hosting cannot be bought on this account right now (new accounts that paid in crypto, as on the website); nothing was charged |\n| `MAILBOX_PROVISION_FAILED` | The mailbox could not be created; the charge was returned to account credit |\n| `MAILBOX_NOT_FOUND` | `\/email\/renewMailbox`: no mailbox with that `mailboxId` \/ `address` on the domain |\n| `MAILBOX_RENEW_FAILED` | The renewal did not go through; the charge was returned to account credit |\n| `MAILBOX_SETUP_FAILED` | The mailbox could not be given its address; nothing changed |\n| `WILDCARD_NOT_SUPPORTED` | Catch-all \/ wildcard forwards are not supported |\n| `INVALID_EMAIL` | An address is not a valid email address |\n| `EMAIL_BLOCKED` | Email services are blocked on this domain; contact support |\n| `IDN_LANGUAGE_REQUIRED` | `\/domain\/create` of an internationalized name whose registry needs its language, sent without `idnLanguage`. Nothing was charged; `idnLanguages` lists the accepted `{name, code}` pairs. Resend with `idnLanguage` |\n| `INVALID_IDN_LANGUAGE` | `idnLanguage` is not one of that registry's languages. Nothing was charged; use a code from `idnLanguages` |\n| `INSUFFICIENT_FUNDS` | Not enough **prepaid account credit**. A purchase paid from credit never charges a saved card by itself, but `POST \/account\/topup` can charge a saved card and add credit in one call, and the purchase can instead be paid directly with `payWith` (`card` or `usdc`). The response carries `cost`, `balance` and `shortfall` in cents, plus `topUpAvailable` (whether a saved card can be charged now) and, when true, `topUpAmountToCover`, and `usdcAmountToCover` (the `POST \/account\/topupCrypto` amount that covers the shortfall after Coinbase's fee); `next_action.type` is `top_up` or `add_funds` accordingly. Dry runs report the same fields |\n| `DNSSEC_CREATE_FAILED` | The registry refused the DS record; the message carries its reason and code |\n| `DNSSEC_DELETE_FAILED` | The registry did not accept the DS removal |\n| `DNSSEC_INFO_UNAVAILABLE` | The registry did not answer for the domain's DNSSEC records; retry |\n| `COST_MISMATCH` | `cost` did not equal the current price in cents for the term being bought; the message names the expected value. Send `dryRun: true` with `cost: 0` to be quoted |\n| `RENEWAL_TOO_SOON` | The domain was registered or renewed within the last 30 days; the API renews it only after that. `renewableAt` says when. Nothing was charged |\n| `ORDER_TOO_LARGE` | A single API registration cannot exceed $100. Register it on the website |\n| `NO_PAYMENT_METHOD` | `POST \/account\/topup` had no saved payment method to charge. A card can only be saved on the website |\n| `TOPUP_LIMIT_EXCEEDED` | The account hit the per-day (5) or per-month (20) ceiling on API top-ups, or the month's dollar ceiling on credit added over the API by any method (the monthly spend limit, or $100 when none is set). Nothing was charged |\n| `PAYMENT_REQUIRED` | HTTP 402: `payWith: \"usdc\"` was sent and this purchase is waiting for payment. Pay the `PAYMENT-REQUIRED` header's x402 terms and repeat the request with `PAYMENT-SIGNATURE`. Nothing was charged |\n| `PAYMENT_FAILED` | HTTP 402: Coinbase refused the payment (for example, insufficient USDC). Fresh terms are in `PAYMENT-REQUIRED`. Nothing was charged |\n| `PAYMENT_IN_PROGRESS` | HTTP 409: another request is processing this payment. Do not pay again |\n| `PAYMENT_PENDING` | HTTP 409: the checkout named by `usdcCheckoutId` was paid and is settling. Repeat after `retryAfter` seconds; do not pay again |\n| `PAYMENT_MISMATCH` | HTTP 409: the USDC checkout or card payment was opened for a different purchase or price. Repeat the request that returned it, unchanged |\n| `PAYMENT_NOT_FOUND` | HTTP 404: `usdcCheckoutId` or `cardPaymentId` is not a payment this account opened (card payments last two hours) |\n| `PAYMENT_ALREADY_USED` | HTTP 409: the checkout was already used: it paid for a purchase, or went to account credit after an hour unclaimed. Do not pay again; buy from credit if the purchase is not done |\n| `PAYMENT_EXPIRED` | HTTP 409: the payment can no longer be used (a USDC checkout expired or failed, or a card hold was released, or a card payment link passed `expiresAt` unpaid). Send the purchase without `usdcCheckoutId` \/ `cardPaymentId` for a new one |\n| `PHONE_CODE_INVALID` | The phone verification code is wrong or expired |\n| `PHONE_VERIFY_UNAVAILABLE` | This phone cannot be verified over the API (no number on the account, or a country that needs the website's CAPTCHA). Verify on the website |\n| `PHONE_VERIFY_LIMIT` | Five codes sent today, or ten wrong codes this hour |\n| `EMAIL_CODE_INVALID` | The email verification code is wrong, expired, or for an older address |\n| `EMAIL_VERIFY_LIMIT` | Ten wrong email codes this hour |\n| `EMAIL_VERIFY_UNAVAILABLE` | The verification email could not be sent, or the account has no address |\n| `PHONE_VERIFY_FAILED` | The code could not be sent. Retry, or use `\"channel\": \"call\"` |\n| `MONTHLY_SPEND_LIMIT_EXCEEDED` | The purchase would take this month's API spending past the account's monthly limit (its own, or the $100 default; `limitSource` says which). Nothing was charged. Only the account holder can raise it, on the website |\n| `CARD_DECLINED` | The saved card was declined on a top-up. No credit was added |\n| `TOPUP_FAILED` | A top-up did not complete and nothing was charged. Retryable |\n| `REGISTRANT_CHANGE_NOT_SUPPORTED` | A registrant name\/org change on a .au domain is a paid auDA ownership trade - do it at porkbun.com; admin\/tech\/billing edits work via the API |\n| `ADDRESS_VALIDATION_REQUIRED` | The registrant address for an address-validated TLD needs validation; re-submit with addressValidationChoice using the returned suggestedAddress |\n\n**DNS operations**\n\n| Code | Meaning |\n|------|---------|\n| `INVALID_TYPE` | DNS record type is not supported |\n| `INVALID_RECORD_ID` | DNS record ID was not found or is not owned by your account |\n| `DUPLICATE_RECORD` | A record with this exact name, type and content already exists, so nothing was created. The existing record's id is returned in `existingId` \u2014 adopt it rather than creating another. HTTP 400. |\n| `NOTHING_TO_IMPORT` | `\/dns\/import` had no records supplied and could discover none. Once a domain has left its old registrar its published records can no longer be read, so they must be supplied explicitly. HTTP 400. |\n| `TOO_MANY_RECORDS` | More than 500 records in one `\/dns\/import` call. HTTP 400. |\n| `ZONE_RECORD_LIMIT` | The zone is at the maximum number of DNS records (2,500). Nothing was created. `count` gives the current size and `limit` the ceiling; delete records you no longer need first. HTTP 400. |\n| `RESTORE_POINT_NOT_FOUND` | That restore point does not exist for this domain. List them with `GET \/dns\/history\/{domain}`. A point belonging to another account reads the same way rather than confirming it exists. HTTP 404. |\n| `RESTORE_POINT_EMPTY` | The restore point holds no records, so there is nothing to restore from it. HTTP 400. |\n| `DNS_READ_FAILED` | The current zone could not be read, so nothing was changed. Retryable. HTTP 500. |\n| `BULK_CHECK_TOO_MANY` | More than 25 domains in one `POST \/domain\/checkDomain` call. Nothing was checked; split the list. HTTP 400. |\n| `BULK_CHECK_TOO_SLOW` | The bulk check's mix of TLDs would need more sequential registry commands than one request can safely wait on (some registries accept only a few domains per command; .de accepts one). Nothing was checked. `registryCommandsNeeded` and `maxRegistryCommands` are in the response. HTTP 400. |\n| `TRANSFER_INIT_FAILED` | The transfer could not be started; the order is refunded automatically. Not a credentials or verification problem. HTTP 400. |\n| `TRANSFER_HOLD_NOT_AVAILABLE` | `holdForDnsSetup` is not supported for this TLD (.uk, Handshake). Nothing was charged; resend without the flag. |\n| `REGISTRANT_EMAIL_NOT_ACCEPTED` | The registry for this TLD (currently .in and its second levels) refuses registrant contacts on temporary or encrypted email providers, and suspends domains that use one. Nothing was charged. Change the email on your default registrant contact, then retry. |\n| `TRANSFER_NOT_FOUND` | No pending inbound transfer for that domain on this account. |\n| `TRANSFER_NOT_HELD` | `\/domain\/startTransfer` on a transfer that is not held at `PENDINGDNS`. |\n| `TRANSFER_ZONE_EMPTY` | Releasing would move the domain to an empty Porkbun zone. Import the records first, or resend with `force: true`. |\n| `TRANSFER_ZONE_FAILED` | The DNS zone could not be created for a held transfer. |\n| `TRANSFER_NOT_CANCELLABLE`, `TRANSFER_ALREADY_COMPLETED` | The transfer is past the point where it can be cancelled. |\n| `TRANSFER_STATE_UNCONFIRMED` | The registry would not confirm the withdrawal, so nothing was refunded and the transfer was left intact. Deliberate: better an unrefunded cancel than a refunded live transfer. |\n| `TRANSFER_NOT_REPAIRABLE` | The transfer is not in a state where replacing the auth code helps. |\n| `INVALID_AUTH_CODE` | The replacement auth code was rejected by the registry, so it was not stored. |\n| `RECORD_CONFLICT` | The record cannot coexist with what is already at that name: a CNAME is exclusive of every other type there (RFC 1034). The blocking records are listed in `conflictingRecords`. HTTP 400. |\n\n**Cloudflare connect**\n\n| Code | Meaning |\n|------|---------|\n| `CLOUDFLARE_NOT_CONNECTED` | No active Cloudflare grant \u2014 the owner must authorize in a browser at porkbun.com\/account\/connectCloudflare, then poll `\/cloudflare\/getConnection` |\n| `NOT_QUEUED` | That domain has never been queued for a Cloudflare move |\n| `ZONE_NOT_READY` | The Cloudflare zone doesn't exist yet \u2014 poll `\/cloudflare\/get\/{domain}` until connected\/done |\n| `CLOUDFLARE_API_ERROR` | Cloudflare rejected the call; their error text is in `message` |\n| `CLOUDFLARE_REAUTHORIZE_REQUIRED` | The stored Cloudflare grant lacks the permission for this call (Cloudflare code 9109) \u2014 the owner should reconnect at the returned `connectUrl` |\n| `INVALID_VALUE` | A supplied value isn't one of the allowed options; the message lists them |\n| `UNSUPPORTED_RECORD_TYPE` | That record type is structured at Cloudflare \u2014 supply a `data` object instead of `content` |\n| `TOO_MANY_DOMAINS` | More than 500 domains in one `\/cloudflare\/connect` call \u2014 split the list |\n| `RETRY_FAILED` | Can't re-queue (already connected, already in progress, or no longer in the account) |\n| `ROLLBACK_FAILED` | Can't undo (never moved, already back on Porkbun nameservers, or being worked on) |\n\n**Two DNS surfaces.** The **Porkbun DNS** endpoints (`\/dns\/*`) manage the zone Porkbun's nameservers serve. The **Cloudflare** endpoints (`\/cloudflare\/*Record`) manage the zone in a customer's own Cloudflare account after a move. Pick by who is authoritative for the domain \u2014 `\/cloudflare\/getZone\/{domain}` will tell you.\n\n**Note on DNS for Cloudflare-connected domains:** once a domain has been moved to Cloudflare, Porkbun's nameservers no longer answer for it. `\/dns\/*` writes still succeed against the Porkbun zone (so it stays in step if you ever roll the move back), but they do **not** change what resolves \u2014 every `\/dns\/*` response for such a domain carries a `warnings` entry saying so. Read and write those domains with `\/cloudflare\/getRecords`, `\/cloudflare\/createRecord`, `\/cloudflare\/editRecord` and `\/cloudflare\/deleteRecord`.\n\n**Hosting**\n\n| Code | Meaning |\n|------|---------|\n| `HOSTING_ALREADY_EXISTS` | The domain already has hosting; deprovision first or manage the existing plan |\n| `HOSTING_NOT_FOUND` | No hosting on this domain \u2014 provision it via `\/hosting\/create` |\n| `HOSTING_NOT_READY` | Provisioning hasn't finished; poll `\/hosting\/get` until `status` is `ACTIVE` |\n| `NOT_SUPPORTED_FOR_PRODUCT` | Wrong product for this endpoint \u2014 file operations are Secure Static Hosting only, WordPress credential endpoints are Cloud for WordPress only |\n| `PREVIEW_SITE_NOT_SUPPORTED` | Free $0 preview\/parked sites aren't managed over the API; upgrade to a paid plan first |\n| `FULL_ACCESS_ACKNOWLEDGMENT_REQUIRED` | `role: \"administrator\"` needs `acknowledgeFullAccess: true` (that credential can install plugins, i.e. run code on the site) |\n| `WP_CLI_FAILED` | The WordPress site rejected the command (e.g. no administrator account to bind to) |\n| `COST_ACKNOWLEDGMENT_REQUIRED` | Echo the plan price in cents as `acknowledgedCost` |\n| `NAMESERVER_CHANGE_REQUIRED` | Provisioning moves the domain to Porkbun NS; re-submit with `agreeToNameserverChange=true` |\n\n**Link in Bio**\n\n| Code | Meaning |\n|------|---------|\n| `LINKINBIO_NOT_FOUND` | The domain has no Link in Bio page; turn on a free one with `\/linkinbio\/create` or buy one with `\/hosting\/create` |\n| `LINKINBIO_ALREADY_EXISTS` | The domain already has a page; read it with `\/linkinbio\/get` |\n| `LINKINBIO_CREATE_FAILED` | The free page could not be turned on (the message says why) |\n| `LINKINBIO_SAVE_FAILED` | A change could not be saved; retry |\n| `FEATURE_NOT_IN_PLAN` | The free plan does not include this (scheduling, animation, passcode protection, analytics IDs); nothing was changed |\n| `LINK_LIMIT_REACHED` | The page has as many links as its plan allows (50 free, 1000 paid) |\n| `LINK_NOT_FOUND` | No link with that id on this page; ids come from `\/linkinbio\/get` |\n| `DUPLICATE_LINK` | Another link has the same title and URL |\n\nAdditional endpoint-specific codes may be returned; always check `message` for details.\n\n**Closeouts**\n\n| Code | Meaning |\n|------|---------|\n| `CLOSEOUT_NOT_FOUND` | That domain is not currently offered as a closeout. Inventory turns over constantly. HTTP 404. |\n| `CLOSEOUT_UNAVAILABLE` | Another buyer claimed it first. Closeouts are first-come at a fixed price, so retrying the same name is pointless. If it happened after the charge, the order was refunded and `refunded: true` is set. HTTP 409. |\n| `CLOSEOUT_NOT_ELIGIBLE` | The account is blocked from auctions and closeouts by support, over past-due invoices or an auction terms violation. Not retryable. HTTP 403. |\n| `CLOSEOUT_CLAIM_FAILED` | The provider rejected the claim after the charge; the order was refunded automatically. |\n| `CLOSEOUT_SOURCE_UNAVAILABLE` | The inventory provider could not be reached. Nothing was charged. HTTP 502. |\n| `PRICING_UNAVAILABLE` | The renewal price for the domain could not be looked up. Nothing was charged. HTTP 502. |\n| `ORDERS_BLOCKED` | This account cannot place new orders. HTTP 403. |\n\n**Webhooks**\n\n| Code | Meaning |\n|------|---------|\n| `INVALID_WEBHOOK_URL` | The endpoint URL is not an acceptable delivery target. It must be `https:\/\/` on port 443, carry no username\/password, and its hostname must resolve to a public internet address \u2014 private, loopback, link-local, CGNAT and other reserved ranges are refused. Re-checked immediately before every delivery, so an endpoint whose DNS later points at private space stops being delivered to. |\n\n## API key restrictions (IP addresses, domains and permissions)\n\nEach API key can optionally be restricted to specific source IPs, specific target domains, and specific permissions. All three are configured per key at [porkbun.com\/account\/api](https:\/\/porkbun.com\/account\/api): when creating the key, or later with the gear icon next to it. Empty\/unset = no restriction.\n\n**Source IP allowlist.** When set, requests from any other IP fail immediately with HTTP 403 `IP_NOT_ALLOWED`, before any other endpoint logic runs. Supports IPv4 and IPv6, both bare addresses and CIDR ranges. One entry per line in the UI. Examples:\n\n```\n203.0.113.10\n198.51.100.0\/24\n2001:db8::\/32\n```\n\n**Target domain allowlist.** When set, any operation against a domain not in the list fails with HTTP 403 `DOMAIN_NOT_ALLOWED`. Exact match only \u2014 `example.com` does not implicitly include `foo.example.com`, because each registered domain is independent. (Subdomains inside DNS records are scoped under the parent domain, which is what gets checked.) Example:\n\n```\nexample.com\nmyothersite.io\n```\n\n**Permissions.** When set, the key can only call operations in its scopes: `read` (see everything, change nothing; always included), `dns` (DNS records, URL forwarding), `domains` (nameservers, contacts, auto-renew, glue, DNSSEC, transfer settings, moving to Cloudflare, SSL certificate downloads), `email` (forwarding, mailboxes), `hosting`, `purchase` (anything that charges), `funding` (adding credit, auto top-up) and `account` (webhooks, invitations, phone verification). The settings page offers **Read-only** and **Manage, but no purchases** (everything except `purchase` and `funding`) as presets. Each operation lists the scope it needs in `x-porkbun-scope`. A call outside the key's scopes fails with HTTP 403 `INSUFFICIENT_SCOPE` (with `requiredScope` and `keyScopes`) and changes nothing; a dry run of a purchase or top-up needs only `read`. `\/ping` returns the key's `scopes`. Apps can ask for scopes when requesting a key (`\/apikey\/request`); the account holder can grant those or less. Connections from ChatGPT, Claude and other apps choose them on the consent page, and the directory endpoint (`\/mcp\/no-purchases`) only grants scopes that cannot spend.\n\n**Recommended pattern for AI agents.** Hand the agent its own dedicated API key with only the permissions it needs (often **Manage, but no purchases**), restricted to the domains it actually needs to manage and (if you know the agent's egress IP) to that IP. The blast radius of an accidentally-leaked key drops to `these operations on these domains from this IP` instead of `anything on the account`.\n\n## Idempotency\n\nAll v3 POST endpoints (excluding partner-only routes) accept an optional `Idempotency-Key` request header. When present, the API stores the response for 24 hours and replays it for any retry of the same request \u2014 so an agent that retries after a network blip will not double-charge or double-register.\n\n**Key format:** any non-empty string up to 255 characters. UUIDs work well; agents can also use their own internal request IDs.\n\n**Behavior on retry:**\n\n- **Same key, same request body** within 24h \u2192 returns the original response with header `Idempotent-Replayed: true`\n- **Same key, different request body** \u2192 returns 409 with `code: IDEMPOTENCY_KEY_MISMATCH` (catches accidental key reuse)\n- **Same key, original request still in flight** \u2192 returns 409 with `code: IDEMPOTENCY_KEY_IN_USE` (retry shortly)\n- **No header sent** \u2192 behavior is unchanged from before; no caching happens\n\n```bash\ncurl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/create\/example.com \\\n  -H \"Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890\" \\\n  -H \"X-API-Key: pk1_...\" -H \"X-Secret-API-Key: sk1_...\" \\\n  -d '{\"cost\":973,\"agreeToTerms\":\"yes\"}'\n```\n\n## Rate limiting\n\nSome endpoints are rate limited. When exceeded, the API returns HTTP 429 with a `RATE_LIMIT_EXCEEDED` body and a `Retry-After` header telling you how many seconds to wait before retrying (the same value as the `ttlRemaining` body field). Fixed-limit endpoints (`\/apikey\/request`, `\/apikey\/retrieve`) also return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers on every call. Availability checks also slow down accounts that check at a sustained scanning rate: about 1,800 names over three hours while registering fewer than one domain per 200 names checked, an account's checks share 50 names per 5 minutes for the next 24 hours (`RATE_LIMIT_EXCEEDED` with `reason: \"SUSTAINED_BULK_CHECKING\"`). Hosting has per-account velocity guards too \u2014 10 provisions\/hour (`\/hosting\/create`) and 20 WordPress credential mints\/hour (`\/hosting\/createWpCredentials`), both with the same headers; `dryRun` calls don't consume budget.\n\n## Webhooks\n\nWhat each event sends, the delivery headers and how to verify a signature are documented with the endpoints: see [Webhooks](#tag\/webhooks).\n\n## Guarantees\n\nExplicit contract points for autonomous clients. These are stable within the `v3` major version.\n\n- **Operation timing** \u2014 nearly every operation is synchronous and takes effect before the response returns. The one long-running operation is an inbound domain transfer (days): poll `GET \/domain\/getTransfer\/{domain}` or subscribe to the `domain.transfer.completed` webhook rather than blocking.\n- **Idempotency** \u2014 `Idempotency-Key` on any POST makes retries safe: the response is stored for **24 hours** and replayed for an identical body; a same-key\/different-body request returns `409 IDEMPOTENCY_KEY_MISMATCH`; an in-flight duplicate returns `409 IDEMPOTENCY_KEY_IN_USE`.\n- **Webhook delivery** \u2014 at-least-once. Deduplicate on the `X-Porkbun-Webhook-Id` (UUIDv7) \u2014 an event may be delivered more than once, and **ordering is not guaranteed**. Failed deliveries retry ~1m\/5m\/30m\/2h\/6h (6 attempts); the delivery log is retained ~30 days; an endpoint with 20 consecutive failures is auto-disabled.\n- **Error contract** \u2014 every error carries a stable `code` and (when known) a `next_action{type, hint, retryable, url?}`. Branch on `code` \/ `type` \/ `retryable`, never on `message` (messages may be reworded without a version bump; codes will not).\n- **Rate limiting** \u2014 a `429` always includes a `Retry-After` header in whole seconds.\n- **Backward compatibility** \u2014 the URL path stays `\/api\/json\/v3\/` for the life of v3. Minor bumps (tracked by `X-API-Version` and the Changelog below) are strictly additive \u2014 new endpoints, optional fields, error codes, or relaxed limits \u2014 and never remove or repurpose an existing field. Only a breaking change introduces a new major version and a new URL path.\n\n## Changelog\n\n### v3.68\n\n- **Hosting auto-renew.** `POST \/hosting\/updateAutoRenew\/{domain}` (`{\"autoRenew\": false}`) turns auto-renew off or on for any paid hosting product, the same switch as on the website. The response says what happens at `expireDate`.\n- A `PAYMENT_REQUIRED` response for `\"payWith\": \"card\"` now carries card-specific `next_action` steps (it used to lead with the USDC ones).\n\n### v3.67\n\n- **Link in Bio.** New `\/linkinbio` endpoints: `get` the page, `create` a free one, and edit it (`updateProfile`, `addLink`, `editLink`, `deleteLink`, `reorderLinks`, `updateSocials`, `updateAppearance`, `updateSettings`). Paid plans (`PIXIELINKSM1`, `PIXIELINKSY1`) are sold through `\/hosting\/create`, which upgrades a free page and keeps its content. The free Link in Bio and WordPress preview that come with a domain no longer block `\/hosting\/create` (it used to return `HOSTING_ALREADY_EXISTS`).\n\n### v3.66\n\n- **Primary and backup cards.** Accounts can save up to three cards on the website: one primary, the rest backups. `POST \/account\/topup` (and auto top-up) charges the primary and falls back to the backups when it is declined. The top-up response now says which card paid (`card`, `usedBackup`) and which were declined first (`declinedFirst`); a `CARD_DECLINED` error lists every card tried (`declined`). `GET \/account\/autoTopup` lists the saved cards, primary first, with their status (`cards`).\n\n### v3.65\n\n- **Date filters on `\/domain\/listAll`.** `addedAfter` \/ `addedBefore` (when the domain joined the account at Porkbun, by registration or transfer), `createdAfter` \/ `createdBefore` (registry creation date) and `expiresAfter` \/ `expiresBefore`. Each domain now includes `addedDate`, and `sortName` accepts `added_date`. To sync only new domains, pass the newest `addedDate` you have seen as `addedAfter`.\n\n### v3.64\n\n- **Email on an expired domain.** `\/email\/renewMailbox` returns `DOMAIN_EXPIRED` while the mailbox's domain is expired, and set-up mailboxes stop renewing automatically until the domain is renewed. Renew the domain with `\/domain\/renew` first.\n\n### v3.63\n\n- **DS records.** DNS records can be type `DS`, to delegate a subdomain to another DNS host with DNSSEC on. `content` is `key_tag algorithm digest_type digest` (e.g. `2371 13 2 1F987C...`); the digest must be 40, 64, or 96 hex characters for digest type 1, 2, or 4. A DS at the domain itself is refused: that one is set at the registry with `\/dns\/createDnssecRecord`.\n\n### v3.62\n\n- **Aftermarket search.** `GET \/aftermarket\/search` finds registered domains that are for sale (Afternic, Sedo, Atom, NameBright, DAX listings and auctions, Porkbun Marketplace) by keyword or exact name, with price, what the price means (`saleType`: buy now, minimum offer or current bid) and a `buyUrl` to buy on porkbun.com. `checkDomain` on a name registered to someone else now carries `aftermarketSearch`, so \"taken\" no longer reads as \"not for sale\".\n\n### v3.61\n\n- **Verify the email address from an agent.** `POST \/account\/verifyEmail\/send` emails the verification message (link plus code) to the address on the account, and `POST \/account\/verifyEmail\/confirm` checks the code, the email counterpart of `\/account\/verifyPhone`. New codes `EMAIL_CODE_INVALID`, `EMAIL_VERIFY_LIMIT`, `EMAIL_VERIFY_UNAVAILABLE`.\n- **Verification webhooks.** `account.verification.code_sent` (a code went to the email or phone, never the code itself) and `account.verification.completed` (with both verification states), from the website as well as the API, so an agent can wait for the person instead of polling.\n- **DNS webhook types.** `id`, `ttl` and `prio` in `dns.record.*` events are always strings, as in `\/dns\/retrieve`; some write paths sent numbers.\n\n### v3.60\n\n- **Asking for permissions when a key is created.** `\/apikey\/request` takes optional `scopes`; the approval page lists them in plain words, the account holder can grant those or less (never more), and the key gets what they granted. Connecting ChatGPT, Claude or another app through the hosted connector now asks which permissions the connection gets, and the directory endpoint (`\/mcp\/no-purchases`) can only grant permissions without `purchase` and `funding`, so its \"no purchases\" promise is enforced by the key itself.\n- **Permission scopes on API keys.** A key can be limited to `read` (see everything, change nothing), or to any of `dns`, `domains`, `email`, `hosting`, `purchase`, `funding` and `account` on top of it; \"Manage, but no purchases\" is every scope except `purchase` and `funding`. Existing keys keep full access. Each operation lists the scope it needs in `x-porkbun-scope`; a call outside the key's permissions returns 403 `INSUFFICIENT_SCOPE` with `requiredScope` and `keyScopes`, and nothing changes. A dry run of a purchase or top-up needs only `read`. `\/ping` returns the key's `scopes`. Set them in API Access on porkbun.com.\n- **First registrations over the API.** An account's first domain registration can be made over the API; the website is not required first. `ID_VERIFICATION_REQUIRED` and `NEW_ACCOUNT_LIMIT` are reserved for checks on new accounts that are measured but not currently enforced.\n\n### v3.59\n\n- **The spec declares retries and rate limits per operation.** Every authenticated POST lists the optional `Idempotency-Key` header (`components.parameters.IdempotencyKey`), every authenticated operation lists its 429 response with `Retry-After`, and success responses list the `X-RateLimit-*` headers, so tools generated from the spec know which calls are safe to retry and how to back off. Behaviour is unchanged.\n- **Email hosting mailboxes.** `GET \/email\/getMailboxes\/{domain}` lists them (with the price and mail client settings), `POST \/email\/setupMailbox\/{domain}` gives a waiting mailbox (already paid for, or the free trial) its address and password for free, and `POST \/email\/createMailbox\/{domain}` buys one ($36.00 a year) and sets it up in one call, paid from credit or with `payWith`. `POST \/email\/renewMailbox\/{domain}` adds a year now (set-up mailboxes also renew on their own when due; `getMailboxes` shows `autoRenews` and `renewalPrice`). A password is generated and returned once unless one is sent. Mail DNS is set up as for forwarding, asking before replacing another mail service (`DNS_CHANGE_CONFIRMATION_REQUIRED`). Same rules as the website: verified account, address rules, no address both a mailbox and a forward, no purchase for new accounts that paid in crypto. Not available on sandbox keys.\n- **API registrations get the free email hosting trial,** a mailbox waiting to be set up, like registrations on the website.\n\n### v3.58\n\n- **Email forwarding.** `GET \/email\/getForwards\/{domain}`, `POST \/email\/addForward\/{domain}` and `POST \/email\/deleteForward\/{domain}` manage forwards (an address at the domain delivered to another mailbox), with the same rules as the website: free up to the domain's limit, no catch-all, not on an address that is an email hosting mailbox. Adding the first forward sets up Porkbun's MX and SPF at the apex; when that would delete another mail service's MX records or rewrite an SPF record, it is refused with `DNS_CHANGE_CONFIRMATION_REQUIRED` and a `dnsChanges` description until resent with `confirmDnsChanges: true`. Deleting the last forward removes those records. Not available on sandbox keys.\n- **`\/email\/setPassword` honours per-key domain scoping.** A key restricted to certain domains could set mailbox passwords on the account's other domains, because the domain arrives in the body rather than the path.\n\n### v3.57\n\n- **Registering internationalized domain names (IDNs).** `\/domain\/create` takes `idnLanguage`: the language the name is written in, as a code or name from `idnLanguages` (any case). Registries need it for IDNs (Verisign refused .com and .net ones with \"Language tag required\"); an IDN sent without one, under a TLD that has languages, is now refused before any charge with `IDN_LANGUAGE_REQUIRED`, listing the accepted languages. `\/domain\/checkDomain` (single and bulk) returns `idnLanguages` for IDN names so the choice can be made up front. Ignored for plain ASCII names.\n- **Unicode names.** `\/domain\/create` and `\/domain\/checkDomain` accept a name in Unicode (`caf\u00e9.com`, percent-encoded in the path or not) and work on its punycode form (`xn--caf-dma.com`). Before, the non-ASCII characters were stripped and a different name was checked.\n\n### v3.56\n\n- **Which account am I in?** `\/ping` with credentials returns `account`, the username of the Porkbun account the key belongs to. `DOMAIN_NOT_FOUND`, `DOMAIN_NOT_REGISTERED`, `API_ACCESS_DISABLED` and the `\/dns\/*` \"not in your account\" `INVALID_DOMAIN` carry `account` too, so an agent holding keys for several accounts can tell a missing domain from the wrong key.\n- **Checking a domain you own.** `\/domain\/checkDomain` (and bulk checks) add `inYourAccount: true` and a `note` when the name is already in the caller's account: `price` is still the registration price, and renewal is `additional.renewal.price`.\n- **Card payment links that expire unpaid.** Repeating a purchase with a `cardPaymentId` whose `payUrl` passed `expiresAt` unpaid now answers `PAYMENT_EXPIRED` instead of returning the dead link; send the purchase without `cardPaymentId` for a fresh one. The `PAYMENT_REQUIRED` message also says what to do on a first-time Link CLI login (`link-cli auth login`) and names Link's MCP server for agents without a shell.\n- **Dry runs quote with `cost: 0`.** `\/domain\/create` and `\/domain\/renew` dry runs accept `cost: 0`, as documented (they refused it with `INVALID_INPUT`); `\/domain\/transfer` already did.\n\n### v3.55\n\n- **Pay for a purchase by card, with a real hold.** `\"payWith\": \"card\"` on `\/domain\/create`, `\/domain\/renew`, `\/domain\/transfer`, `\/closeout\/buy` or a paid `\/hosting\/create` answers HTTP 402 `PAYMENT_REQUIRED` with `method: \"card\"`, a single-use `payUrl` and a `cardPaymentId`; nothing is charged. Paying the `payUrl` over MPP with a Stripe Link agent wallet (the person approves in Link) **authorizes** the card for the exact price: a hold, not a charge. Repeat the purchase with `cardPaymentId`: if it succeeds the hold is captured, and if it fails the hold is released and nothing is charged. A hold nobody comes back for is released after an hour, and one whose repeat is refused for a reason a retry cannot fix (the name taken, the price changed, the account blocked) is released at once. No account credit is needed. The response carries `payment` (`paidWith: \"card\"`, or `charged: false` with `holdReleased` on failure).\n\n### v3.54\n\n- **Pay for a purchase directly in USDC (x402).** Send `\"payWith\": \"usdc\"` on `\/domain\/create`, `\/domain\/renew`, `\/domain\/transfer`, `\/closeout\/buy` or a paid `\/hosting\/create`. After every normal check passes, the first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's x402 terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with `PAYMENT-SIGNATURE`, and the purchase completes in that call, with the receipt in `PAYMENT-RESPONSE` and `payment` in the body. No account credit is needed. If the purchase fails after payment, the payment is kept as account credit, not returned to the wallet (`payment.keptAsCredit: true`, with `balance_cents`). The monthly spend limit is checked before any charge. A 402 releases its `Idempotency-Key`, so the paid repeat with the same key runs. New codes `PAYMENT_REQUIRED`, `PAYMENT_FAILED`, `PAYMENT_IN_PROGRESS`; new `next_action.type` `pay`.\n- **Pay-then-confirm, for wallets that pay a URL themselves** (a wallet MCP, Coinbase's `awal` CLI, or a person on the Coinbase page): pay the 402's `x402Url` or `payUrl`, then repeat the purchase with `\"usdcCheckoutId\"`. The API checks the checkout is this account's, for this exact purchase and price, paid and unused, then completes the purchase. New codes `PAYMENT_PENDING`, `PAYMENT_MISMATCH`, `PAYMENT_NOT_FOUND`, `PAYMENT_ALREADY_USED`, `PAYMENT_EXPIRED`. A paid purchase checkout nobody claims within an hour is added to the account's credit.\n\n### v3.53\n\n- **Sustained bulk checking is throttled.** An account checking about 1,800 names over three hours while registering fewer than one domain per 200 names checked gets 50 names per 5 minutes for the next 24 hours, on `\/domain\/checkDomain` (single and bulk) and the older v2 and reseller check endpoints alike (`RATE_LIMIT_EXCEEDED`, `reason: \"SUSTAINED_BULK_CHECKING\"`, `retryAfter`). Ordinary checking is untouched; accounts with 100+ registrations a year, active affiliates and per-key limits set by support are exempt.\n- **One monthly limit on everything the API does with money.** The account's monthly spend limit now covers every API purchase (registrations, renewals, transfers, closeouts and paid hosting) and, counted separately against the same figure, all credit added over the API: card top-ups, auto top-ups set off by an API purchase, MPP and USDC. Closeouts and paid hosting were checked against the limit but never counted toward it; they now do.\n- **A $100\/month default for accounts that have not set a limit.** It applies to purchases as well as top-ups now. Accounts that bought over the API before 2026-10-03 keep no default on purchases. `GET \/account\/apiSettings` returns `spendLimit` (`limit`, `source`: `account`, `default` or `none`, `spent`, `remaining`) and `topupLimit` (`limit`, `source`, `added`), the limits actually enforced.\n- **The check holds the amount for the length of the order,** so parallel orders cannot each pass against the same total and overshoot the limit together.\n- `MONTHLY_SPEND_LIMIT_EXCEEDED` now carries `monthlySpendLimit`, `limitSource`, `monthlySpendSoFar` and `cost`, and closeout dry runs return it too. `POST \/account\/topupCrypto` can return `TOPUP_LIMIT_EXCEEDED` (unpaid API checkouts from the last day count as if paid).\n- **Verify the phone from an agent.** `POST \/account\/verifyPhone\/send` texts or calls a code to the number on the account and `POST \/account\/verifyPhone\/confirm` checks it, so `VERIFICATION_REQUIRED` on a new account no longer means leaving the conversation. New codes `PHONE_CODE_INVALID`, `PHONE_VERIFY_UNAVAILABLE`, `PHONE_VERIFY_LIMIT`, `PHONE_VERIFY_FAILED`.\n- **New accounts can pay with USDC.** The 7-day account age no longer blocks `POST \/account\/topupCrypto`, so an account an agent just made can pay its first order in USDC.\n- **Small USDC top-ups.** `POST \/account\/topupCrypto` now takes $1 and up (was $5), and returns `estimatedFee_cents` and `estimatedCredit_cents`: Coinbase keeps about 1%, not the $0.01 these docs said. `INSUFFICIENT_FUNDS` and dry runs carry `usdcAmountToCover`, the checkout amount that still covers the shortfall after the fee, so an agent can buy a $2.50 domain with a $2.53 checkout.\n\n### v3.52\n\n- **Agents can top up with the user's card (MPP).** `POST \/account\/topupMpp` returns a signed, single-use link that an AI agent pays over the Machine Payments Protocol with a Stripe Shared Payment Token for the user's card (Stripe's Link Agent Wallet): HTTP 402 challenge, credential, credit added at once. No card needs to be saved at Porkbun. Same limits as card top-ups. New codes `MPP_NOT_AVAILABLE`, `MPP_UNAVAILABLE`.\n\n### v3.51\n\n- **Top up with USDC, payable by an AI agent (x402).** `POST \/account\/topupCrypto` opens a Coinbase checkout for account credit and returns `x402Url`, which an agent with a wallet pays directly (a gasless USDC transfer on Base), and `payUrl` for a person. `GET \/account\/topupCryptoStatus\/{checkoutId}` reports when it is credited. A way past `INSUFFICIENT_FUNDS` for accounts with no saved card. New codes `CRYPTO_NOT_AVAILABLE`, `CRYPTO_TOPUP_LIMIT`, `CRYPTO_CHECKOUT_FAILED`, `CHECKOUT_NOT_FOUND`.\n\n### v3.50\n\n- **German, Austrian and Swiss states in their own languages.** Contact `state` now accepts local names and ISO 3166-2 codes for DE, AT and CH (`Bayern`, `BY`, `Th\u00fcringen`\/`Thueringen`, `Wien`, `9`, `Gen\u00e8ve`, `GR`, ...) and stores the canonical name (`Bavaria`, `Vienna`, `Geneva`). Before, `Bayern` and `BY` returned `INVALID_STATE`.\n\n### v3.49\n\n- **`\/hosting\/deleteFile` is idempotent and says why.** Deleting a path that is already gone returns `SUCCESS` with `alreadyAbsent: true` instead of `HOSTING_DELETE_FAILED`. A non-empty directory returns the new `HOSTING_DIR_NOT_EMPTY` with its `contents`, and the new `recursive: true` deletes a directory with everything in it (up to 2,000 items per call). The old single error, \"missing, or a non-empty directory\", is gone.\n\n### v3.48\n\n- **`\/dns\/edit` edits only what you send.** Any field left out now keeps the record's current value. Before, the edit replaced the whole record: leaving out `name` moved a subdomain record to the apex and reset `ttl` and `prio`, and leaving out `type` failed. An explicitly empty `name` still means the apex.\n\n### v3.47\n\n- **`API_ACCESS_DISABLED` everywhere.** `\/dns\/*`, `\/ssl\/*`, `\/email\/*` and a batch path in `\/domain` answered a domain that is not opted in to API access without a stable `code` (you got one derived from the message text). They now return `API_ACCESS_DISABLED` with its `next_action`, like every other endpoint.\n\n### v3.46\n\n- **Invoice download links.** `GET \/account\/invoice\/{orderId}` and `GET \/account\/invoicePdf\/{orderId}` add `downloadUrl`, a link that downloads the PDF without signing in for 15 minutes (`downloadExpires`), so an integration or AI assistant can hand the user the file instead of reproducing it from base64. Each link is signed for one order and its account.\n\n### v3.45\n\n- **Invoices.** `GET \/account\/invoices` lists the account's invoices (filter by `year`, page with `start`\/`limit`); `GET \/account\/invoice\/{orderId}` returns one as data (bill-to, payment method, lines, gross\/refunded\/net totals); `GET \/account\/invoicePdf\/{orderId}` returns the PDF, base64-encoded, identical to Download PDF on porkbun.com. New codes `INVOICE_NOT_FOUND` and `INVOICE_PDF_FAILED`.\n\n### v3.44\n\n- **Glue and DNSSEC deletes are idempotent, as documented.** `deleteGlue` on a host that does not exist, and `deleteDnssecRecord` on a key tag (or domain) with nothing published, now return `SUCCESS` with a \"nothing to delete\" message instead of an error. A successful `deleteGlue` was also reported as `UPDATE_FAILED` (the registry answered success in a shape the code misread); fixed.\n- **`deleteDnssecRecord` matches the DS record by key tag.** It could remove the first key regardless of the key tag passed, or send a malformed removal when the key tag was not found.\n- **Registry reasons are passed through.** Glue create\/update\/delete failures (still `UPDATE_FAILED`) now carry the registry's reason, e.g. that an IP address is reserved. DNSSEC failures get stable codes: `DNSSEC_CREATE_FAILED`, `DNSSEC_DELETE_FAILED`, `DNSSEC_INFO_UNAVAILABLE`, each with a `next_action`.\n\n### v3.43\n\n- **`\/pricing\/get` filters by TLD on GET as well as POST.** `tld` (one) and\/or `tlds` (an array, or comma-separated on GET): `?tld=io`, `?tlds=com,io,dev`. Case, a leading dot and IDN form (Unicode or punycode) are all handled. Two bugs that returned `SUCCESS` with wrong data are fixed: `GET ?tlds=io` returned an empty `pricing`, and a TLD Porkbun does not sell was priced at $0.00. Unsold TLDs are now listed in a new `unsupported` array instead, and a filter with nothing sellable returns `INVALID_TLD` (HTTP 400).\n\n### v3.42\n\n- **`INSUFFICIENT_FUNDS` says whether a top-up can fix it.** Every insufficient-funds response (registration, renewal, transfer, closeout, hosting) and every short dry run now carries `topUpAvailable`: true when a card is saved on the account, so `POST \/account\/topup` can charge it and add the credit right now. When true, `topUpAmountToCover` is the smallest top-up that covers the order and `next_action.type` is the new **`top_up`**; when false it is `add_funds` with the website link. Before this, the message only pointed at the website, and a live agent told a user with a saved card to go and buy credit by hand.\n\n### v3.41\n\n- **`INVALID_STATE`**: on a TLD whose registry validates the contact's state (the .mx family today), a state we cannot match is refused before the command is built, and the error carries every accepted value for that country in `validStates`. Checked against real traffic before enabling: of 2,182 contacts .mx has accepted on our .mx domains, the gate blocks none.\n\n### v3.40\n\n- **Contact `state` is now normalised the way the website's dropdown does it.** The API used to uppercase it, which broke every contact update on `.mx`: the registry validates the field case sensitively and wants `Ciudad de M\u00e9xico` for Mexico City, and the translation table that produces that is keyed on the canonical name, so `MEXICO CITY` missed it and `NEW JERSEY` was refused outright. `state` is now matched against the country's official list by name or short code, ignoring case and accents.\n- **A contact the registry refuses no longer produces a broken update.** When contact creation failed, the domain update went out anyway with empty handles \u2014 the registry saw an add with no contact in it next to a real remove. It was refused here with an opaque syntax error; a more permissive registry would have stripped the domain's contacts and added none. The update is now abandoned and the response names what the registry actually objected to, with no contacts changed.\n\n### v3.39\n\n- **`POST \/account\/topup` takes an optional `amount`.** Omitted, it charges the configured amount exactly as before. Supplied (500\u201350000 cents), it charges that for one call without touching the stored setting \u2014 previously an agent that needed a different figure had to rewrite the customer's saved top-up amount first, which is a worse outcome than letting it name a number. The monthly ceiling and daily caps apply either way, and `amountSource` reports which figure was used.\n\n### v3.38\n\n- **The top-up amount is now a setting in its own right.** `POST \/account\/autoTopup` accepts `{\"amount\": <cents>}` with no `enabled`, and `enabled: false` no longer clears it \u2014 switching auto top-up off means stop firing on a threshold, not forget how much. `POST \/account\/topup` charges that same figure, so the two were never really separate settings. The website says the same thing: Account Settings \u2192 API now reads \"Top-Up Settings\" with \"Top-Up Amount\" always visible, and the trigger balance tucked under the Auto Top-Up switch.\n- **Top-ups are bounded in dollars, not just in frequency.** An account's monthly spend limit now caps what `POST \/account\/topup` can charge in a calendar month, and an account with no limit set gets a $100\/month ceiling rather than none. Domain spend is unchanged: an unset limit still means no cap there.\n\n### v3.37\n\n- **Account credit can be topped up over the API.** `POST \/account\/topup` charges the saved payment method and adds the credit immediately, so an agent that runs out mid-task can fund itself instead of stopping until a human visits the website. `GET|POST \/account\/autoTopup` reads and sets auto top-up, which until now could only be configured in the browser.\n- **The caller does not choose the amount.** A top-up charges the account's configured auto top-up amount, or $50 if none has ever been set. An amount set through the API is capped at $500; an amount the account holder sets on the website is honoured as-is. Top-ups are limited to 5 a day and 20 a month, only a payment method saved outside the API can be charged, and every successful charge emails the account holder.\n- New error codes: `NO_PAYMENT_METHOD`, `TOPUP_LIMIT_EXCEEDED`, `CARD_DECLINED`, `TOPUP_FAILED`, each with a `next_action`.\n\n### v3.36\n\n- **`INSUFFICIENT_FUNDS` now says what to do about it.** It used to be the two words \"No funds.\" The response now carries `cost`, `balance` and `shortfall` in cents, and a message naming the price, the balance, the gap, the fact that API purchases are paid from prepaid credit rather than a card on file, where to top up, and how to turn on auto top-up. Same treatment on `\/domain\/create`, `\/domain\/renew` and `\/domain\/transfer`.\n- **A dry run is no longer refused for being broke.** The funds check short-circuited `dryRun: true`, so the rehearsal that exists to tell you what an order needs was the one thing you could not get when the balance was short. A dry run now runs to completion and reports `wouldSucceed: false` with `shortfall`. It still charges nothing and still consumes no rate-limit budget.\n- **`dryRun` with `cost: 0` returns a quote on `\/domain\/create` and `\/domain\/renew`**, matching `\/domain\/transfer` and `\/closeout\/buy`. A caller who does not know the price no longer has to guess one in order to be told it is wrong.\n- **`COST_MISMATCH` is a code on all three endpoints** (registration previously returned an uncoded message) and the expected cost is named in cents and dollars.\n\n### v3.35\n\n- **`POST \/domain\/transfer\/{domain}` can now return `REGISTRANT_EMAIL_NOT_ACCEPTED`.** Some registries refuse registrant contacts that use temporary or encrypted email providers and suspend domains that use one; .in and its second levels are the first. The transfer is refused before any charge. Change the email on the default registrant contact for the account and retry. This concerns the contact recorded on the domain, not who hosts mail for it.\n\n### v3.34\n\n- **A DNS write to a domain that is not delegated to us now says so.** We keep a zone for every domain in your account whether or not the domain points at our nameservers, and a write to a zone nobody is querying used to return a bare `SUCCESS` -- stored by us, invisible to the world. Those responses now carry a `warnings` array naming the nameservers that actually answer for the domain and what to do about it. The write is still performed (the zone stays ready if the delegation comes back), and `status` is still `SUCCESS`, so this is additive: `warnings` is absent when there is nothing to say. Covers `\/dns\/create`, `\/dns\/edit`, `\/dns\/editByNameType`, `\/dns\/delete`, `\/dns\/deleteByNameType`, `\/dns\/import` and `\/dns\/restore`.\n- **`GET \/dns\/preflight\/{domain}` gained a `nameservers-ours` check**, reporting the same fact ahead of time, including the mixed case where only some of the delegated nameservers are ours and resolvers disagree.\n\n### v3.33\n\n- **`GET \/dns\/preflight\/{domain}`** reports whether a change is about to break a domain, before you change delegation, transfer it out or enable DNSSEC. Read-only. Every check in it comes from an incident: DNSSEC active during a nameserver change (which takes a domain dark rather than degrading it), CNAME exclusivity, more than one apex SPF record, an SPF chain over the ten-lookup limit, an apex MX pointing at localhost, and names that have stopped inheriting a wildcard because an MX or TXT record made them exist (RFC 4592). Findings split into `blockers` and `warnings`, each naming its rule and carrying a `next_action`.\n- **A second apex SPF record is now refused**, on create and on edit, with `SPF_CONFLICT`. Two SPF records at one name is a permerror under RFC 7208 4.5 and around 16,000 zones had drifted into it. The refusal carries the existing record id, its current content, and where the two can be safely combined the single merged record to write instead, so a caller fixes it in one step rather than hitting a wall.\n\n### v3.32\n\n- **Zone history: DNS finally has an undo.** `GET \/dns\/history\/{domain}` lists the versions of a zone we hold, `GET \/dns\/diff\/{domain}\/{id}` shows what changed against the live zone, and `POST \/dns\/restore\/{domain}` puts it back. Restore points are taken automatically before the first write to a zone in each hour, before any bulk import or wipe, and before any restore -- so recovering a mistaken DNS edit no longer depends on the customer remembering what the record said.\n- A restore adds back what is missing and leaves records you have added since alone; `prune: true` opts into removing them. The state before the restore is itself saved and returned as `previousStateSavedAs`, so a restore can be undone. `dryRun` is supported. Parking and other masked records cannot be recreated this way and come back in `failed` rather than being counted as restored.\n- New errors `RESTORE_POINT_NOT_FOUND`, `RESTORE_POINT_EMPTY`, `DNS_READ_FAILED`.\n\n### v3.31\n\n- **`securityLock`, `whoisPrivacy`, `autoRenew`, `apiAccess` and `notLocal` are now always integers** in `\/domain\/listAll` and `\/domain\/get\/{domain}`. They previously came back as the string `\"1\"` when the flag was on and the number `0` when it was off, so a single response mixed types for the same field. The spec has always documented all five as integers; this makes the response match. Reported by a customer. If you were comparing against `\"1\"` as a string, compare against `1`.\n\n### v3.30\n\n- Bulk checks are refused up front with `BULK_CHECK_TOO_SLOW` when a batch's TLD mix would need more sequential registry commands than one request can wait on, instead of blocking until the request is killed and the body truncated.\n- **Bulk availability checks**: `POST \/domain\/checkDomain` now takes a `domains` array (up to 25) and answers them in one call, keyed by domain. It has its own budget of **200 domains per 60 seconds**, counted per domain rather than per request, which is deliberately more generous per name than the single check because checks are chunked per registry and a batch really is less work than the same names one at a time. Read the three result lists: `domains` (answered), `invalid` (not checkable, one typo does not fail the call) and `unresolved` (the registry did not answer, which is **not** the same as unavailable). New `BULK_CHECK_TOO_MANY`.\n- **Availability checks raised from 1 per 10 seconds to 10 per 10 seconds** per account. Of the 15 API keys that had ever been given a per-key check override, every one raised the rate (to between 1\/s and 10\/s) and none lowered it, so the old default was a number that failed for anyone who noticed it. The window stays 10 seconds rather than becoming 1 per second at the same average, because an agent checking a list of candidate names sends a burst and then goes quiet.\n- **`\/domain\/checkSingleDomain` now honours the per-key check overrides.** It always shared one rate-limit bucket with `\/domain\/checkDomain`, but hardcoded 1-per-10s, so a key explicitly granted a higher rate still got the old limit through this endpoint, and the refusal came back as a 400 with no error code instead of a 429 with `Retry-After`. Both endpoints now run the same check.\n- **Corrected the documented attempt limit on `\/domain\/create` and `\/domain\/renew`**: it is 1 per second, not 1 per 10 seconds. The code has always been 1 per second; the spec was wrong.\n\n### v3.29\n\n- **DNSSEC values are checked against the registry's policy before the request is sent.** Registries are retiring the algorithms and digest types deprecated by RFC 9904\/9905\/9906 on their own schedules, so `\/dns\/createDnssecRecord` now validates per registry rather than globally. A value the registry has already stopped accepting is refused up front with `DNSSEC_ALGORITHM_DEPRECATED`, naming the field and a replacement, instead of being relayed as an opaque registry `2306`. A value that still works but is on its way out comes back in `warnings` on an otherwise successful create. Nothing changes for `\/dns\/getDnssecRecords` or `\/dns\/deleteDnssecRecord`: existing records keep resolving and can always be deleted, whatever they are signed with.\n\n### v3.28\n\n- **Registration, renewal and transfer success limits raised from 50 to 1000 per 24 hours** per account. The attempt limit (1 per second) is unchanged, and both remain configurable per API key with current values returned in the `limits` field. The old ceiling predated bulk agent use and was being hit by legitimate portfolio work.\n- **`X-RateLimit-Limit`, `-Remaining`, `-Reset` and `-Window` headers are now returned on every authenticated response**, reflecting a general per-API-key request budget (currently 20 requests per 2 seconds, i.e. 10\/second sustained with room for a burst). A second, per-account budget applies across all of an account's keys. These are **not being enforced yet**: the counters run and the headers are accurate, but no request is rejected, and responses carry `X-RateLimit-Mode: observe` while that is true. Build against the headers now; enforcement will be announced before it is switched on. The existing per-operation limits are unaffected and remain stricter where they apply.\n- **New `ZONE_RECORD_LIMIT` error on DNS creates.** A zone is now capped at 2,500 records. The limit is enforced in the shared DNS write path, so it applies identically to the API, the website and internal jobs, and it is checked after the duplicate test: re-creating a record that already exists adds nothing to the zone and is still answered by `DUPLICATE_RECORD`. The response carries `count` and `limit`. Measured against live data, 14 zones hold more than 500 records and only 2 hold more than 2,500, so no ordinary zone is affected.\n\n### v3.27\n\n- **Closeout endpoints are now denied to sandbox keys** (`SANDBOX_UNSUPPORTED`), alongside hosting and email. The inventory and the claim both live with a third party that has no test environment, so a sandbox purchase would have deducted fake credit and then claimed a real domain. Use `dryRun` with a live key to rehearse.\n\n### v3.26\n\n- **Fixed `sortDirection` on `closeout\/search?sortName=registrationDate`**, which was inverted: `asc` returned the newest registrations instead of the oldest. The provider orders that field by age rather than by date, so its codes run the opposite way round to every other sort it offers. `asc` now means earliest registration first, which is the whole point of sorting by it.\n- Ascending on `revenue`, `visitors` or `inboundLinks` returns the rows with no recorded figure first, because the provider stores \"unknown\" as a sentinel that sorts below every real value. The response now carries a `warnings` entry saying so rather than looking like a broken sort.\n\n### v3.25\n\n- **Closeout eligibility loosened to match domain registration**, plus verified email and phone. The account-age minimum and the prior-qualifying-order requirement are gone, on the web as well as the API: both were auction-grade rules that did not fit a fixed-price buy-it-now, where the listing is first-come and often gone within minutes. Accounts support has blocked from auctions and closeouts are still blocked, and auction *bidding* keeps the original rules.\n\n### v3.24\n\n- **Closeouts are now searchable and buyable over the API**: `GET \/closeout\/search`, `GET \/closeout\/get\/{domain}` and `POST \/closeout\/buy\/{domain}`. Closeouts are expired domains offered at a fixed, descending price with no bidding.\n- Search exposes every filter the inventory supports \u2014 keyword, TLD, exact name length, age range, price range \u2014 and sorts by domain, end time, price, revenue, visitors, inbound links or **registration date**. Age and registration date are returned on every row. The website paginates by price tier and does not show registration date, so finding aged names there means walking several tier pages and running a WHOIS per candidate; `sortName=registrationDate&sortDirection=asc` does it in one call.\n- Buying charges account credit and claims the name in one call, against an exact `cost` you took from `\/closeout\/get\/{domain}`. Every post-charge failure refunds automatically and reports `refunded: true`, including losing the race to another buyer. The name is reserved rather than delivered \u2014 the provider releases it over the following days, so poll `\/domain\/listAll` or use the `domain.registered` webhook.\n\n### v3.23\n\n- **Newly registered domains are now transfer-locked at the registry, as they always should have been.** The registration path carried a lock step whose guard tested a variable nothing ever assigned, so it was dead code and the lock never ran: sampling found `.com` registrations still carrying no registry status ten days later. A domain registered through `\/domain\/create` now comes back with the registrar lock applied (`clientTransferProhibited` and friends), which is what inbound transfers have always done. Unlock it with the usual controls if you intend to transfer it away. TLDs that do not support registrar locks (.de, .eu, the .au family) are unaffected.\n\n### v3.22\n\n- **Fixed: a domain could only ever be transferred in once through the API.** `transfers_in` is unique on (domain, transfer_id) and this path passed a literal `0`, so the first API transfer of a domain claimed that pair permanently and every later attempt failed with a bare \"Unable to initiate transfer.\" \u2014 including a legitimate retry after fixing whatever the losing registrar objected to. It now mints a transfer batch id the way the website does. Previously-blocked domains are freed without any change to their records.\n- Both failure paths now return **`TRANSFER_INIT_FAILED`** with a `next_action`, and say the order was refunded automatically and that the cause is not credentials or account verification.\n- **Documented the no-downtime transfer flow, which was live but absent from this spec**: `holdForDnsSetup` on `\/domain\/transfer`, plus `\/domain\/getTransferSetup\/{domain}`, `\/domain\/prepareTransfer\/{domain}`, `\/domain\/startTransfer\/{domain}`, `\/domain\/cancelTransfer\/{domain}` and `\/domain\/updateTransferAuthCode\/{domain}`. Hold the transfer, build the zone, import the records, then release \u2014 so the domain never moves to an empty zone.\n\n### v3.21\n\n- **`GET \/dns\/scan\/{domain}` and `POST \/dns\/import\/{domain}`: keep DNS working through a registrar transfer.** A transfer moves the delegation only, so the losing registrar's records vanish the moment it stops answering for the zone \u2014 and a domain whose records were never recreated goes dark right then. `\/dns\/scan` reads what a domain currently publishes while its old nameservers still answer. `\/dns\/import` bulk-creates records, either from an exact list you supply or from a scan, and is idempotent: records that already exist come back as `skipped` rather than failures, so it is safe to re-run.\n- Porkbun now also snapshots a pending inbound transfer's published zone automatically and restores it once the transfer completes, if the Porkbun zone is still empty at that point. A zone somebody has already populated is never overwritten.\n\n### v3.20\n\n- **`\/dns\/create` no longer reports SUCCESS for a record it refused to create.** A refusal from the DNS layer was being written into the `id` field while the status stayed `SUCCESS`, so a duplicate returned `{\"status\":\"SUCCESS\",\"id\":{\"error\":1,\"message\":\"...\"}}` \u2014 `id` is documented as a string. Every refusal now returns HTTP 400 with a proper code. This also fixes the more serious case of the same bug: a CNAME conflict (RFC 1034) was detected, the write refused, and `SUCCESS` returned anyway.\n- **New `DUPLICATE_RECORD`**, which carries `existingId` \u2014 the id of the record that already holds that exact name\/type\/content \u2014 so you can adopt it without a second lookup.\n- **New `RECORD_CONFLICT`**, which carries `conflictingRecords` (id, name, type, content) for the records that block the write.\n- **`\/dns\/edit` and `\/dns\/editByNameType` no longer fail when the record already matches.** Re-applying the current values reported \"We were unable to edit the DNS record\" because the underlying UPDATE changed no rows. A no-op edit is now a success, so converge loops that write desired state unconditionally work.\n- Documented that nameservers are an **unordered set**: `\/domain\/getNs` reads live from the registry and the registry may return them in any order, so it will not necessarily echo the order sent to `\/domain\/updateNs`.\n\n_Thanks to David Lazar, who reported the `\/dns\/create` response shape and the nameserver ordering question while building a Terraform provider._\n\n### v3.19\n\n- **Cloudflare connect: a domain that stops resolving mid-move is restored immediately, and the activation deadline is now 72h.** The two waits are bounded separately: a move that is still resolving gets the full 72 hours (a real activation has taken 59), while one whose domain has gone dark \u2014 delegation on Cloudflare, Cloudflare not serving it, Porkbun's zone retired \u2014 has its nameservers put back within minutes and is reported `undone`. A `failed` row that was waiting on Cloudflare is re-checked for up to 30 days, so a late activation is reported as connected instead of staying failed.\n- **Cloudflare connect: large batches now pace zone creation.** Cloudflare provisions each new zone from a queue that cannot be inspected or hurried, and creating faster than it drains leaves an account holding domains Cloudflare will neither set up nor accept more alongside (`1118`). Zone creation therefore pauses while the account has several domains awaiting activation, and the remaining rows stay `queued` with a message explaining the wait rather than failing.\n- **Cloudflare connect: a new non-terminal queue status `setup`, and automatic recovery from a stalled activation.** A move now waits in `setup` until Cloudflare has actually provisioned the zone, instead of repointing the registry nameservers at a zone Cloudflare has not finished setting up \u2014 which could leave a domain resolving from nobody once Cloudflare retired the old Porkbun zone. Waiting in `setup` changes nothing for anyone resolving the domain. If a move does time out in `activating` while the domain has stopped resolving, the nameservers are restored to Porkbun automatically and the row is reported as `undone` with the reason in `message`.\n- **Webhook delivery targets are now validated against the public internet.** A webhook URL must be `https:\/\/` on port 443 with a hostname that resolves to a public address; private, loopback, link-local, CGNAT and reserved ranges are refused with `INVALID_WEBHOOK_URL`, and credentials in the URL are refused (verify the `X-Porkbun-Signature` HMAC instead). The check runs again immediately before every delivery and the connection is pinned to the address that passed, so DNS that changes after registration cannot redirect a delivery. A hostname that does not resolve yet is still accepted at registration time, so you can create the endpoint before deploying the receiver.\n- Transport failures in `lastError` are now reported as a class (\"The endpoint could not be reached.\", \"...did not respond in time.\") rather than the raw socket error. HTTP status failures are unchanged and still reported as `HTTP <code>`.\n\n### v3.18\n\n- **Cloudflare connect over the API.** Also: `\/cloudflare\/preview\/{domain}` (what a move would copy), `\/cloudflare\/getRecords\/{domain}` (live records at Cloudflare), `\/cloudflare\/getZone\/{domain}` (Cloudflare's own zone state + nameserver-drift detection), `\/cloudflare\/getProxy`\u2192`setProxy` orange-cloud control, `cloudflare.connect.completed`\/`.failed` webhooks so you needn't poll, and a `warnings` entry on every `\/dns\/*` response for a domain whose DNS Cloudflare now serves. Move domains into a customer's own Cloudflare account programmatically: `\/cloudflare\/inventory` (eligibility), `\/cloudflare\/connect` (queue, `dryRun`-able), `\/cloudflare\/getQueue` + `\/cloudflare\/get\/{domain}` (progress), `\/cloudflare\/retry\/{domain}`, `\/cloudflare\/rollback\/{domain}` (undo), `\/cloudflare\/disconnect`, plus `\/cloudflare\/getProxy\/{domain}` and `\/cloudflare\/setProxy\/{domain}` to control the orange cloud (the move itself always imports DNS-only). The one-time Cloudflare authorization stays a browser action; `GET \/cloudflare\/getConnection` is the poll target for it.\n\n\n### v3.17\n\n- **Cloud for WordPress is API-provisionable.** `POST \/hosting\/create\/{domain}` now accepts the `CLOUDWORDPRESS\u2026` SKUs (see `GET \/hosting\/plans`) to spin up a managed WordPress site, same trial\/cost-acknowledgement rules as static hosting.\n- **Per-key domain allowlists now cover `\/hosting\/*`.** A key restricted to specific domains can no longer provision hosting or mint WordPress credentials for a different domain in the account (`DOMAIN_NOT_ALLOWED`). Previously only `\/domain\/*`, `\/dns\/*` and `\/ssl\/*` were enforced.\n- **WordPress REST credentials for agents.** `POST \/hosting\/createWpCredentials\/{domain}` mints a WordPress Application Password (returned once) so an agent can drive the site via `\/wp-json\/`; defaults to a least-privilege `editor` user, with `administrator` gated behind `acknowledgeFullAccess`. List with `GET \/hosting\/getWpCredentials\/{domain}`, revoke with `POST \/hosting\/deleteWpCredentials\/{domain}`.\n\n### v3.16\n\n- **App-return redirect on the PKCE key handoff.** `POST \/apikey\/request` accepts an optional `returnUrl` (HTTPS Universal Link \/ App Link, requires `codeChallenge`). After the user approves in the system browser, Porkbun redirects back to the app with `status` + `requestToken`; the key is still delivered only via `\/apikey\/retrieve` (PKCE), never in the redirect. Lets a native\/mobile app send a user through normal Porkbun signup + approval and get control back automatically. The approval window is 30 minutes (accommodates new-user signup + email verification); the post-approval \/apikey\/retrieve window stays 10 minutes.\n\nThe API URL path stays at `\/api\/json\/v3\/`. The version below (`major.minor`, also on the `X-API-Version` response header) tracks backward-compatible additions \u2014 new endpoints, new optional fields, new error codes, relaxed limits. Existing integrations are never broken by a minor bump; only a breaking change would introduce a new major version (and a new URL path).\n\n### v3.15\n\n- **Credential-free mock server.** `GET \/mock` and `GET|POST \/mock\/<path>` return schema-accurate example responses for any endpoint with no key required (`?status=error` for the error shape).\n- **Webhooks in the sandbox.** Sandbox operations emit signed webhook events, and `POST \/sandbox\/triggerWebhook` fires any event type on demand so you can test your handler end-to-end.\n- **`Retry-After` on 429.** Every rate-limited response now sets a `Retry-After` header (whole seconds until the window resets) and returns HTTP 429 with `code: RATE_LIMIT_EXCEEDED`.\n- **Agent metadata & explicit guarantees.** Every operation now carries an `x-porkbun-agent` extension (`safe`\/`cost`\/`destructive`\/`reversible`\/`requiresConfirmation`; `requiresConfirmation` is set for every billable or destructive operation \u2014 including a hosting deploy, which overwrites live public content); error `next_action` objects gained a `retryable` boolean; and a new **Guarantees** section documents idempotency retention, webhook delivery\/ordering, the error contract, and the backward-compatibility policy.\n\n### v3.14\n\n- **Sandbox \/ test mode.** Use a `pk1_sb_` API key to run the whole API against an isolated sandbox (fake credit, no real registry\/DNS\/charges); responses carry `sandbox:true`. New `POST \/sandbox\/topup` and `POST \/sandbox\/reset`. Hosting\/email return `SANDBOX_UNSUPPORTED` in sandbox. See \u201cSandbox \/ test mode\u201d above.\n\n### v3.13\n\n- **Provision hosting by SKU.** `POST \/hosting\/create\/{domain}` now takes a single `sku` (from `GET \/hosting\/plans`) instead of `product` + `plan` \u2014 one identifier to pass, and new hosting products\/plans can be offered without a breaking request change.\n- **`POST \/hosting\/makeDir\/{domain}`** \u2014 create a directory (and missing parents) explicitly. Deploy already auto-creates the directories in a file\u2019s `path`, so this is for standing up an empty directory.\n\n### v3.12\n\n- **Static site hosting via the API.** New **Hosting** endpoints provision and deploy Secure Static Hosting: `GET \/hosting\/plans` (discover provisionable plans + prices), `POST \/hosting\/create\/{domain}` (15-day free trial on a domain\u2019s first provision, auto-renews at the plan price; a re-provision after deprovision is charged to account credit \u2014 one free trial per domain), `GET \/hosting\/get\/{domain}`, `POST \/hosting\/deploy\/{domain}` (upload base64 files, \u226410\u00a0MB\/request, static types only), `GET \/hosting\/files\/{domain}`, `POST \/hosting\/deleteFile\/{domain}`, and `POST \/hosting\/delete\/{domain}` (deprovision). Provisioning switches the domain to Porkbun nameservers (gated behind `agreeToNameserverChange`) and requires `acknowledgedCost`. New codes: `COST_ACKNOWLEDGMENT_REQUIRED`, `NAMESERVER_CHANGE_REQUIRED`, `HOSTING_ALREADY_EXISTS`, `HOSTING_NOT_FOUND`, `HOSTING_NOT_READY`, `HOSTING_PROVISION_FAILED`, `FILE_TOO_LARGE`.\n\n### v3.11\n\n- **Address validation for contact edits.** A registrant change via `POST \/domain\/updateContacts` for a TLD that requires a validated address (.de\/.nrw\/.uk\/.us\/.ca\/.nyc\/.au\/.eu\/.in\/.nz families) now runs Google Address Validation (per-account rate-limited + 24h cached) instead of being blocked. If the address needs correction the call returns `ADDRESS_VALIDATION_REQUIRED` with a `suggestedAddress`; re-submit with `addressValidationChoice` = `accept_suggestion` or `use_as_entered`. For .de, DENIC registry verification is then auto-attempted (address fingerprint \/ email); a proof-of-address document upload, if still required, is completed at porkbun.com. Only a .au registrant name\/org change stays `REGISTRANT_CHANGE_NOT_SUPPORTED` (paid ownership trade).\n\n### v3.10\n\n- **Edit domain contacts.** New `GET \/domain\/getContacts\/{domain}` returns the four contacts (registrant\/admin\/tech\/billing), and `POST \/domain\/updateContacts\/{domain}` edits them. Send a `contacts` object with any subset of roles (unspecified roles are left unchanged) or a single `contact` applied to all four. Mirrors the website: pushes to the registry on thick TLDs and a registrant change fires the same new-owner notice\/verification email (no 60-day lock). Supports `dryRun`. A registrant name\/org change on .au and any registrant change on address-validation TLDs (.de\/.nrw) return `REGISTRANT_CHANGE_NOT_SUPPORTED` (do those at porkbun.com); admin\/tech\/billing edits still work.\n\n### v3.9\n\n- **Smoother agent key handoff (PKCE)** - `POST \/apikey\/request` now accepts an optional PKCE `codeChallenge` (RFC 7636, S256). When supplied, the account holder just approves in the browser (nothing to copy) and `POST \/apikey\/retrieve` returns BOTH the public and secret keys once to the caller presenting the matching `codeVerifier` - the key is minted lazily at retrieve time, so the secret is never shown in the browser or persisted. Omit `codeChallenge` for the unchanged legacy flow (public key only; secret shown in the browser). Adds response field `deliveryMode` and codes `CODE_VERIFIER_REQUIRED`, `INVALID_CODE_VERIFIER`, `SECRET_ALREADY_CLAIMED`.\n\n### v3.8\n\n- **URL forwarding \u2014 full web-dashboard parity.** `POST \/domain\/addUrlForward` now accepts `masked` forwards and an optional `redirectType` (`301`\/`302`\/`307`\/`masked`) to select the exact redirect code \u2014 this is how you create a 307 temporary redirect or a masked forward via the API. `redirectType` takes precedence over `type` and defaults to 302 for temporary. `GET \/domain\/getUrlForwarding` now returns `redirectType` (the exact stored code) alongside `type`, so 302 and 307 are distinguishable. Backward compatible \u2014 `type`-only requests are unchanged.\n\n### v3.7\n\n- **Clearer auth errors** \u2014 a request with a valid API key but a missing or misnamed secret now returns `MISSING_SECRETAPIKEY` with an explicit message (the field is `secretapikey`, not `secretkey`) instead of the generic `INVALID_API_KEYS_002`. A genuinely wrong secret still returns the deliberately-vague `INVALID_API_KEYS_002`, and auth errors now carry `next_action` hints.\n\n### v3.6\n\n- **Validate-only writes** \u2014 `dryRun: true` now works beyond the billable endpoints: DNS record writes (`\/dns\/create`, `\/dns\/edit`, `\/dns\/editByNameType`, `\/dns\/delete`, `\/dns\/deleteByNameType`) and nameserver updates (`\/domain\/updateNs`) validate and return `wouldSucceed` without mutating anything. Rehearse a destructive change before applying it.\n\n### v3.5\n\n- **Actionable errors** \u2014 most error responses now include a `next_action` object (`{type, hint, url?}`) telling an agent how to recover (e.g. re-quote the price, enable API access, add funds, register on the website). `type` is a small stable vocabulary; branch on it instead of string-matching messages.\n\n### v3.4\n\n- **TLD registration requirements as JSON Schema** \u2014 `GET \/domain\/getRegistrationRequirements\/{tld}` returns whether a TLD is API-registerable, the `\/domain\/create` body as a JSON Schema, and (for TLDs with registry eligibility rules like .us\/.ca) a second schema enumerating the required fields and allowed values. Lets an agent validate a registration before attempting it.\n\n### v3.3\n\n- **Outbound webhooks** \u2014 register HTTPS endpoints that Porkbun POSTs signed JSON to when lifecycle events occur (`domain.registered`, `domain.renewed`, `domain.transfer.completed`, `domain.expiring`, `dns.record.created|updated|deleted`). Manage them under the **Webhooks** endpoints below or at `porkbun.com\/account\/api`. Each delivery is signed with HMAC-SHA256 \u2014 see the **Webhooks** section above.\n\n### v3.2\n\n- **Dry run** \u2014 `dryRun: true` on register\/renew\/transfer previews availability, cost, balance, and `wouldSucceed` without charging or creating anything.\n- **LLM-readable docs** \u2014 full API reference as flat Markdown at `\/llms-full.txt`, plus per-topic pages indexed at `\/llms`, generated from this spec; refreshed `\/llms.txt` index; non-JavaScript fallback on the docs page so agents can read it.\n\n### v3.1\n\n- **Idempotency** \u2014 `Idempotency-Key` request header on POST endpoints; retries within 24h replay the original response instead of re-charging.\n- **Request IDs** \u2014 `X-Request-Id` response header and `requestId` body field on every response.\n- **Version signalling** \u2014 `X-API-Version` response header.\n- **Domain transfers** \u2014 `transfer`, `getTransfer`, and `listTransfers` endpoints for inbound transfers via API.\n- **Single-domain lookup** \u2014 `GET \/domain\/get\/{domain}` plus new filters on `listAll` (`domain`, `nameContains`, `tlds`, `expiringWithinDays`, `autoRenew`, `apiAccess`, `sortName`, `sortDirection`).\n- **Marketplace filtering** \u2014 server-side `query`, `tlds`, `sldLengthMin`\/`Max`, and sort parameters on `marketplace\/getAll`.\n- **Account endpoints** \u2014 `GET \/account\/balance` and `GET \/account\/apiSettings`.\n- **Spend controls** \u2014 per-account monthly spend limit, low-balance alert, and auto top-up, enforced on registrations\/renewals\/transfers.\n- **Per-key restrictions** \u2014 each API key can be scoped to specific source IPs (with CIDR support) and\/or specific target domains (`IP_NOT_ALLOWED` \/ `DOMAIN_NOT_ALLOWED`).\n- **DNS record types** \u2014 `HTTPS`, `SVCB`, and `SSHFP` documented as supported types.\n- **Machine-readable error codes** \u2014 every error response includes a `code` field.","contact":{"name":"Porkbun Support","url":"https:\/\/porkbun.com\/support"},"x-logo":{"url":"https:\/\/porkbun.com\/img\/logo.png"}},"servers":[{"url":"https:\/\/api.porkbun.com\/api\/json\/v3","description":"Production"},{"url":"https:\/\/api-ipv4.porkbun.com\/api\/json\/v3","description":"Production (IPv4 only \u2014 use this if your client is on an IPv6 network and needs to reach the API over IPv4)"}],"paths":{"\/ping":{"post":{"tags":["Utility"],"summary":"Test credentials and get caller IP","description":"Returns the caller's public IP address. Optionally validates API credentials.\n\n- **No credentials supplied** \u2014 returns IP only.\n- **Valid credentials supplied** \u2014 returns IP with `credentialsValid: true`.\n- **Invalid credentials supplied** \u2014 returns an error.\n\nUseful for agents and clients to verify their API key is working before making other calls.","operationId":"ping","requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"apikey":{"type":"string","description":"Your API key (optional)"},"secretapikey":{"type":"string","description":"Your secret API key (optional, required if apikey is supplied)"}}}}}},"responses":{"200":{"description":"Success \u2014 IP returned, credentials validated if supplied","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/IpResponse"},{"type":"object","properties":{"credentialsValid":{"type":"boolean","description":"Present and true when valid credentials were supplied"},"account":{"type":"string","description":"Username of the Porkbun account the key belongs to. Present when valid credentials were supplied"},"scopes":{"type":"array","items":{"type":"string"},"description":"The key's permission scopes, or [\"full\"] for full access. Present when valid credentials were supplied","example":["read","dns"]}}}]}}}},"400":{"description":"Invalid credentials","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/ping \\\n  -H 'Content-Type: application\/json' \\\n  -d '[]'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Utility"],"summary":"Test credentials and get caller IP","description":"Returns the caller's public IP address. Optionally validates API credentials.\n\n- **No credentials supplied** \u2014 returns IP only.\n- **Valid credentials supplied** \u2014 returns IP with `credentialsValid: true`.\n- **Invalid credentials supplied** \u2014 returns an error.\n\nUseful for agents and clients to verify their API key is working before making other calls.","operationId":"pingGet","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Success \u2014 IP returned, credentials validated if supplied","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/IpResponse"},{"type":"object","properties":{"credentialsValid":{"type":"boolean","description":"Present and true when valid credentials were supplied"},"account":{"type":"string","description":"Username of the Porkbun account the key belongs to. Present when valid credentials were supplied"},"scopes":{"type":"array","items":{"type":"string"},"description":"The key's permission scopes, or [\"full\"] for full access. Present when valid credentials were supplied","example":["read","dns"]}}}]}}}},"400":{"description":"Invalid credentials","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/ping'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/ip":{"post":{"tags":["Utility"],"summary":"Get caller IP address","description":"Returns the caller's public IP address. No credentials required. Use the `api-ipv4.porkbun.com` hostname if you need to force an IPv4 address.","operationId":"ipPost","requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Caller IP address","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/IpResponse"}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/ip \\\n  -H 'Content-Type: application\/json' \\\n  -d '[]'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Utility"],"summary":"Get caller IP address","description":"Returns the caller's public IP address. No credentials required. Use the `api-ipv4.porkbun.com` hostname if you need to force an IPv4 address.","operationId":"getIp","parameters":[],"responses":{"200":{"description":"Caller IP address","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/IpResponse"}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/ip'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/pricing\/get":{"post":{"tags":["Pricing"],"summary":"Retrieve domain pricing (public)","description":"Retrieve default domain pricing information for all supported TLDs, or only the ones you ask for. Does not require authentication. Prices are in US dollars.\n\nFilter with `tld` (one TLD) and\/or `tlds` (several). Names are case-insensitive, a leading dot is ignored, and IDN TLDs may be sent in Unicode or punycode (`\u307f\u3093\u306a` or `xn--q9jyb4c`). A TLD Porkbun does not sell is left out of `pricing` and listed in `unsupported`; if none of the requested TLDs is sold the response is `INVALID_TLD` (HTTP 400). No filter returns every TLD.","operationId":"getPricing","requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"tld":{"type":"string","description":"Optional single TLD to price, e.g. `io`. Combines with `tlds`.","example":"io"},"tlds":{"oneOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}],"description":"Optional TLDs to price: an array, or a comma-separated string. If neither `tld` nor `tlds` is given (or both are empty), all supported TLDs are returned.","example":["com","io"]}}}}}},"responses":{"400":{"description":"`INVALID_TLD`: a filter was given and none of the requested TLDs is sold. The message names them.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"200":{"description":"Pricing data","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"unsupported":{"type":"array","items":{"type":"string"},"description":"Present only when a filter named TLDs Porkbun does not sell: those names, as sent. They are not in `pricing`.","example":["nope"]},"pricing":{"type":"object","description":"Object keyed by TLD string","additionalProperties":{"type":"object","properties":{"registration":{"type":"string","example":"9.73"},"renewal":{"type":"string","example":"9.73"},"transfer":{"type":"string","example":"9.73"},"specialType":{"type":"string","description":"Present only for special TLDs (e.g. 'handshake')"},"coupons":{"type":"object","description":"Active coupon codes for this TLD, keyed by product type","additionalProperties":{"type":"object","properties":{"code":{"type":"string"},"max_per_user":{"type":"integer"},"first_year_only":{"type":"string","enum":["yes","no"]},"type":{"type":"string"},"amount":{"type":"number"}}}}}}}}}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/pricing\/get \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"tlds\":[\"com\",\"io\"]}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Pricing"],"summary":"Retrieve domain pricing (public)","description":"Retrieve default domain pricing information for all supported TLDs, or only the ones you ask for. Does not require authentication. Prices are in US dollars.\n\nFilter with `tld` (one TLD) and\/or `tlds` (several). Names are case-insensitive, a leading dot is ignored, and IDN TLDs may be sent in Unicode or punycode (`\u307f\u3093\u306a` or `xn--q9jyb4c`). A TLD Porkbun does not sell is left out of `pricing` and listed in `unsupported`; if none of the requested TLDs is sold the response is `INVALID_TLD` (HTTP 400). No filter returns every TLD. On GET, `tlds` is comma-separated: `?tlds=com,io,dev`.","operationId":"getPricingGet","parameters":[{"name":"tld","in":"query","required":false,"schema":{"type":"string"},"description":"One TLD to price, e.g. `io`.","example":"io"},{"name":"tlds","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated TLDs to price, e.g. `com,io,dev`. Omit (with `tld`) for every TLD.","example":"com,io,dev"}],"responses":{"400":{"description":"`INVALID_TLD`: a filter was given and none of the requested TLDs is sold. The message names them.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"200":{"description":"Pricing data","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"unsupported":{"type":"array","items":{"type":"string"},"description":"Present only when a filter named TLDs Porkbun does not sell: those names, as sent. They are not in `pricing`.","example":["nope"]},"pricing":{"type":"object","description":"Object keyed by TLD string","additionalProperties":{"type":"object","properties":{"registration":{"type":"string","example":"9.73"},"renewal":{"type":"string","example":"9.73"},"transfer":{"type":"string","example":"9.73"},"specialType":{"type":"string","description":"Present only for special TLDs (e.g. 'handshake')"},"coupons":{"type":"object","description":"Active coupon codes for this TLD, keyed by product type","additionalProperties":{"type":"object","properties":{"code":{"type":"string"},"max_per_user":{"type":"integer"},"first_year_only":{"type":"string","enum":["yes","no"]},"type":{"type":"string"},"amount":{"type":"number"}}}}}}}}}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/pricing\/get'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/apikey\/request":{"post":{"tags":["API Key Management"],"summary":"Initiate an API key authorization request","description":"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`.\n\n**Rate limit:** 20 requests per IP per 3600 seconds.\n\nSANDBOX: pass `sandbox: true` to skip approval and get a sandbox key pair back immediately (both `apikey` and `secretapikey` in the response, plus `sandbox: true`).","operationId":"apikeyRequest","x-ratelimit":{"requests":20,"window":3600,"windowUnit":"seconds","scope":"ip","note":"Counted per originating IP address. The Redis key is API:apikeyRequest:{ip}."},"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"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.\"","example":"Acme deploy bot \u2014 my-project"},"codeChallenge":{"type":"string","pattern":"^[A-Za-z0-9\\-_]{43}$","description":"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).","example":"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"},"codeChallengeMethod":{"type":"string","enum":["S256"],"default":"S256","description":"PKCE challenge method. Only S256 is supported."},"sandbox":{"type":"boolean","description":"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 \u2014 no real registry actions, DNS changes, or charges. Pass `scopes` to get a restricted sandbox key. Rate-limited (20\/IP\/hour)."},"returnUrl":{"type":"string","format":"uri","maxLength":2048,"description":"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 \u2014 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 \u2014 only the request token and status.","example":"https:\/\/app.example.com\/porkbun\/callback"},"scopes":{"type":"array","items":{"type":"string","enum":["read","dns","domains","email","hosting","purchase","funding","account"]},"description":"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.","example":["dns"]}}}}}},"responses":{"200":{"description":"Authorization request created","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Maximum requests allowed in the window (20)"},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Remaining requests in the current window"},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp when the window resets"}},"content":{"application\/json":{"schema":{"type":"object","required":["requestToken"],"properties":{"status":{"type":"string","example":"SUCCESS"},"requestToken":{"type":"string","description":"Token used to poll `\/apikey\/retrieve` to check approval status."},"authUrl":{"type":"string","description":"URL the account holder must visit to approve the request","example":"https:\/\/porkbun.com\/account\/apiKeyApproval\/abc123..."},"expiration":{"type":"string","description":"ISO datetime when this request expires (30 minutes from creation)","example":"2026-03-30 14:30:00"},"deliveryMode":{"type":"string","enum":["legacy","pkce"],"description":"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":{"type":"string","description":"Human-readable instructions"}}}}}},"400":{"description":"Validation error (e.g. app name too long)","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"500":{"description":"Internal error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/apikey\/request \\\n  -H 'Content-Type: application\/json' \\\n  -d '[]'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"none"}},"\/apikey\/retrieve":{"post":{"tags":["API Key Management"],"summary":"Poll for API key approval","description":"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 \u2014 it is displayed in the user's browser and must be pasted into the application.\n\n**Rate limit:** 120 requests per IP per 3600 seconds.","operationId":"apikeyRetrieve","x-ratelimit":{"requests":120,"window":3600,"windowUnit":"seconds","scope":"ip","note":"Counted per originating IP address. The Redis key is API:apikeyRetrieve:{ip}."},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["requestToken"],"properties":{"requestToken":{"type":"string","description":"The token returned by \/apikey\/request. Must be a 64-character lowercase hex string.","pattern":"^[a-f0-9]{64}$"},"codeVerifier":{"type":"string","minLength":43,"maxLength":128,"pattern":"^[A-Za-z0-9\\-._~]+$","description":"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."}}}}}},"responses":{"200":{"description":"Approval status. `status` may be `SUCCESS` (approved), `PENDING` (awaiting user action), or `ERROR` (expired\/denied\/invalid).","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Maximum requests allowed in the window (120)"},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Remaining requests in the current window"},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp when the window resets"}},"content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["SUCCESS","PENDING","ERROR"],"example":"SUCCESS"},"apikey":{"type":"string","description":"The approved public API key. Present only when status is SUCCESS."},"secretapikey":{"type":"string","description":"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":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code. Present when status is ERROR."}}}}}},"400":{"description":"Validation error or expired\/denied token","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"403":{"description":"Request was denied by the account holder","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"security":[],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/apikey\/retrieve \\\n  -H 'Content-Type: application\/json' \\\n  -d '[]'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"none"}},"\/domain\/checkDomain":{"post":{"tags":["Domain"],"summary":"Check several domains at once","description":"Check availability and pricing for up to 25 domains in one call. Same endpoint as the single check, with a `domains` array in the body and no domain in the path.\n\n**Prefer this over looping the single check.** Checks are chunked per registry, so a batch is materially less work for us than the same names one at a time, and the budget reflects that: this draws on a separate allowance of **200 domains per 60 seconds** per account, against 10 checks per 10 seconds for the single form. Counting is per DOMAIN, not per request, so 25 domains spends 25 of the 200.\n\n**Sustained bulk checking.** An account that checks about 1,800 names over three hours while registering little of what it checks (fewer than one domain per 200 names over a day) is treated as scanning for the next 24 hours: its checks, single and bulk, on every API version, share a budget of 50 names per 5 minutes. Over it, the call is refused whole with `RATE_LIMIT_EXCEEDED`, `reason: \"SUSTAINED_BULK_CHECKING\"` and `retryAfter`; nothing is part-answered. Normal use, including checking a long list of candidate names once, is nowhere near this. Accounts that buy (100 or more registrations in a year) and affiliates sending orders are never treated as scanning, and a per-key limit from support overrides it.\n\n**Partial results are normal and must be read.** Three lists come back and they mean different things:\n- `domains` - answered, keyed by domain name, each value identical in shape to the single-check `response`.\n- `invalid` - entries that are not checkable at all (not a domain, unsupported TLD). One typo does not fail the call; the other names are still answered.\n- `unresolved` - the registry did not answer in time. These are **neither available nor taken**; treating them as unavailable is wrong. Retry them.\n\nDuplicates are removed before the budget is charged. If more than 25 remain, nothing is checked and `BULK_CHECK_TOO_MANY` is returned rather than a truncated answer.\n\n**Latency.** This is synchronous: one request, one complete answer, no polling. 25 domains across 25 different TLDs measures around 3 seconds, because the connection manager fans a batch that fits under every registry's cap out as a single command. A few registries cap low and **.de accepts one domain per command**, so a batch heavy in those becomes several sequential commands; if that would need more than 4, the call is refused with `BULK_CHECK_TOO_SLOW` rather than left to time out mid-response.","operationId":"domainCheckDomainBulk","x-ratelimit":{"requests":200,"window":60,"windowUnit":"seconds","scope":"account","note":"Counted in DOMAINS, not requests, in its own bucket (Redis key API:bulkCheckDomains:{accountId}) separate from the single-check budget. Max 25 domains per call."},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["domains"],"properties":{"domains":{"type":"array","maxItems":25,"items":{"type":"string"},"description":"Domains to check, e.g. [\"example.com\", \"example.net\"]. Duplicates are removed.","example":["example.com","example.net","example.dev"]}}}]}}}},"responses":{"200":{"description":"Per-domain availability and pricing, plus any invalid or unresolved entries","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"checked":{"type":"integer","description":"How many domains were answered","example":3},"domains":{"type":"object","description":"Keyed by domain name; each value has the same shape as the single-check `response` object","additionalProperties":{"type":"object"}},"unresolved":{"type":"array","description":"The registry did not answer for these. NOT a statement of availability - retry them.","items":{"type":"string"}},"invalid":{"type":"array","description":"Entries that could not be checked, each with a reason","items":{"type":"object"}},"limits":{"type":"object","description":"Bulk budget usage; `countedIn` is \"domains\""}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"No checkable domains, or more than 25 supplied (BULK_CHECK_TOO_MANY)","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Bulk check budget exhausted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/checkDomain \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"domains\":[\"example.com\",\"example.net\"]}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"read"}},"\/domain\/checkDomain\/{domain}":{"post":{"tags":["Domain"],"summary":"Check domain availability","description":"Check if a domain is available for registration and retrieve current pricing. Includes registration, renewal, and transfer prices.\n\n**Rate limit:** Configurable per API key. Default is 10 checks per 10 seconds per account (raised from 1 per 10 seconds in 3.29). The window is intentionally 10 seconds rather than 1, so a batch of candidate names can be checked back-to-back. `\/domain\/checkSingleDomain` draws on the same budget. Rate limit usage is returned in the `limits` field of the response.\n\n**Sustained bulk checking.** An account that checks about 1,800 names over three hours while registering little of what it checks (fewer than one domain per 200 names over a day) is treated as scanning for the next 24 hours: its checks, single and bulk, on every API version, share a budget of 50 names per 5 minutes. Over it, the call is refused whole with `RATE_LIMIT_EXCEEDED`, `reason: \"SUSTAINED_BULK_CHECKING\"` and `retryAfter`; nothing is part-answered. Normal use, including checking a long list of candidate names once, is nowhere near this. Accounts that buy (100 or more registrations in a year) and affiliates sending orders are never treated as scanning, and a per-key limit from support overrides it.","operationId":"domainCheckDomain","x-ratelimit":{"requests":10,"window":10,"windowUnit":"seconds","scope":"account","note":"Default values. The actual limit and window are configurable per API key (api.checkLimit \/ api.checkTTL). The Redis key is API:checkSingleDomain:{accountId}, shared with \/domain\/checkSingleDomain."},"parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Availability and pricing","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CheckDomainResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Rate limit exceeded (returned as 200 body with status ERROR) or other error. See also 429.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/checkDomain\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/create\/{domain}":{"post":{"tags":["Domain"],"summary":"Register a domain","description":"Register a domain, paid from account credit, directly in USDC, or by card. Requirements:\n- Account email and phone must be verified\n- Enough **prepaid credit** to cover the price, or `\"payWith\": \"usdc\"` \/ `\"card\"` to pay directly (see \"Paying for it\" below)\n- `agreeToTerms` must be `'yes'` or `'1'`\n- `cost` must equal the current price for the domain's minimum registration duration (in pennies)\n- Premium domains cannot be registered via API, and a single order cannot exceed $100 (`ORDER_TOO_LARGE`)\n\n## Paying for it\n\nThree ways: from prepaid account credit (the default), directly in USDC, or directly by card with a hold.\n\n**From account credit.** No card on file is charged at purchase time - the thing most first-time callers get wrong, because having a card saved is not enough; the balance has to be there before the call.\n- Read the balance with `GET \/account\/balance` (cents, plus a display string).\n- Add to it: `POST \/account\/topup` charges a saved card now (with the account holder's OK); `POST \/account\/topupMpp` returns a link an agent pays with the user's card from a Stripe Link agent wallet; `POST \/account\/topupCrypto` opens a USDC checkout an agent's wallet can pay over x402; or the account holder adds credit at https:\/\/porkbun.com\/account\/credit.\n- Turn on **auto top-up** at https:\/\/porkbun.com\/account\/api so a saved card refills the balance when it drops below a threshold. The hands-off option for an agent that registers on its own.\n- Rehearse with `dryRun: true`, which reports `cost`, `balance`, `sufficientFunds` and, when short, `shortfall`.\n\nA purchase attempted without enough credit returns `INSUFFICIENT_FUNDS` carrying `cost`, `balance`, `shortfall`, `topUpAvailable` and `usdcAmountToCover` in cents, and a message naming them.\n\n**Directly in USDC (x402).** Add `\"payWith\": \"usdc\"`. After every check passes, the call answers HTTP 402 `PAYMENT_REQUIRED` with Coinbase's x402 terms in the `PAYMENT-REQUIRED` header and charges nothing. An x402 client pays them and repeats the same request with a `PAYMENT-SIGNATURE` header, and the registration completes in that call, with the receipt in `PAYMENT-RESPONSE`. No credit is needed. If the registration fails after payment, the payment is not sent back to the wallet: it stays on the account as credit (`payment.keptAsCredit` and `balance_cents` in the error), ready to retry from credit. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402\n\n**Directly by card, with a hold (MPP).** Add `\"payWith\": \"card\"`. The call answers HTTP 402 `PAYMENT_REQUIRED` with `method: \"card\"`, a single-use `payUrl` and a `cardPaymentId`, and charges nothing. Pay the `payUrl` with a Stripe Link agent wallet over MPP (for example `link-cli mpp pay <payUrl> -X POST`; the person approves in Link): that only **authorizes** the card. Repeat the request with `\"cardPaymentId\"` and the registration runs: if it succeeds the hold is captured, and if it fails the hold is released and nothing is charged. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp\n\n**Monthly limit.** Either way, purchases count toward the account's monthly API spend limit, $100 a month until the account holder sets their own at https:\/\/porkbun.com\/account\/api (`MONTHLY_SPEND_LIMIT_EXCEEDED`). Guide: https:\/\/porkbun.com\/llms\/guides\/spend-limits\n\nRegistrations are always for the registry-minimum duration (usually 1 year).\n\n**WHOIS privacy.** WHOIS privacy is automatically enabled on new registrations (when the TLD supports it). Pass the optional `whoisPrivacy` field to override this on a per-registration basis, or change the account-level default under Account Security Settings on porkbun.com\/account.\n\n**Rate limits (both apply):**\n- Attempt limit (default: 1 attempt per second per account)\n- Success limit (default: 1000 successful registrations per 86400 seconds per account)\n\nBoth limits are configurable per API key and their current values are returned in the `limits` field of the response.\n\n## Dry run\n\nAdd `dryRun: true` to validate everything and preview the cost WITHOUT registering or charging \u2014 nothing is created. Send `cost: 0` with it to be quoted rather than having to match a price you do not know yet, and note that a dry run is answered even when the balance is too low: `wouldSucceed` comes back false with `shortfall` saying by how much. Example response:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"dryRun\": true,\n  \"wouldSucceed\": true,\n  \"operation\": \"registration\",\n  \"domain\": \"example.com\",\n  \"tld\": \"com\",\n  \"available\": \"available\",\n  \"premium\": false,\n  \"duration\": 1,\n  \"cost\": 973,\n  \"costDisplay\": \"$9.73\",\n  \"balance\": 5000,\n  \"sufficientFunds\": true,\n  \"message\": \"Dry run: this registration would succeed and cost $9.73. No order was created and no charge was made.\",\n  \"requestId\": \"019e04fa-258d-7d11-aa86-4d5795c3fe8f\"\n}\n``` Every registration includes a free email hosting trial mailbox, waiting to be set up with \/email\/setupMailbox (see \/email\/getMailboxes).","operationId":"domainCreate","x-ratelimit":{"requests":null,"window":null,"windowUnit":"seconds","scope":"account","note":"Two separate limits apply. Attempt limit: default 1 attempt per 1s (api.createLimit \/ api.createTTL). Success limit: default 50 successes per 86400s (api.createSuccessLimit \/ api.createSuccessTTL). Both are configurable per API key. Redis keys: API:createSingleDomain:{accountId} and API:createSuccessSingleDomain:2:{accountId}."},"parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateDomainRequest"}}}},"responses":{"200":{"description":"Domain registered successfully","content":{"application\/json":{"schema":{"oneOf":[{"$ref":"#\/components\/schemas\/CreateDomainResponse"},{"$ref":"#\/components\/schemas\/DryRunPreviewResponse"}]},"examples":{"registered":{"summary":"Live registration (domain registered, charged)","value":{"status":"SUCCESS","domain":"example.com","cost":973,"orderId":12345678,"limits":{"attempts":{"TTL":1,"limit":1,"used":1,"naturalLanguage":"1 out of 1 create attempts within 1 seconds used."},"success":{"TTL":86400,"limit":50,"used":1,"naturalLanguage":"1 out of 1000 successful creates within 86400 seconds used."}},"balance":4027,"ttlRemaining":86400,"requestId":"019e04fa-258d-7d11-aa86-4d5795c3fe8f"}},"dryRun":{"summary":"Dry run (dryRun:true \u2014 nothing registered or charged)","value":{"status":"SUCCESS","dryRun":true,"wouldSucceed":true,"operation":"registration","domain":"example.com","tld":"com","available":"available","premium":false,"duration":1,"cost":973,"costDisplay":"$9.73","balance":5000,"sufficientFunds":true,"message":"Dry run: this registration would succeed and cost $9.73. No order was created and no charge was made.","requestId":"019e04fa-258d-7d11-aa86-4d5795c3fe8f"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation, fraud, or rate-limit error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/create\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"cost\":0,\"agreeToTerms\":\"yes\"}'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/domain\/renew\/{domain}":{"post":{"tags":["Domain"],"summary":"Renew a domain","description":"Renew a domain, paid from account credit, directly in USDC, or by card. Requirements:\n- Domain must be in your account and active\n- Domain must be opted in to API access\n- Account email and phone must be verified\n- Sufficient account credit, or `\"payWith\": \"usdc\"` \/ `\"card\"` to pay directly\n- `cost` must equal the current renewal price for the domain's minimum renewal duration (in pennies)\n- The domain must not have been registered or renewed in the last 30 days (`RENEWAL_TOO_SOON` otherwise, with `renewableAt`: when it can be renewed)\n- Premium renewals are not currently supported via API\n\nRenewals are always for the registry-minimum duration (usually 1 year). Use `\/domain\/checkDomain\/{domain}` with `priceType=renewal` to get the current price before renewing.\n\n**Rate limits (both apply):**\n- Attempt limit (default: 1 attempt per second per account)\n- Success limit (default: 1000 successful renewals per 86400 seconds per account)\n\nBoth limits are configurable per API key and their current values are returned in the `limits` field of the response.\n\n## Dry run\n\nAdd `dryRun: true` to validate everything and preview the cost WITHOUT renewing or charging \u2014 nothing is created. Example response:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"dryRun\": true,\n  \"wouldSucceed\": true,\n  \"operation\": \"renewal\",\n  \"domain\": \"example.com\",\n  \"tld\": \"com\",\n  \"available\": \"unavailable\",\n  \"premium\": false,\n  \"duration\": 1,\n  \"cost\": 1099,\n  \"costDisplay\": \"$10.99\",\n  \"balance\": 5000,\n  \"sufficientFunds\": true,\n  \"message\": \"Dry run: this renewal would succeed and cost $10.99. No order was created and no charge was made.\",\n  \"requestId\": \"019e04fa-3c11-7a02-9bd2-1f7c0e4a8b55\"\n}\n```\n\n**Paying for it.** Two ways. **From prepaid account credit** (the default): no card on file is charged at purchase time, so the balance has to be there first. Read it with `GET \/account\/balance`; add to it with `POST \/account\/topup` (a saved card, with the account holder's OK), `POST \/account\/topupMpp` (the user's card from a Stripe Link agent wallet), `POST \/account\/topupCrypto` (USDC) or at https:\/\/porkbun.com\/account\/credit, and auto top-up at https:\/\/porkbun.com\/account\/api keeps it filled. Too little credit returns `INSUFFICIENT_FUNDS` with `cost`, `balance`, `shortfall` and `usdcAmountToCover` in cents. **Or directly in USDC:** add `\"payWith\": \"usdc\"` and an x402 client pays the HTTP 402 this returns (`PAYMENT_REQUIRED`, terms in the `PAYMENT-REQUIRED` header) and repeats the request with `PAYMENT-SIGNATURE`; no credit is needed, and if the purchase fails the payment stays on the account as credit (https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402). **Or by card with a hold:** `\"payWith\": \"card\"` returns a single-use `payUrl` to pay from a Stripe Link agent wallet (it only authorizes); repeat with `cardPaymentId`, and the hold is captured if the purchase succeeds or released if it fails (https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp). Either way the account's monthly API spend limit applies ($100 a month until the account holder sets their own; `MONTHLY_SPEND_LIMIT_EXCEEDED`). `dryRun: true` reports cost and balance without charging, and `dryRun` with `cost: 0` returns a quote.","operationId":"domainRenew","x-ratelimit":{"requests":null,"window":null,"windowUnit":"seconds","scope":"account","note":"Two separate limits apply. Attempt limit: default 1 attempt per 1s (api.createLimit \/ api.createTTL). Success limit: default 50 successes per 86400s (api.createSuccessLimit \/ api.createSuccessTTL). Both are configurable per API key. Redis keys: API:renewSingleDomain:{accountId} and API:renewSuccessSingleDomain:{accountId}."},"parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RenewDomainRequest"}}}},"responses":{"200":{"description":"Domain renewed successfully","content":{"application\/json":{"schema":{"oneOf":[{"$ref":"#\/components\/schemas\/RenewDomainResponse"},{"$ref":"#\/components\/schemas\/DryRunPreviewResponse"}]},"examples":{"renewed":{"summary":"Live renewal (domain renewed, charged)","value":{"status":"SUCCESS","domain":"example.com","expirationDate":"2027-04-21","cost":1099,"orderId":12345679,"balance":3928,"ttlRemaining":86400,"requestId":"019e04fa-3c11-7a02-9bd2-1f7c0e4a8b55"}},"dryRun":{"summary":"Dry run (dryRun:true \u2014 nothing renewed or charged)","value":{"status":"SUCCESS","dryRun":true,"wouldSucceed":true,"operation":"renewal","domain":"example.com","tld":"com","available":"unavailable","premium":false,"duration":1,"cost":1099,"costDisplay":"$10.99","balance":5000,"sufficientFunds":true,"message":"Dry run: this renewal would succeed and cost $10.99. No order was created and no charge was made.","requestId":"019e04fa-3c11-7a02-9bd2-1f7c0e4a8b55"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation or rate-limit error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/renew\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"cost\":0}'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/domain\/transfer\/{domain}":{"post":{"summary":"Initiate a domain transfer","description":"Initiates an inbound domain transfer to Porkbun, paid from account credit, directly in USDC, or by card. The transfer is processed asynchronously and typically takes 5\u20137 days to complete.\n\n**Requirements:**\n- Account email and phone must be verified.\n- Sufficient account credit to cover the transfer cost, or `\"payWith\": \"usdc\"` \/ `\"card\"` to pay directly.\n- Domain must not already be in your account.\n- No other active transfer for the same domain.\n- `.uk` and manage-only TLDs are not supported via API.\n- Premium domain transfers are not supported via API.\n- Some registries refuse registrant contacts on temporary or encrypted email providers and suspend domains that use one. `.in` and its second levels are the first; a transfer whose registrant contact uses one is refused with `REGISTRANT_EMAIL_NOT_ACCEPTED` before any charge. This is about the contact recorded on the domain, not about who hosts mail for it.\n\n## Dry run\n\nAdd `dryRun: true` to validate everything and preview the cost WITHOUT initiating the transfer or charging \u2014 nothing is created. Example response:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"dryRun\": true,\n  \"wouldSucceed\": true,\n  \"operation\": \"transfer\",\n  \"domain\": \"example.com\",\n  \"tld\": \"com\",\n  \"available\": \"unavailable\",\n  \"premium\": false,\n  \"duration\": 1,\n  \"cost\": 999,\n  \"costDisplay\": \"$9.99\",\n  \"balance\": 5000,\n  \"sufficientFunds\": true,\n  \"message\": \"Dry run: this transfer would succeed and cost $9.99. No order was created and no charge was made.\",\n  \"requestId\": \"019e04fa-5f22-7c93-8a41-2e9d0b3f6c77\"\n}\n```\n\n**Paying for it.** Two ways. **From prepaid account credit** (the default): no card on file is charged at purchase time, so the balance has to be there first. Read it with `GET \/account\/balance`; add to it with `POST \/account\/topup` (a saved card, with the account holder's OK), `POST \/account\/topupMpp` (the user's card from a Stripe Link agent wallet), `POST \/account\/topupCrypto` (USDC) or at https:\/\/porkbun.com\/account\/credit, and auto top-up at https:\/\/porkbun.com\/account\/api keeps it filled. Too little credit returns `INSUFFICIENT_FUNDS` with `cost`, `balance`, `shortfall` and `usdcAmountToCover` in cents. **Or directly in USDC:** add `\"payWith\": \"usdc\"` and an x402 client pays the HTTP 402 this returns (`PAYMENT_REQUIRED`, terms in the `PAYMENT-REQUIRED` header) and repeats the request with `PAYMENT-SIGNATURE`; no credit is needed, and if the purchase fails the payment stays on the account as credit (https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402). **Or by card with a hold:** `\"payWith\": \"card\"` returns a single-use `payUrl` to pay from a Stripe Link agent wallet (it only authorizes); repeat with `cardPaymentId`, and the hold is captured if the purchase succeeds or released if it fails (https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp). Either way the account's monthly API spend limit applies ($100 a month until the account holder sets their own; `MONTHLY_SPEND_LIMIT_EXCEEDED`). `dryRun: true` reports cost and balance without charging, and `dryRun` with `cost: 0` returns a quote.","operationId":"transferDomain","tags":["Domain"],"parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"The domain name to transfer (e.g. `example.com`)."},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/TransferDomainRequest"}}}},"responses":{"200":{"description":"Transfer initiated, dry-run preview, or error.","content":{"application\/json":{"schema":{"oneOf":[{"$ref":"#\/components\/schemas\/TransferDomainResponse"},{"$ref":"#\/components\/schemas\/DryRunPreviewResponse"}]},"examples":{"initiated":{"summary":"Live transfer initiated (charged)","value":{"status":"SUCCESS","domain":"example.com","orderId":12345680,"transferId":98765,"message":"Transfer initiated. The domain transfer process has begun and typically takes 5-7 days to complete.","balance":2829,"ttlRemaining":86400,"requestId":"019e04fa-5f22-7c93-8a41-2e9d0b3f6c77"}},"dryRun":{"summary":"Dry run (dryRun:true \u2014 nothing transferred or charged)","value":{"status":"SUCCESS","dryRun":true,"wouldSucceed":true,"operation":"transfer","domain":"example.com","tld":"com","available":"unavailable","premium":false,"duration":1,"cost":999,"costDisplay":"$9.99","balance":5000,"sufficientFunds":true,"message":"Dry run: this transfer would succeed and cost $9.99. No order was created and no charge was made.","requestId":"019e04fa-5f22-7c93-8a41-2e9d0b3f6c77"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/transfer\/example.com' \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\n    \"apikey\": \"pk1_...\",\n    \"secretapikey\": \"sk1_...\",\n    \"authCode\": \"abc123\",\n    \"cost\": 899\n  }'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/domain\/getTransferSetup\/{domain}":{"get":{"tags":["Domain"],"summary":"State of an in-flight inbound transfer","operationId":"domainGetTransferSetup","description":"Where a pending inbound transfer is and what it is waiting on: whether it is held at `PENDINGDNS`, whether its DNS zone exists, how many records are in it, what the domain currently delegates to, and the next step to take. Use it to resume a no-downtime transfer without keeping state of your own.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com","description":"Domain name."}],"responses":{"200":{"description":"Current setup state.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"transfer":{"type":"object"},"held":{"type":"integer"},"zoneReady":{"type":"integer"},"recordCount":{"type":"integer"},"delegation":{"type":"object"},"dnssec":{"type":"integer"},"nextStep":{"type":"object"},"porkbunNameservers":{"type":"array","items":{"type":"string"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"No such pending transfer (`TRANSFER_NOT_FOUND`)"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/prepareTransfer\/{domain}":{"post":{"tags":["Domain"],"summary":"Create the DNS zone for a held transfer","operationId":"domainPrepareTransfer","description":"Create the Porkbun DNS zone for a domain whose inbound transfer is held at `PENDINGDNS`, so records can be added **before** the domain moves. The zone is created deliberately rather than as a side effect of the first record write. Returns the Porkbun nameservers to point the domain at. Then load the zone with `\/dns\/import\/{domain}` (or the `\/dns\/*` endpoints) and release with `\/domain\/startTransfer\/{domain}`. Supports `dryRun`.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com","description":"Domain name."},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Zone ready.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"zoneReady":{"type":"boolean"},"recordCount":{"type":"integer"},"message":{"type":"string"},"porkbunNameservers":{"type":"array","items":{"type":"string"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`TRANSFER_NOT_FOUND`, or the zone could not be created (`TRANSFER_ZONE_FAILED`)"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/startTransfer\/{domain}":{"post":{"tags":["Domain"],"summary":"Release a held transfer to the registry","operationId":"domainStartTransfer","description":"Release a transfer held at `PENDINGDNS` into the registry pipeline. Nothing releases a held transfer on a timer \u2014 this call is the only thing that does.\n\nRefuses with `TRANSFER_ZONE_EMPTY` if the zone has no records, which is the outage the hold exists to prevent. Pass `force: true` only if the domain genuinely needs no DNS at Porkbun. Supports `dryRun`.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com","description":"Domain name."},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean","description":"Release even though the zone is empty. Use only when the domain needs no DNS here."}}}}}},"responses":{"200":{"description":"Released.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`TRANSFER_NOT_FOUND`, `TRANSFER_NOT_HELD`, `TRANSFER_ZONE_EMPTY`, `TRANSFER_RELEASE_FAILED`"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/domain\/cancelTransfer\/{domain}":{"post":{"tags":["Domain"],"summary":"Cancel an inbound transfer and refund it","operationId":"domainCancelTransfer","description":"Cancel a pending inbound transfer and refund the order. The sequence is deliberate: mark the transfer cancelled locally, withdraw it at the registry, **verify** the registry actually accepted the withdrawal, and only then refund. If the registry state cannot be confirmed the local row is restored and `TRANSFER_STATE_UNCONFIRMED` is returned rather than refunding a transfer that may still be live.\n\nThe response reports `withdrawnAtRegistry`, `registryResultCode`, `refunded` and `refundAmount` so you can see exactly how far it got. Supports `dryRun`.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com","description":"Domain name."},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Cancelled.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"transferStatus":{"type":"string"},"withdrawnAtRegistry":{"type":"boolean"},"registryResultCode":{"type":"string"},"registryCode":{"type":"string"},"refunded":{"type":"boolean"},"refundAmount":{"type":"integer"},"orderId":{"type":"integer"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`TRANSFER_NOT_FOUND`, `TRANSFER_NOT_CANCELLABLE`, `TRANSFER_ALREADY_COMPLETED`, `TRANSFER_CANCEL_FAILED`, `TRANSFER_STATE_UNCONFIRMED`, `REFUND_FAILED`"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/domain\/updateTransferAuthCode\/{domain}":{"post":{"tags":["Domain"],"summary":"Replace the auth code on a stuck transfer","operationId":"domainUpdateTransferAuthCode","description":"Replace the authorization code on an inbound transfer that stalled because the code was wrong, and re-queue it \u2014 instead of cancelling, refunding and re-submitting.\n\nThe new code is validated against the registry before it is stored, so a bad code is rejected here (`INVALID_AUTH_CODE`) rather than failing again later. Only transfers in a repairable state qualify; anything else returns `TRANSFER_NOT_REPAIRABLE`. Supports `dryRun`.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com","description":"Domain name."},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["authCode"],"properties":{"authCode":{"type":"string","description":"The replacement authorization code, exactly as the losing registrar issued it. Never interpolate this into a shell command."}}}}}},"responses":{"200":{"description":"Code replaced and the transfer re-queued.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"authCodeValid":{"type":"boolean"},"previousStatus":{"type":"string"},"transferStatus":{"type":"string"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`INVALID_INPUT`, `INVALID_AUTH_CODE`, `TRANSFER_NOT_FOUND`, `TRANSFER_NOT_REPAIRABLE`, `TRANSFER_UPDATE_FAILED`"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/getTransfer\/{domain}":{"get":{"tags":["Domain"],"summary":"Get transfer status","description":"Returns the most recent transfer record for the specified domain in your account. Authenticate using `X-API-Key` and `X-Secret-API-Key` headers, or `Authorization: Bearer <token>`.","operationId":"getTransferGet","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"The domain name (e.g. `example.com`)."}],"responses":{"200":{"description":"Transfer record or error.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/GetTransferResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getTransfer\/example.com' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/listTransfers":{"get":{"tags":["Domain"],"summary":"List active transfers","description":"Returns all active inbound domain transfers for your account (excludes completed and canceled transfers). Authenticate using `X-API-Key` and `X-Secret-API-Key` headers, or `Authorization: Bearer <token>`.","operationId":"listTransfersGet","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"List of active transfers.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ListTransfersResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/listTransfers' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/listAll":{"post":{"tags":["Domain"],"summary":"List all domains","description":"Retrieve domains in the authenticated account. Results are returned in chunks of up to 1000 domains. Use `start` to paginate.\n\n**Filtering:** all filter parameters are optional. Combine them freely.\n\n- `domain` \u2014 exact match (returns 0 or 1)\n- `nameContains` \u2014 substring search\n- `tlds` \u2014 limit to these TLDs\n- `expiringWithinDays` \u2014 only domains expiring within N days\n- `addedAfter` \/ `addedBefore` \u2014 when the domain joined this account at Porkbun, by registration or transfer (`addedDate`). Use `addedAfter` to fetch only what's new since your last sync\n- `createdAfter` \/ `createdBefore` \u2014 the registry creation date (`createDate`); a transferred-in domain keeps its original one\n- `expiresAfter` \/ `expiresBefore` \u2014 an expiry window by date\n- `autoRenew` \u2014 `yes` \/ `no`\n- `apiAccess` \u2014 `yes` \/ `no` (filter to domains the API key can operate on)\n- `sortName` \u2014 `domain` \/ `tld` \/ `create_date` \/ `added_date` \/ `expire_date`\n\nDates are `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS`, in UTC like every date in the response. `...After` includes the moment given; `...Before` excludes it.\n- `sortDirection` \u2014 `asc` \/ `desc`\n\nSupports both GET (with header auth) and POST (with body or header auth). For multi-value `tlds` on GET, use bracket syntax: `?tlds[]=com&tlds[]=io`.","operationId":"listDomains","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ListAllRequest"}}}},"responses":{"200":{"description":"Domain list","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DomainListAllResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Authentication error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/listAll \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"start\":0,\"includeLabels\":\"yes\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"read"},"get":{"tags":["Domain"],"summary":"List all domains","description":"Retrieve domains in the authenticated account. Results are returned in chunks of up to 1000 domains. Use `start` to paginate.\n\n**Filtering:** all filter parameters are optional. Combine them freely.\n\n- `domain` \u2014 exact match (returns 0 or 1)\n- `nameContains` \u2014 substring search\n- `tlds` \u2014 limit to these TLDs\n- `expiringWithinDays` \u2014 only domains expiring within N days\n- `addedAfter` \/ `addedBefore` \u2014 when the domain joined this account at Porkbun, by registration or transfer (`addedDate`). Use `addedAfter` to fetch only what's new since your last sync\n- `createdAfter` \/ `createdBefore` \u2014 the registry creation date (`createDate`); a transferred-in domain keeps its original one\n- `expiresAfter` \/ `expiresBefore` \u2014 an expiry window by date\n- `autoRenew` \u2014 `yes` \/ `no`\n- `apiAccess` \u2014 `yes` \/ `no` (filter to domains the API key can operate on)\n- `sortName` \u2014 `domain` \/ `tld` \/ `create_date` \/ `added_date` \/ `expire_date`\n\nDates are `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS`, in UTC like every date in the response. `...After` includes the moment given; `...Before` excludes it.\n- `sortDirection` \u2014 `asc` \/ `desc`\n\nSupports both GET (with header auth) and POST (with body or header auth). For multi-value `tlds` on GET, use bracket syntax: `?tlds[]=com&tlds[]=io`.","operationId":"getDomains","responses":{"200":{"description":"Domain list","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DomainListAllResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Authentication error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0},"description":"Zero-based offset for pagination. Returns up to 1000 domains per call."},{"name":"includeLabels","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]},"description":"Return label metadata for each domain. Defaults to no."},{"name":"domain","in":"query","required":false,"schema":{"type":"string"},"description":"Exact domain name match. Returns 0 or 1 result."},{"name":"nameContains","in":"query","required":false,"schema":{"type":"string"},"description":"Substring match against the full domain name. Case-insensitive."},{"name":"expiringWithinDays","in":"query","required":false,"schema":{"type":"integer"},"description":"Filter to domains expiring within this many days."},{"name":"addedAfter","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains added to this account at Porkbun (registered or transferred in) at or after this date or date-time, UTC. Compare `addedDate` in the response. To sync only new domains, pass the newest `addedDate` you have seen.","example":"2026-10-01"},{"name":"addedBefore","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains added to this account at Porkbun before this date or date-time, UTC.","example":"2026-11-01"},{"name":"createdAfter","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains whose registry creation date (`createDate`) is at or after this date or date-time, UTC. A transferred-in domain keeps its original creation date, so use `addedAfter` for \"new in my account\".","example":"2024-01-01"},{"name":"createdBefore","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains whose registry creation date is before this date or date-time, UTC.","example":"2025-01-01"},{"name":"expiresAfter","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains expiring at or after this date or date-time, UTC.","example":"2027-01-01"},{"name":"expiresBefore","in":"query","required":false,"schema":{"type":"string"},"description":"Only domains expiring before this date or date-time, UTC.","example":"2027-07-01"},{"name":"tlds","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true,"description":"Limit to these TLDs (no leading dot). Use bracket form: `?tlds[]=com&tlds[]=io`."},{"name":"autoRenew","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]},"description":"Filter to domains with auto-renew on or off."},{"name":"apiAccess","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]},"description":"Filter to domains opted in to API access."},{"name":"sortName","in":"query","required":false,"schema":{"type":"string","enum":["domain","tld","create_date","added_date","expire_date"]},"description":"Field to sort by. Default: `expire_date` ascending. `added_date` sorts by `addedDate`."},{"name":"sortDirection","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]},"description":"Sort direction. Default: asc."}],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/listAll?tlds[]=com&expiringWithinDays=30&autoRenew=no' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/get\/{domain}":{"get":{"tags":["Domain"],"summary":"Get a single domain","description":"Get the metadata for a single domain in the authenticated account. Returns the same per-domain shape as `listAll` items but as a single object. Returns HTTP 404 with code `DOMAIN_NOT_FOUND` if the domain isn't in the account.","operationId":"getDomain","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"Fully qualified domain name in the authenticated account.","example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"includeLabels","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]},"description":"Return label metadata. Defaults to no."}],"responses":{"200":{"description":"Domain metadata","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"object","properties":{"domain":{"type":"string","example":"example.com"},"status":{"type":"string","example":"ACTIVE"},"tld":{"type":"string","example":"com"},"createDate":{"type":"string"},"expireDate":{"type":"string"},"securityLock":{"type":"integer"},"whoisPrivacy":{"type":"integer"},"autoRenew":{"type":"integer"},"apiAccess":{"type":"integer"},"notLocal":{"type":"integer"}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"Domain not found in the authenticated account.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/get\/example.com' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/updateAutoRenew\/{domain}":{"post":{"tags":["Domain"],"summary":"Update auto-renew setting","description":"Update the auto-renew setting for one or more domains. The domain can be passed in the URL path or in the `domains` array in the request body (or both). Both are combined and deduplicated.","operationId":"domainUpdateAutoRenew","parameters":[{"name":"domain","in":"path","required":false,"description":"Optional single domain in URL. Omit or use `\/domain\/updateAutoRenew\/` (without trailing domain) when using the `domains` body array instead.","schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/UpdateAutoRenewRequest"}}}},"responses":{"200":{"description":"Per-domain results","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"results":{"type":"object","description":"Object keyed by domain name","additionalProperties":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/updateAutoRenew\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"status\":\"on\",\"domains\":[]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/getNs\/{domain}":{"post":{"tags":["Domain"],"summary":"Get nameservers","description":"Retrieve the authoritative nameservers listed at the registry for the domain. Supports both GET (with header auth) and POST (with body or header auth). **Nameservers are an unordered set.** This reads live from the registry, and registries are free to return the set in any order \u2014 so the order here will often differ from the order you sent to `\/domain\/updateNs`. Order carries no meaning in DNS (an NS RRset is unordered, RFC 1034\/2181) and is not preserved by the parent zone. Compare nameservers as a set; if you are writing a Terraform provider or similar, model this as a set, not an ordered list, or every plan will show phantom drift.","operationId":"domainGetNs","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Nameserver list","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"ns":{"type":"array","items":{"type":"string"},"example":["ns1.porkbun.com","ns2.porkbun.com"]}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getNs\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Domain"],"summary":"Get nameservers","description":"Retrieve the authoritative nameservers listed at the registry for the domain. Supports both GET (with header auth) and POST (with body or header auth). **Nameservers are an unordered set.** This reads live from the registry, and registries are free to return the set in any order \u2014 so the order here will often differ from the order you sent to `\/domain\/updateNs`. Order carries no meaning in DNS (an NS RRset is unordered, RFC 1034\/2181) and is not preserved by the parent zone. Compare nameservers as a set; if you are writing a Terraform provider or similar, model this as a set, not an ordered list, or every plan will show phantom drift.","operationId":"getDomainNs","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Nameserver list","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"ns":{"type":"array","items":{"type":"string"},"example":["ns1.porkbun.com","ns2.porkbun.com"]}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getNs\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/updateNs\/{domain}":{"post":{"tags":["Domain"],"summary":"Update nameservers","description":"Update the nameservers for the domain at the registry. The list you send is applied as a set \u2014 the registry may store and return it in a different order, so do not expect `\/domain\/getNs` to echo your ordering back. See `\/domain\/getNs`.","operationId":"domainUpdateNs","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/UpdateNsRequest"}}}},"responses":{"200":{"description":"Update result","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/updateNs\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"ns\":[\"ns1.example.com\",\"ns2.example.com\"]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/getGlue\/{domain}":{"post":{"tags":["Domain"],"summary":"Get glue records","description":"Retrieve all glue records (host objects) registered under the domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"domainGetGlue","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Glue records","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"hosts":{"type":"array","description":"Array of [hostname, ipAddresses] tuples. Each element is a two-item array: index 0 is the full hostname (string), index 1 is an object with `v4` (array of IPv4 strings) and `v6` (array of IPv6 strings). Example: `[\"ns1.example.com\", {\"v4\": [\"1.2.3.4\"], \"v6\": []}]`","items":{"type":"array","description":"Two-element array: [0] full hostname string, [1] IP addresses object","prefixItems":[{"type":"string","description":"Full hostname (e.g. 'ns1.example.com')"},{"type":"object","description":"IP address object","properties":{"v4":{"type":"array","items":{"type":"string"},"description":"Array of IPv4 address strings"},"v6":{"type":"array","items":{"type":"string"},"description":"Array of IPv6 address strings"}}}]}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getGlue\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Domain"],"summary":"Get glue records","description":"Retrieve all glue records (host objects) registered under the domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDomainGlue","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Glue records","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"hosts":{"type":"array","description":"Array of [hostname, ipAddresses] tuples. Each element is a two-item array: index 0 is the full hostname (string), index 1 is an object with `v4` (array of IPv4 strings) and `v6` (array of IPv6 strings). Example: `[\"ns1.example.com\", {\"v4\": [\"1.2.3.4\"], \"v6\": []}]`","items":{"type":"array","description":"Two-element array: [0] full hostname string, [1] IP addresses object","prefixItems":[{"type":"string","description":"Full hostname (e.g. 'ns1.example.com')"},{"type":"object","description":"IP address object","properties":{"v4":{"type":"array","items":{"type":"string"},"description":"Array of IPv4 address strings"},"v6":{"type":"array","items":{"type":"string"},"description":"Array of IPv6 address strings"}}}]}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getGlue\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/createGlue\/{domain}\/{subdomain}":{"post":{"tags":["Domain"],"summary":"Create glue record","description":"Create a glue record (host object) for a nameserver hostname under the domain. Use this when you want to host a nameserver at a subdomain of the domain itself (e.g. ns1.example.com).","operationId":"domainCreateGlue","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain portion only (e.g. 'ns1' for ns1.example.com)","example":"ns1"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/GlueRecordRequest"}}}},"responses":{"200":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/createGlue\/{domain}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"ips\":[\"1.2.3.4\",\"2001:db8::1\"]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/updateGlue\/{domain}\/{subdomain}":{"post":{"tags":["Domain"],"summary":"Update glue record","description":"Update the IP addresses of a glue record. All existing IP addresses are replaced with the supplied list.","operationId":"domainUpdateGlue","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"example":"ns1"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/GlueRecordRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/updateGlue\/{domain}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"ips\":[\"1.2.3.4\",\"2001:db8::1\"]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/domain\/deleteGlue\/{domain}\/{subdomain}":{"post":{"tags":["Domain"],"summary":"Delete glue record","description":"Delete a glue record (host object) for a subdomain.","operationId":"domainDeleteGlue","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"example":"ns1"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/deleteGlue\/{domain}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/domain\/getUrlForwarding\/{domain}":{"post":{"tags":["Domain"],"summary":"List URL forwards","description":"Retrieve all active URL forwards for a domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"domainGetUrlForwarding","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"URL forward list","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/GetUrlForwardingResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getUrlForwarding\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Domain"],"summary":"List URL forwards","description":"Retrieve all active URL forwards for a domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDomainUrlForwarding","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"URL forward list","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/GetUrlForwardingResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getUrlForwarding\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/addUrlForward\/{domain}":{"post":{"tags":["Domain"],"summary":"Add URL forward","description":"Add a URL forward for a domain or subdomain.","operationId":"domainAddUrlForward","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AddUrlForwardRequest"}}}},"responses":{"200":{"description":"Forward added","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/addUrlForward\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"subdomain\":\"www\",\"location\":\"https:\\\/\\\/destination.example.com\",\"type\":\"temporary\",\"includePath\":\"yes\",\"wildcard\":\"yes\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"dns"}},"\/domain\/deleteUrlForward\/{domain}\/{id}":{"post":{"tags":["Domain"],"summary":"Delete URL forward","description":"Delete a specific URL forward by ID.","operationId":"domainDeleteUrlForward","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"URL forward record ID","example":"12345"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/deleteUrlForward\/{domain}\/{id} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/domain\/getContacts\/{domain}":{"get":{"tags":["Domain"],"summary":"Get domain contacts","description":"Return the domain's four contacts (registrant, admin, tech, billing) with their current field values. Also available via POST.","operationId":"domainGetContacts","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"The four contacts, keyed by role.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"contacts":{"type":"object","properties":{"registrant":{"$ref":"#\/components\/schemas\/DomainContact"},"admin":{"$ref":"#\/components\/schemas\/DomainContact"},"tech":{"$ref":"#\/components\/schemas\/DomainContact"},"billing":{"$ref":"#\/components\/schemas\/DomainContact"}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getContacts\/example.com \\\n  -H 'X-API-Key: pk1_...' -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/domain\/updateContacts\/{domain}":{"post":{"tags":["Domain"],"summary":"Update domain contacts","description":"Edit the domain's contacts. Send a `contacts` object keyed by role (`registrant`, `admin`, `tech`, `billing`) containing ANY subset of roles \u2014 unspecified roles keep their current values \u2014 or a single `contact` object to apply to all four. Behaves like the website: on thick TLDs the change is pushed to the registry, and a **registrant** change (name\/organization\/email) triggers the same material-change record and new-owner notice\/verification email (no 60-day transfer lock is imposed). Per-TLD extension data (e.g. .us nexus, .ca legalType) is preserved. Supports `dryRun`.\n\n**`state` is matched against the official list for the country**, the same list the website's dropdown is built from, so case, accents and short codes all work: `NEW JERSEY`, `new jersey` and `NJ` are all stored as `New Jersey`, and `MEXICO CITY` as `Mexico City`. This matters because some registries validate the field and compare it case sensitively \u2014 .mx refuses `NEW JERSEY` and requires `Ciudad de M\u00e9xico` for Mexico City, which Porkbun substitutes for you. For a TLD whose registry validates the field (currently the .mx family) a state that matches nothing is refused up front with `INVALID_STATE`, and the response lists every accepted value for that country in `validStates` \u2014 better than the registry's own \"Object does not exist\". For every other TLD an unrecognised state is sent through unchanged rather than rewritten. If a registry then refuses it, the contact is NOT created and no contacts are changed; the error names the registry's complaint.\n\n**Address-validated TLDs.** On a **registrant** change for a TLD where we check the address (`.de`, `.nrw`, `.uk`\/`.co.uk`\/\u2026, `.us`, `.ca`, `.nyc`, `.au`, `.eu`, `.in`\/`.co.in`\/\u2026, `.nz`\/`.co.nz`\/\u2026), the API runs Google Address Validation (per-account rate-limited + 24h cached). If the address needs correction the call returns `ADDRESS_VALIDATION_REQUIRED` with a `suggestedAddress` and `addressValidationStatus`; re-submit with `addressValidationChoice` = `accept_suggestion` (save the standardized address) or `use_as_entered` (keep yours). Either choice saves; validation suggests, it never refuses. For `.de`, DENIC registry verification is then attempted automatically (address-fingerprint \/ email); if it still needs a proof-of-address document upload, that is completed at porkbun.com. **Still not supported via API:** a registrant name\/organization change on a `.au` domain is an auDA paid ownership trade and returns `REGISTRANT_CHANGE_NOT_SUPPORTED` (do it at porkbun.com). Admin\/tech\/billing edits are unaffected everywhere.","operationId":"domainUpdateContacts","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","properties":{"contacts":{"type":"object","description":"Any subset of roles to change; unspecified roles are left as-is.","properties":{"registrant":{"$ref":"#\/components\/schemas\/DomainContact"},"admin":{"$ref":"#\/components\/schemas\/DomainContact"},"tech":{"$ref":"#\/components\/schemas\/DomainContact"},"billing":{"$ref":"#\/components\/schemas\/DomainContact"}}},"contact":{"allOf":[{"$ref":"#\/components\/schemas\/DomainContact"}],"description":"A single contact applied to all four roles. Use this OR `contacts`, not both."},"dryRun":{"type":"boolean","description":"Validate and report wouldSucceed without applying the change."},"addressValidationChoice":{"type":"string","enum":["accept_suggestion","use_as_entered"],"description":"Only for a registrant change on an address-validated TLD after an ADDRESS_VALIDATION_REQUIRED response. 'accept_suggestion' saves the standardized suggestedAddress; 'use_as_entered' keeps the submitted address. Either choice saves the edit."}}}]}}}},"responses":{"200":{"description":"Updated, or a dry-run preview. `registrantChanged` indicates whether a new-owner notice email was sent.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"message":{"type":"string"},"registrantChanged":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/updateContacts\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"contacts\":{\"tech\":{\"firstName\":\"Ada\",\"lastName\":\"Lovelace\",\"organization\":\"Analytical Engines\",\"address1\":\"1 Countess Rd\",\"city\":\"London\",\"state\":\"\",\"postalCode\":\"NW5 1AA\",\"country\":\"GB\",\"phone\":\"2071234567\",\"phoneCountryCode\":\"44\",\"email\":\"ada@example.com\"}}}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/dns\/retrieve\/{domain}":{"post":{"tags":["Porkbun DNS"],"summary":"Retrieve all DNS records","description":"Retrieve all editable DNS records for a domain. SOA records and Porkbun default NS records are excluded. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"dnsRetrieve","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"DNS records","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieve\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Porkbun DNS"],"summary":"Retrieve all DNS records","description":"Retrieve all editable DNS records for a domain. SOA records and Porkbun default NS records are excluded. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDnsRecords","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"DNS records","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieve\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/retrieve\/{domain}\/{id}":{"post":{"tags":["Porkbun DNS"],"summary":"Retrieve DNS record by ID","description":"Retrieve a specific DNS record by its numeric ID. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"dnsRetrieveById","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Numeric DNS record ID","example":"123456789"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"DNS record(s) matching the ID","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieve\/{domain}\/{id} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Porkbun DNS"],"summary":"Retrieve DNS record by ID","description":"Retrieve a specific DNS record by its numeric ID. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDnsRecordById","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Numeric DNS record ID","example":"123456789"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"DNS record(s) matching the ID","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieve\/{domain}\/{id}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/retrieveByNameType\/{domain}\/{type}\/{subdomain}":{"post":{"tags":["Porkbun DNS"],"summary":"Retrieve DNS records by name and type","description":"Retrieve all DNS records for a domain that match a specific subdomain and record type. Omit `subdomain` (or leave the path segment empty) to query the root domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"dnsRetrieveByNameType","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"description":"DNS record type (A, AAAA, CNAME, MX, TXT, etc.)","example":"A"},{"name":"subdomain","in":"path","required":false,"schema":{"type":"string"},"description":"Subdomain portion only. Omit or leave empty for root domain records.","example":"www"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Matching DNS records","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieveByNameType\/{domain}\/{type}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Porkbun DNS"],"summary":"Retrieve DNS records by name and type","description":"Retrieve all DNS records for a domain that match a specific subdomain and record type. Omit `subdomain` (or leave the path segment empty) to query the root domain. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDnsRecordsByNameType","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"description":"DNS record type (A, AAAA, CNAME, MX, TXT, etc.)","example":"A"},{"name":"subdomain","in":"path","required":false,"schema":{"type":"string"},"description":"Subdomain portion only. Omit or leave empty for root domain records.","example":"www"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Matching DNS records","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRecordsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/retrieveByNameType\/{domain}\/{type}\/{subdomain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/create\/{domain}":{"post":{"tags":["Porkbun DNS"],"summary":"Create DNS record","description":"Create a new DNS record for a domain. The record ID is returned in the response.","operationId":"dnsCreate","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateDnsRequest"}}}},"responses":{"200":{"description":"Record created","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"id":{"type":"string","description":"The numeric ID of the newly created record","example":"123456789"},"warnings":{"$ref":"#\/components\/schemas\/DnsWriteWarnings"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/create\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"name\":\"www\",\"type\":\"A\",\"content\":\"1.2.3.4\",\"ttl\":600,\"prio\":10}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"dns"}},"\/dns\/edit\/{domain}\/{id}":{"post":{"tags":["Porkbun DNS"],"summary":"Edit DNS record by ID","description":"Edit a specific DNS record by its numeric ID. Send only the fields you want to change: any field left out keeps its current value (send `\"name\": \"\"` to move the record to the apex). SOA and default Porkbun NS records cannot be edited.","operationId":"dnsEdit","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Numeric DNS record ID","example":"123456789"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EditDnsRequest"}}}},"responses":{"200":{"description":"Record updated","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/edit\/{domain}\/{id} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"type\":\"A\",\"content\":\"5.6.7.8\",\"ttl\":0,\"prio\":0}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"dns"}},"\/dns\/editByNameType\/{domain}\/{type}\/{subdomain}":{"post":{"tags":["Porkbun DNS"],"summary":"Edit DNS records by name and type","description":"Replace the content of all records matching the given subdomain and type. SOA and NS records cannot be edited with this method (use edit by ID instead).","operationId":"dnsEditByNameType","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"example":"A"},{"name":"subdomain","in":"path","required":false,"schema":{"type":"string"},"description":"Subdomain portion only. Omit or leave empty for root domain records.","example":"www"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EditDnsByNameTypeRequest"}}}},"responses":{"200":{"description":"Records updated","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/editByNameType\/{domain}\/{type}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"content\":\"5.6.7.8\",\"ttl\":0,\"prio\":0}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"dns"}},"\/dns\/delete\/{domain}\/{id}":{"post":{"tags":["Porkbun DNS"],"summary":"Delete DNS record by ID","description":"Delete a specific DNS record. SOA and default Porkbun NS records cannot be deleted.","operationId":"dnsDelete","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Numeric DNS record ID","example":"123456789"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/delete\/{domain}\/{id} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/dns\/deleteByNameType\/{domain}\/{type}\/{subdomain}":{"post":{"tags":["Porkbun DNS"],"summary":"Delete DNS records by name and type","description":"Delete all DNS records matching the given subdomain and type. SOA and NS records cannot be deleted with this method (use delete by ID instead).","operationId":"dnsDeleteByNameType","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"example":"A"},{"name":"subdomain","in":"path","required":false,"schema":{"type":"string"},"description":"Subdomain portion only. Omit or leave empty for root domain records.","example":"www"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/deleteByNameType\/{domain}\/{type}\/{subdomain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/closeout\/search":{"get":{"tags":["Closeouts"],"summary":"Search expired-domain closeouts","operationId":"closeoutSearch","description":"Search the closeout inventory. Closeouts are expired domains that did not sell at auction and are now offered at a fixed, descending price \u2014 there is no bidding, the first buyer at the current price takes the name.\n\nEvery filter is optional; with none you get the first page of the whole list plus `totalAvailable`. Page with `start`\/`limit` until `start >= totalAvailable`.\n\n**`age` and `registrationDate` are on every row and both are sortable.** On the website the inventory is paginated by price tier and registration date is not shown, so finding aged names means walking several tier pages and then doing a WHOIS lookup per candidate. `sortName=registrationDate&sortDirection=asc` returns the oldest registrations first in one call.\n\n`price` is the closeout price alone. The binding total adds the renewal or transfer year you are also buying, and comes from `\/closeout\/get\/{domain}` \u2014 it cannot be derived from search results, because a domain already at Porkbun is renewed while anything else is transferred in, and those are priced differently.","parameters":[{"name":"query","in":"query","required":false,"schema":{"type":"string"},"description":"Keyword match on the domain name.","example":"invest"},{"name":"tld","in":"query","required":false,"schema":{"type":"string"},"description":"Single TLD, with or without the leading dot. Omit to search all.","example":"org"},{"name":"nameLength","in":"query","required":false,"schema":{"type":"integer"},"description":"Exact SLD character count.","example":4},{"name":"ageMin","in":"query","required":false,"schema":{"type":"integer"},"description":"Minimum domain age in years. Pair with sortName=registrationDate to find aged names.","example":10},{"name":"ageMax","in":"query","required":false,"schema":{"type":"integer"},"description":"Maximum domain age in years.","example":30},{"name":"priceMin","in":"query","required":false,"schema":{"type":"integer"},"description":"Minimum closeout price, integer US cents. Converted to whole dollars for the provider, rounding outward so the range never excludes an item inside it.","example":500},{"name":"priceMax","in":"query","required":false,"schema":{"type":"integer"},"description":"Maximum closeout price, integer US cents.","example":3000},{"name":"sortName","in":"query","required":false,"schema":{"type":"string"},"description":"One of: domain, endTime, price, revenue, visitors, inboundLinks, registrationDate. Ascending on registrationDate means oldest registration first, which is how you find aged names. Ascending on revenue, visitors or inboundLinks puts the domains with no recorded figure first (they return null) \u2014 use desc on those to see the highest values; the response carries a `warnings` entry reminding you.","example":"registrationDate"},{"name":"sortDirection","in":"query","required":false,"schema":{"type":"string"},"description":"`asc` or `desc`.","example":"asc"},{"name":"start","in":"query","required":false,"schema":{"type":"integer"},"description":"Offset for paging. Default 0.","example":0},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"},"description":"Rows per page, 1-500. Default 100.","example":100}],"responses":{"200":{"description":"Matching closeouts.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"count":{"type":"integer"},"totalAvailable":{"type":"integer","description":"Size of the filtered set, for paging."},"start":{"type":"integer"},"limit":{"type":"integer"},"closeouts":{"type":"array","items":{"$ref":"#\/components\/schemas\/CloseoutItem"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Invalid filter (`INVALID_INPUT`, `INVALID_TLD`)"},"502":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Inventory provider unreachable (`CLOSEOUT_SOURCE_UNAVAILABLE`)"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/closeout\/get\/{domain}":{"get":{"tags":["Closeouts"],"summary":"One closeout, with the binding total","operationId":"closeoutGet","description":"A single closeout plus `totalPrice` \u2014 the closeout price plus the registration year that comes with it. That total is what `\/closeout\/buy` will charge and what it expects back as `cost`.\n\n`localDomain` tells you which side of the pricing you are on: true means the name is already at Porkbun and is renewed, false means it is transferred in. `available` is false once somebody has claimed it.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"The closeout.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"available":{"type":"boolean"},"closeout":{"$ref":"#\/components\/schemas\/CloseoutItem"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Not currently offered as a closeout (`CLOSEOUT_NOT_FOUND`)"},"502":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`CLOSEOUT_SOURCE_UNAVAILABLE` or `PRICING_UNAVAILABLE`"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/closeout\/buy\/{domain}":{"post":{"tags":["Closeouts"],"summary":"Buy a closeout outright","operationId":"closeoutBuy","description":"**Spends account credit, or pays directly in USDC with `\"payWith\": \"usdc\"` (HTTP 402, then repeat with `PAYMENT-SIGNATURE`; kept as account credit if the claim fails), or by card with `\"payWith\": \"card\"` (a hold, captured only if the claim succeeds).** Counts toward the monthly API spend limit. Buys a closeout at its current price and claims the name with the provider in one call. Send `cost` as the exact `totalPrice` from `\/closeout\/get\/{domain}`; any other value is refused with `COST_MISMATCH` so a caller can never be charged a price it did not name. `dryRun: true` with `cost: 0` returns a quote and charges nothing. Honours `Idempotency-Key`.\n\n**The domain does not arrive immediately.** Claiming reserves it; the provider then has to release it, which usually takes a few days. Poll `\/domain\/listAll` or subscribe to the `domain.registered` webhook rather than expecting it in your account when this returns.\n\nEvery post-charge failure refunds automatically and says so via `refunded: true` \u2014 including losing the race to another buyer (`CLOSEOUT_UNAVAILABLE`) and the price moving between quote and claim (`COST_MISMATCH`). Closeouts are first-come at a fixed price, so a lost race is not worth retrying on the same name.\n\nNot available with a sandbox key (`SANDBOX_UNSUPPORTED`): the inventory provider has no test environment, so a purchase cannot be rehearsed without claiming a real name. Use `dryRun` against a live key instead \u2014 it validates and prices without charging.\n\nEligibility is the same as registering a domain, plus verified email and phone. There is no account-age or prior-order requirement \u2014 a first-time customer can buy a closeout, which matters because listings are first-come and often gone within minutes. An account that support has blocked from auctions and closeouts (past-due invoices, or an auction terms violation) returns `CLOSEOUT_NOT_ELIGIBLE`, which is not retryable.","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["cost"],"properties":{"cost":{"type":"integer","description":"Exact totalPrice in integer US cents, from \/closeout\/get\/{domain}. Use 0 only with dryRun to request a quote.","example":1684},"payWith":{"type":"string","enum":["usdc","card"],"description":"Optional. `\"usdc\"` pays for this purchase directly in USDC on Base over x402 instead of from account credit. The first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with a `PAYMENT-SIGNATURE` header, and the purchase completes in that call. If it then fails, the payment stays on the account as account credit; it is not sent back to the wallet. If your wallet pays URLs itself instead, pay the 402's `x402Url` (or have a person pay `payUrl`) and repeat the request with `usdcCheckoutId`. See the guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402. `\"card\"` pays by card through a Stripe Link agent wallet over MPP instead, with a real hold: the 402 carries a single-use `payUrl` and a `cardPaymentId`; pay the link (it authorizes, not charges), then repeat with `cardPaymentId`; the hold is captured if the purchase succeeds and released if it fails. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp"},"usdcCheckoutId":{"type":"string","description":"With `payWith: \"usdc\"`: the `checkoutId` from an earlier `PAYMENT_REQUIRED`, once its `x402Url` or `payUrl` has been paid. Send it with otherwise the same request; the purchase completes if the checkout is paid and was opened for this purchase."},"cardPaymentId":{"type":"string","description":"With `payWith: \"card\"`: the `cardPaymentId` from an earlier `PAYMENT_REQUIRED`, once its `payUrl` has been paid (the card is then held). Send it with otherwise the same request; the hold is captured if the purchase succeeds and released if not."},"dryRun":{"type":"boolean","description":"Validate and price without charging or claiming.","example":true}}}}}},"responses":{"200":{"description":"Claimed, or a dry-run quote.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"domain":{"type":"string"},"orderId":{"type":"integer"},"closeoutId":{"type":"integer"},"closeoutPrice":{"type":"integer"},"renewalPrice":{"type":"integer"},"totalPrice":{"type":"integer"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`COST_MISMATCH`, `INVALID_INPUT`, `UNSUPPORTED_TLD`, `INSUFFICIENT_FUNDS`"},"403":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`CLOSEOUT_NOT_ELIGIBLE`, `VERIFICATION_REQUIRED`, `ORDERS_BLOCKED`"},"404":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"`CLOSEOUT_NOT_FOUND`"},"409":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Already claimed by another buyer (`CLOSEOUT_UNAVAILABLE`)"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/aftermarket\/search":{"get":{"tags":["Aftermarket"],"summary":"Search domains for sale on the aftermarket","operationId":"aftermarketSearch","description":"Find registered domains that are for sale: the partner aftermarket (Afternic, Sedo, Atom, NameBright and DAX listings, merged to the lowest price per name), Porkbun Marketplace, and DAX auctions. The same listings the website's search shows. Use it when `checkDomain` says a name is taken (its `aftermarketSearch` field links here), or to browse names for sale by keyword.\n\n`query` is a keyword (`coffee`) or a full domain (`coffee.com`). For a full domain, `exactMatch` holds its listing if it has one, and related listings follow. Results come back as: the exact name first, then buy-now listings, then make-offer, then auctions, each by price.\n\n**Read `saleType` before quoting a price.** `buy_now`: `price` is the price to buy it. `make_offer`: `price` is the seller's minimum offer, not a price. `auction`: `price` is the current bid. All prices are integer US cents and are the current listed price; the binding price is set at checkout.\n\n**Buying is on the website, not the API:** send the user to the listing's `buyUrl`. An aftermarket name arrives by transfer from the seller, usually within a few days (`fastTransfer: true` is quicker).\n\nA cold search queries several marketplaces and can take a few seconds; results are cached for 10 minutes. Limited to 20 searches a minute per account (`RATE_LIMIT_EXCEEDED`). If a source cannot be reached, the others are still returned and `sourcesUnavailable` names it. Also accepts POST with the same fields in the body. Works with sandbox keys (the listings are real).","parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string"},"description":"A keyword or a full domain.","example":"coffee"},{"name":"tld","in":"query","required":false,"schema":{"type":"string"},"description":"Only listings on this TLD, with or without the leading dot.","example":"com"},{"name":"saleType","in":"query","required":false,"schema":{"type":"string","enum":["buy_now","make_offer","auction"]},"description":"Only this kind of listing."},{"name":"priceMin","in":"query","required":false,"schema":{"type":"integer"},"description":"Minimum price, integer US cents.","example":10000},{"name":"priceMax","in":"query","required":false,"schema":{"type":"integer"},"description":"Maximum price, integer US cents.","example":500000},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200},"description":"Listings to return (1-200, default 50). `total` says how many matched."}],"responses":{"200":{"description":"Listings (possibly none).","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"query":{"type":"string","example":"fun.now"},"exactMatch":{"description":"The listing for the exact domain searched, or null.","allOf":[{"$ref":"#\/components\/schemas\/AftermarketListing"}],"nullable":true},"listings":{"type":"array","items":{"$ref":"#\/components\/schemas\/AftermarketListing"}},"count":{"type":"integer","example":50},"total":{"type":"integer","description":"Listings that matched before `limit`.","example":200},"sourcesUnavailable":{"type":"array","items":{"type":"string"},"description":"Sources that could not be searched this time (partners, marketplace, daxauction).","example":[]},"note":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Invalid filter (`INVALID_INPUT`, `INVALID_TLD`)"},"502":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"description":"Inventory provider unreachable (`CLOSEOUT_SOURCE_UNAVAILABLE`)"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/preflight\/{domain}":{"get":{"tags":["DNS"],"summary":"Check whether a change will break the domain","description":"Read-only. Answers the question of whether a change is about to break the domain, before you change delegation, transfer it out, or turn DNSSEC on. It changes nothing and spends nothing.\n\n**Every check here comes from an incident we actually had**, which is the point: these are the failures where a zone looks fine and stops working anyway, and nobody can say why afterwards. Each finding names the rule it comes from and carries a `next_action`, so it argues its case rather than asserting a verdict.\n\nPass `intent` to scope it: `general` (default) runs everything applicable, `move-nameservers` adds the delegation checks, `transfer-out` adds the checks that matter when leaving, `enable-dnssec` the DNSSEC ones.\n\nRead `blockers` first -- those will break something. `warnings` will not break outright but are usually what the customer asks about next. `safe` is true only when both lists are empty.\n\n| id | severity | what it catches |\n|----|----------|-----------------|\n| `dnssec-active` | blocker | DS records are published, so new nameservers serve answers that do not match them and validating resolvers refuse the whole zone. The domain goes dark rather than degrading. Remove DNSSEC, wait for the DS TTL, then move. |\n| `dnssec-unknown` | warning | the registry did not answer conclusively. Do not read that as an absence of DNSSEC. |\n| `cname-exclusivity` | blocker | a CNAME sharing a name with another record type, which RFC 1034 forbids and resolvers handle inconsistently. |\n| `spf-duplicate` | blocker | more than one apex SPF record: a permerror under RFC 7208 4.5 that makes receivers fail the check for every sender. |\n| `spf-lookups` | blocker | an SPF chain over the ten DNS lookups RFC 7208 4.6.4 allows, which is also a permerror. |\n| `mx-undeliverable` | blocker | an apex MX pointing somewhere unreachable such as localhost, so mail bounces or vanishes. |\n| `wildcard-shadowed` | warning | names that exist only because of an MX or TXT record. Under RFC 4592 a wildcard answers only names that do NOT exist, so those names stop inheriting the wildcard address and go dark with nothing in the zone looking wrong. |\n| `apex-resolves` | warning | nothing answers at the bare domain. |\n| `zone-size` | warning | within 10 percent of the 2,500-record limit. |\n| `nameservers-ours` | warning | the domain is delegated to nameservers we do not operate, or to a mix of ours and someone else's -- so the zone you are inspecting is not what the world resolves. Writes to it keep succeeding and change nothing visible. |\n| `cloudflare-zone` | info | the zone is served through Cloudflare, which can lag briefly after a delegation change. |","operationId":"dnsPreflight","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"intent","in":"query","required":false,"description":"What you are about to do. One of general, move-nameservers, transfer-out, enable-dnssec. May also be sent in the request body.","schema":{"type":"string","enum":["general","move-nameservers","transfer-out","enable-dnssec"]},"example":"move-nameservers"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"The findings for this intent","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsPreflightResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/history\/{domain}":{"get":{"tags":["DNS"],"summary":"List restore points for a zone","description":"Every version of a zone we still hold, newest first.\n\n**DNS is the only part of a stack with no undo, and this is it.** If a record was deleted or edited by mistake, you do not have to know what it used to say -- ask for the restore points, diff one against the live zone, and put it back.\n\nRestore points are taken automatically: before the **first** write to a zone in each hour (so a session of edits costs one point, not one per record), before any bulk import or zone wipe, and before any restore. `recordCount` is the size of the zone **as it was at that moment**, not now.\n\n`matchesLive` tells you which point the zone currently sits on without diffing each one. Up to 50 are returned.","operationId":"dnsHistory","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Restore points, newest first","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsHistoryResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X GET https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/history\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/diff\/{domain}\/{snapshotId}":{"get":{"tags":["DNS"],"summary":"Compare a restore point with the live zone","description":"What changed between a restore point and the zone as it stands now.\n\n- `missing` -- in the restore point, not live. These are the records a restore would add back.\n- `extra` -- live, not in the restore point. A restore leaves these alone unless you pass `prune`.\n- `inSync` -- true when neither list has anything in it.\n\nRecords are compared on **name, type, content and priority**, not on id: an id means nothing across a delete and re-create, and what anyone means by \"the same record\" is the data. SOA and NS are excluded from both lists because they are the zone's own scaffolding.\n\nFor masked records (parking, `ALIAS`, `HTTPS`) the value shown is the one you configured, not the internal host it resolves to.","operationId":"dnsDiff","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"snapshotId","in":"path","required":true,"description":"A restore point id from GET \/dns\/history\/{domain}.","schema":{"type":"integer"},"example":1234}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"The difference between the restore point and the live zone","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsDiffResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"RESTORE_POINT_NOT_FOUND","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X GET https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/diff\/example.com\/1234 \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/restore\/{domain}":{"post":{"tags":["DNS"],"summary":"Restore a zone to a previous state","description":"Put a zone back to a restore point.\n\n**What it does by default:** adds back every record in the restore point that is not live now. It does **not** remove records you have added since -- pass `prune: true` for that. \"Restore my records\" usually means \"put back what I lost\", not \"delete everything I have done since\", so the destructive half is opt-in.\n\n**This is itself reversible.** The zone's current state is snapshotted before anything changes and the new point's id comes back as `previousStateSavedAs`, so an unwanted restore is undone by restoring that.\n\n**Read `failed`.** Parking records and other masked types (`ALIAS`, `HTTPS`) are managed by another part of the platform and cannot be recreated this way. They come back named in `failed` rather than being counted as restored, so `restored` is a true count. SOA and NS are never touched.\n\nSupports `dryRun: true`, which reports exactly what would be added and removed and changes nothing.","operationId":"dnsRestore","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["snapshotId"],"properties":{"snapshotId":{"type":"integer","description":"The restore point to go back to, from GET \/dns\/history\/{domain}.","example":1234},"prune":{"type":"boolean","description":"Also delete live records that are not in the restore point. Default false, which only adds back what is missing.","example":false},"dryRun":{"type":"boolean","description":"Report what would change without changing anything.","example":true}}}]}}}},"responses":{"200":{"description":"What was restored","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DnsRestoreResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"RESTORE_POINT_NOT_FOUND","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/restore\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"snapshotId\":1234,\"dryRun\":true}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/dns\/scan\/{domain}":{"get":{"tags":["DNS"],"summary":"Discover the records a domain currently publishes","description":"Query a domain's **live authoritative nameservers** and return the records they answer with, writing nothing. This is what makes an inbound transfer non-destructive: a transfer moves only the delegation \u2014 EPP carries no zone data \u2014 so once the losing registrar stops answering for the zone, whatever it published is gone and unrecoverable. Run this **before** the nameservers move, then pass the result to `\/dns\/import\/{domain}`.\n\nThe scan probes a wide list of well-known names (apex, common subdomains, MX, DKIM selectors, provider verification hosts) and consolidates wildcards. It cannot enumerate a zone \u2014 DNS has no listing operation and AXFR is universally refused \u2014 so treat it as thorough but **not exhaustive**. If the old registrar exposes the zone through its own API, that is authoritative: read it with your own credentials (they never need to reach Porkbun) and POST those records to `\/dns\/import` instead.\n\nMetered separately at 20 calls per hour per account, because each call is roughly 90 DNS lookups.","operationId":"dnsScan","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Records discovered at the domain's current nameservers.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"recordCount":{"type":"integer"},"records":{"type":"array","items":{"$ref":"#\/components\/schemas\/DnsRecordInput"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"description":"Scan rate limit reached","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}},"headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/import\/{domain}":{"post":{"tags":["DNS"],"summary":"Bulk-create DNS records (transfer restore)","description":"Create many records in one call, so a domain arriving from another registrar keeps resolving instead of going dark.\n\nSend `records` to import an exact list \u2014 the better path when the old registrar has an API you can read with your own credentials. Omit `records` entirely and whatever `\/dns\/scan\/{domain}` can discover is imported instead.\n\n**Idempotent.** A record that already exists is counted in `skipped`, not `failed`, so this is safe to re-run and safe to use as a converge step. Records that fail individually are listed in `failures` while the rest still import; the call only errors outright if nothing was importable.\n\nImported records do nothing until the domain actually points at the Porkbun nameservers \u2014 check `\/domain\/getNs\/{domain}` and set them with `\/domain\/updateNs\/{domain}`. Maximum 500 records per call. Supports `dryRun`.","operationId":"dnsImport","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"records":{"type":"array","description":"Records to create. Omit to import what a scan discovers. NS and SOA entries are ignored \u2014 they describe the delegation, not the zone contents.","items":{"$ref":"#\/components\/schemas\/DnsRecordInput"}}}}}}},"responses":{"200":{"description":"Import summary.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"created":{"type":"integer"},"skipped":{"type":"integer","description":"Already present, so nothing needed doing."},"failed":{"type":"integer"},"failures":{"type":"array","items":{"type":"object"}},"warnings":{"$ref":"#\/components\/schemas\/DnsWriteWarnings"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error, or nothing importable","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"dns"}},"\/dns\/getDnssecRecords\/{domain}":{"post":{"tags":["Porkbun DNS"],"summary":"Get DNSSEC records","description":"Retrieve DNSSEC records associated with the domain at the registry. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"dnsGetDnssecRecords","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"DNSSEC records keyed by key tag","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"records":{"type":"object","description":"Object keyed by key tag value. Each value is an object containing the DNSSEC data fields.","additionalProperties":{"type":"object","properties":{"keyTag":{"type":"string"},"alg":{"type":"string"},"digestType":{"type":"string"},"digest":{"type":"string"},"pubKey":{"type":"string","description":"Present for key data records"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/getDnssecRecords\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Porkbun DNS"],"summary":"Get DNSSEC records","description":"Retrieve DNSSEC records associated with the domain at the registry. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getDnssecRecords","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"DNSSEC records keyed by key tag","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"records":{"type":"object","description":"Object keyed by key tag value. Each value is an object containing the DNSSEC data fields.","additionalProperties":{"type":"object","properties":{"keyTag":{"type":"string"},"alg":{"type":"string"},"digestType":{"type":"string"},"digest":{"type":"string"},"pubKey":{"type":"string","description":"Present for key data records"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/getDnssecRecords\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/dns\/createDnssecRecord\/{domain}":{"post":{"tags":["Porkbun DNS"],"summary":"Create DNSSEC record","description":"Create a DNSSEC DS or key record at the registry. DNSSEC requirements vary by registry \u2014 `keyTag`, `alg`, `digestType`, and `digest` are the minimum required fields. Key data fields are optional and will be omitted if not accepted by the registry.\n\nAlgorithm and digest type are validated against the registry's own policy before the request is sent. Registries are retiring the values deprecated by RFC 9904\/9905\/9906 on different schedules, so what is accepted depends on the TLD: a value one registry has already dropped may still be accepted by another. A value the registry no longer accepts returns `DNSSEC_ALGORITHM_DEPRECATED`; one that still works but is being retired returns `status: SUCCESS` with a `warnings` array. Use algorithm 8 (RSA\/SHA-256) or 13 (ECDSA\/SHA-256) with digest type 2 (SHA-256) to be accepted everywhere. Existing DNSSEC records keep resolving regardless, and can always be deleted.","operationId":"dnsCreateDnssecRecord","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateDnssecRequest"}}}},"responses":{"200":{"description":"DNSSEC record created","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/BasicResponse"},{"type":"object","properties":{"warnings":{"type":"array","items":{"type":"string"},"description":"Present only when a value was accepted but is being retired, e.g. a digest type the registry has announced it will stop accepting. Advisory: the record was created."}}}]}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error, including DNSSEC_ALGORITHM_DEPRECATED when the registry no longer accepts the supplied algorithm or digest type","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/createDnssecRecord\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"keyTag\":\"12345\",\"alg\":\"13\",\"digestType\":\"2\",\"digest\":\"ABCD1234...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/dns\/deleteDnssecRecord\/{domain}\/{keytag}":{"post":{"tags":["Porkbun DNS"],"summary":"Delete DNSSEC record","description":"Delete a DNSSEC record from the registry by key tag. Note: most registries delete all records matching the key data, not only the record with the specified key tag.","operationId":"dnsDeleteDnssecRecord","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"keytag","in":"path","required":true,"schema":{"type":"string"},"description":"The DNSSEC key tag value","example":"12345"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/dns\/deleteDnssecRecord\/{domain}\/{keytag} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/hosting\/plans":{"get":{"tags":["Hosting"],"summary":"List provisionable hosting plans (static + WordPress)","description":"**Applies to:** Both products.\n\nList the hosting plans that can be provisioned via the API, with price, interval, trial length, and features. Pass a row's `plan` to `\/hosting\/create` and its `price` (cents) as `acknowledgedCost`. Currently Secure Static Hosting; more products are added over time. Also available via POST. Includes both Secure Static Hosting and Cloud for WordPress (managed WordPress) plans; the `product` field distinguishes them.","operationId":"hostingPlans","responses":{"200":{"description":"Available plans.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"plans":{"type":"array","items":{"type":"object","properties":{"product":{"type":"string","example":"secureStaticHosting"},"plan":{"type":"string","example":"monthly"},"sku":{"type":"string"},"interval":{"type":"string"},"price":{"type":"integer","description":"Price in cents; pass as acknowledgedCost to \/hosting\/create."},"priceFormatted":{"type":"string"},"trialDays":{"type":"integer"},"name":{"type":"string"},"features":{"type":"object"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl https:\/\/api.porkbun.com\/api\/json\/v3\/hosting\/plans \\\n  -H 'X-API-Key: pk1_...' -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/create\/{domain}":{"post":{"tags":["Hosting"],"summary":"Provision hosting \u2014 a static site or a WordPress site","description":"**Applies to:** Both products \u2014 the `sku` decides which.\n\nProvision hosting (Secure Static Hosting or Cloud for WordPress) for a domain in the account. The FIRST provision for a domain starts a **15-day free trial** ($0 now) that **auto-renews** at the plan price when the trial ends; a re-provision after deprovision is charged immediately, to account credit or directly in USDC with `\"payWith\": \"usdc\"` (HTTP 402, then repeat with `PAYMENT-SIGNATURE`; kept as account credit if provisioning fails) or by card with `\"payWith\": \"card\"` (a hold, captured only if provisioning succeeds), and counts toward the monthly API spend limit (one free trial per domain). Provisioning **switches the domain to Porkbun nameservers** if it isn't already \u2014 pass `agreeToNameserverChange: true` to allow that. Supports `dryRun`. Remote setup can be async: `status` may be `PENDING` \u2014 poll `\/hosting\/get` until `ACTIVE` before deploying.\n\n**Cloud for WordPress:** pass a `CLOUDWORDPRESS\u2026` sku to provision a managed WordPress site instead of static hosting. The file endpoints (deploy\/files\/deleteFile\/makeDir) do not apply \u2014 manage the site through WordPress, using `\/hosting\/createWpCredentials\/{domain}` for REST API credentials.\n\n**Rate limit:** 10 provisions per account per hour (`dryRun` calls are free).\n\n**Link in Bio** (`PIXIELINKSM1` monthly, `PIXIELINKSY1` yearly): a paid Link in Bio page. Over a free Link in Bio page (most .com domains registered at Porkbun have one) this upgrades it and keeps the title, links and settings; the response says so. The free Link in Bio and the free WordPress preview that come with a domain never block a purchase: another product replaces them. Edit the page with the `\/linkinbio` endpoints.","operationId":"hostingCreate","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["sku","acknowledgedCost","agreeToTerms"],"properties":{"sku":{"type":"string","enum":["PIXIESECURESTATICM2","PIXIESECURESTATICY2"],"description":"The hosting plan SKU to provision. Discover the provisionable SKUs (and each one\u2019s price\/interval\/trial) via GET \/hosting\/plans, then pass the row\u2019s `sku`. Currently Secure Static Hosting: PIXIESECURESTATICM2 ($3.00\/mo) or PIXIESECURESTATICY2 ($30.00\/yr)."},"acknowledgedCost":{"type":"integer","description":"Echo the plan price in cents (300 monthly \/ 3000 yearly) to confirm the account holder understands the auto-renew \/ charge. Mismatch returns COST_ACKNOWLEDGMENT_REQUIRED."},"agreeToTerms":{"type":"string","enum":["yes"]},"agreeToNameserverChange":{"type":"boolean","description":"Required (true) when the domain is not already on Porkbun nameservers \u2014 provisioning will switch them."},"payWith":{"type":"string","enum":["usdc","card"],"description":"Optional. `\"usdc\"` pays for this purchase directly in USDC on Base over x402 instead of from account credit. The first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with a `PAYMENT-SIGNATURE` header, and the purchase completes in that call. If it then fails, the payment stays on the account as account credit; it is not sent back to the wallet. If your wallet pays URLs itself instead, pay the 402's `x402Url` (or have a person pay `payUrl`) and repeat the request with `usdcCheckoutId`. See the guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402. `\"card\"` pays by card through a Stripe Link agent wallet over MPP instead, with a real hold: the 402 carries a single-use `payUrl` and a `cardPaymentId`; pay the link (it authorizes, not charges), then repeat with `cardPaymentId`; the hold is captured if the purchase succeeds and released if it fails. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp"},"usdcCheckoutId":{"type":"string","description":"With `payWith: \"usdc\"`: the `checkoutId` from an earlier `PAYMENT_REQUIRED`, once its `x402Url` or `payUrl` has been paid. Send it with otherwise the same request; the purchase completes if the checkout is paid and was opened for this purchase."},"cardPaymentId":{"type":"string","description":"With `payWith: \"card\"`: the `cardPaymentId` from an earlier `PAYMENT_REQUIRED`, once its `payUrl` has been paid (the card is then held). Send it with otherwise the same request; the hold is captured if the purchase succeeds and released if not."},"dryRun":{"type":"boolean","description":"Validate + preview without provisioning or charging."}}}]}}}},"responses":{"200":{"description":"Provisioned (or dry-run preview).","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"orderId":{"type":"integer"},"hosting":{"type":"object","properties":{"domain":{"type":"string"},"product":{"type":"string"},"plan":{"type":"string"},"server":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","PENDING"]},"trial":{"type":"boolean"},"trialEndsAt":{"type":"string","nullable":true},"autoRenew":{"type":"boolean"},"sku":{"type":"string"}}},"charged":{"type":"integer","description":"Cents captured now (0 on the free trial)."},"cost":{"type":"object","properties":{"amount":{"type":"integer"},"formatted":{"type":"string"},"interval":{"type":"string"}}},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/hosting\/create\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"plan\":\"monthly\",\"acknowledgedCost\":300,\"agreeToTerms\":\"yes\"}'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/hosting\/get\/{domain}":{"get":{"tags":["Hosting"],"summary":"Get hosting status (either product)","description":"**Applies to:** Both products.\n\nReturn the Secure Static Hosting status for a domain (plan, server, trial, expiry, auto-renew), or `hosting: null` if none. Also available via POST.","operationId":"hostingGet","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Hosting status.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"hosting":{"type":"object","nullable":true,"properties":{"domain":{"type":"string"},"product":{"type":"string"},"status":{"type":"string"},"plan":{"type":"string"},"server":{"type":"string"},"isTrial":{"type":"boolean"},"autoRenew":{"type":"boolean"},"expireDate":{"type":"string"},"pendingRetire":{"type":"boolean","description":"True when a deprovision (retire) is queued\/underway. Poll this endpoint until hosting is null for teardown completion."},"cost":{"type":"object"}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl https:\/\/api.porkbun.com\/api\/json\/v3\/hosting\/get\/example.com \\\n  -H 'X-API-Key: pk1_...' -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/deploy\/{domain}":{"post":{"tags":["Hosting"],"summary":"Upload site files (static hosting only)","description":"**Applies to:** Secure Static Hosting only \u2014 a WordPress site returns `NOT_SUPPORTED_FOR_PRODUCT`; manage its content through WordPress instead.\n\nUpload static files to the domain's Secure Static Hosting space. Send `files` as an array of `{ path, content }` where `content` is base64. Total payload \u2264 10 MB per request (split larger sites across calls). Only static-web file types are accepted (html\/css\/js\/images\/fonts\/\u2026); server-executable types are rejected. Hosting must be ACTIVE. A file\u2019s `path` may include directories (e.g. `assets\/css\/style.css`); any missing parent directories are created automatically. Paths are sanitized (no traversal\/control chars) and the filename extension must be an allowed static-web type.","operationId":"hostingDeploy","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["files"],"properties":{"files":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","example":"index.html"},"content":{"type":"string","description":"base64-encoded file contents"}}}}}}]}}}},"responses":{"200":{"description":"Deploy result.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"deployed":{"type":"array","items":{"type":"string"}},"skipped":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"reason":{"type":"string"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/hosting\/deploy\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"files\":[{\"path\":\"index.html\",\"content\":\"PGgxPkhlbGxvPC9oMT4=\"}]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}},"\/hosting\/files\/{domain}":{"get":{"tags":["Hosting"],"summary":"List site files (static hosting only)","description":"**Applies to:** Secure Static Hosting only \u2014 a WordPress site returns `NOT_SUPPORTED_FOR_PRODUCT`.\n\nList file\/directory names under an optional `path` in the domain's hosting space. Also available via POST (send `path` in the body).","operationId":"hostingFiles","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"File list.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"path":{"type":"string"},"files":{"type":"array","items":{"type":"string"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/deleteFile\/{domain}":{"post":{"tags":["Hosting"],"summary":"Delete a site file (static hosting only)","description":"**Applies to:** Secure Static Hosting only \u2014 a WordPress site returns `NOT_SUPPORTED_FOR_PRODUCT`.\n\nDelete a file or directory at `path` in the domain's hosting space. Idempotent: a path that is already gone returns `SUCCESS` with `alreadyAbsent: true`. A directory that still has files returns `HOSTING_DIR_NOT_EMPTY` with its `contents`, unless `recursive: true` is sent, which deletes it with everything inside (the site root can never be deleted).","operationId":"hostingDeleteFile","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["path"],"properties":{"path":{"type":"string","example":"old\/page.html"},"recursive":{"type":"boolean","default":false,"description":"Delete a directory together with everything in it (up to 2,000 items per call; call again to continue). Without it, a non-empty directory returns HOSTING_DIR_NOT_EMPTY."}}}]}}}},"responses":{"200":{"description":"Deleted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"deleted":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}},"\/hosting\/delete\/{domain}":{"post":{"tags":["Hosting"],"summary":"Deprovision hosting (either product)","description":"**Applies to:** Both products.\n\nDeprovision (cancel) Secure Static Hosting for a domain; teardown is scheduled and completed by Porkbun. Note: the domain has already used its one free trial, so provisioning it again later will be charged (no second free trial).\n\n**Link in Bio:** cancelling a paid plan moves the page to the free plan (50 links, fewer features); the page and its content stay. A free page cannot be cancelled here (`NOT_SUPPORTED_FOR_PRODUCT`).","operationId":"hostingDelete","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Deprovision scheduled.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}},"\/ssl\/retrieve\/{domain}":{"post":{"tags":["SSL"],"summary":"Retrieve SSL bundle","description":"Retrieve the Let's Encrypt SSL certificate bundle for a domain. The certificate must already be issued (status HAVECERT). Token-based access is not supported for this endpoint. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"sslRetrieve","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"SSL certificate bundle","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"certificatechain":{"type":"string","description":"The full PEM-encoded certificate chain (certificate + intermediates)"},"privatekey":{"type":"string","description":"The PEM-encoded private key"},"publickey":{"type":"string","description":"The PEM-encoded public key"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error (e.g. certificate not ready, domain not found)","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/ssl\/retrieve\/{domain} \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"},"get":{"tags":["SSL"],"summary":"Retrieve SSL bundle","description":"Retrieve the Let's Encrypt SSL certificate bundle for a domain. The certificate must already be issued (status HAVECERT). Token-based access is not supported for this endpoint. Supports both GET (with header auth) and POST (with body or header auth).","operationId":"getSslRetrieve","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"SSL certificate bundle","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"certificatechain":{"type":"string","description":"The full PEM-encoded certificate chain (certificate + intermediates)"},"privatekey":{"type":"string","description":"The PEM-encoded private key"},"publickey":{"type":"string","description":"The PEM-encoded public key"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error (e.g. certificate not ready, domain not found)","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/ssl\/retrieve\/{domain}' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"domains"}},"\/email\/getForwards\/{domain}":{"post":{"tags":["Email"],"summary":"List email forwards","description":"The email forwards on a domain: each `address` at the domain and the `forwardTo` mailbox it is delivered to (an address can forward to several). `limits` gives how many the domain has and may have; `dnsConfigured` says whether the apex MX records in the Porkbun zone point at Porkbun forwarding (if the domain uses other nameservers, those decide). Supports GET with header auth.","operationId":"emailGetForwards","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Forwards","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"forwards":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","example":"info@example.com"},"forwardTo":{"type":"string","example":"me@example.net"},"created":{"type":"string","example":"2026-10-07 17:46:11"}}}},"limits":{"type":"object","description":"Forwards the domain has (`used`, including any Porkbun manages itself) and may have (`max`)","properties":{"used":{"type":"integer","example":2},"max":{"type":"integer","example":20}}},"dnsConfigured":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/getForwards\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Email"],"summary":"List email forwards","description":"The email forwards on a domain: each `address` at the domain and the `forwardTo` mailbox it is delivered to (an address can forward to several). `limits` gives how many the domain has and may have; `dnsConfigured` says whether the apex MX records in the Porkbun zone point at Porkbun forwarding (if the domain uses other nameservers, those decide). Supports GET with header auth.","operationId":"getEmailForwards","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Forwards","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"forwards":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","example":"info@example.com"},"forwardTo":{"type":"string","example":"me@example.net"},"created":{"type":"string","example":"2026-10-07 17:46:11"}}}},"limits":{"type":"object","description":"Forwards the domain has (`used`, including any Porkbun manages itself) and may have (`max`)","properties":{"used":{"type":"integer","example":2},"max":{"type":"integer","example":20}}},"dnsConfigured":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/getForwards\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/email\/addForward\/{domain}":{"post":{"tags":["Email"],"summary":"Add an email forward","description":"Forward an address at the domain to another mailbox. Free, up to the domain's limit (20 by default). Forwarding needs Porkbun's MX records and SPF include at the zone apex, and this sets them up. **When that would delete other apex MX records (another mail service) or rewrite an existing SPF record, the call is refused with `DNS_CHANGE_CONFIRMATION_REQUIRED` and `dnsChanges` describing exactly what would change; tell the user, then resend with `confirmDnsChanges: true`.** `dryRun: true` previews it, including whether confirmation is needed. Catch-all\/wildcard forwards are not supported. An address cannot be both a forward and an email hosting mailbox.","operationId":"emailAddForward","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["address","forwardTo"],"properties":{"address":{"type":"string","description":"The address at the domain to forward: the part before the @ (`info`) or the whole address (`info@example.com`)","example":"info"},"forwardTo":{"type":"string","description":"The mailbox to deliver to","example":"me@example.net"},"confirmDnsChanges":{"type":"boolean","description":"Agree to the DNS changes a DNS_CHANGE_CONFIRMATION_REQUIRED response described"},"dryRun":{"type":"boolean"}}}]}}}},"responses":{"200":{"description":"Forward added","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"address":{"type":"string"},"forwardTo":{"type":"string"},"dnsChanges":{"type":"object","description":"What setting up forwarding does to the zone apex","properties":{"mxRecordsRemoved":{"type":"array","description":"Apex MX records that are not Porkbun forwarding and are (or would be) deleted","items":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"prio":{"type":"string"}}}},"spf":{"type":"string","enum":["set","merge","unchanged","unchanged_over_limits"],"description":"set: Porkbun's SPF record is put in place; merge: include:_spf.porkbun.com is added to the existing SPF record (spfBefore\/spfAfter); unchanged: it is already there; unchanged_over_limits: adding it would exceed SPF limits, so the record is left alone and forwarded mail may fail SPF"},"spfBefore":{"type":"string"},"spfAfter":{"type":"string"}}},"limits":{"type":"object","description":"Forwards the domain has (`used`, including any Porkbun manages itself) and may have (`max`)","properties":{"used":{"type":"integer","example":2},"max":{"type":"integer","example":20}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/addForward\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"address\":\"info\",\"forwardTo\":\"me@example.net\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"email"}},"\/email\/deleteForward\/{domain}":{"post":{"tags":["Email"],"summary":"Delete an email forward","description":"Delete one forward, named by its `address` and `forwardTo` (an address can forward to several). When it was the domain's last forward and there are no email hosting mailboxes, Porkbun's forwarding MX and SPF records are removed too (`dnsRecordsRemoved: true`).","operationId":"emailDeleteForward","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["address","forwardTo"],"properties":{"address":{"type":"string","example":"info"},"forwardTo":{"type":"string","example":"me@example.net"}}}]}}}},"responses":{"200":{"description":"Forward deleted","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"address":{"type":"string"},"forwardTo":{"type":"string"},"dnsRecordsRemoved":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/deleteForward\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"address\":\"info\",\"forwardTo\":\"me@example.net\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"email"}},"\/email\/getMailboxes\/{domain}":{"post":{"tags":["Email"],"summary":"List mailboxes","description":"The domain's email hosting mailboxes, including ones bought or included as a trial that are still waiting to be set up (`status: PENDINGSETUP`, `address: null`). Also returns the price of another mailbox and the mail client settings. Supports GET with header auth.","operationId":"emailGetMailboxes","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Mailboxes","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mailboxes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","example":8095474},"address":{"type":"string","nullable":true,"description":"Null while the mailbox waits to be set up","example":"me@example.com"},"status":{"type":"string","enum":["PENDINGSETUP","ACTIVE","SUSPENDED","PAUSED"]},"trial":{"type":"boolean","description":"The free trial that comes with a registration"},"expires":{"type":"string","example":"2027-10-08 12:00:00"},"quota":{"type":"string","example":"10G"},"autoRenews":{"type":"boolean","description":"Renews on its own when due (set-up mailboxes)"},"renewalPrice":{"type":"integer","description":"A year's renewal in cents"}}}},"price":{"type":"object","properties":{"amount":{"type":"integer","example":3600},"formatted":{"type":"string"},"interval":{"type":"string","example":"year"}}},"clientSettings":{"type":"object","description":"Mail client settings: IMAP imap.porkbun.com:993 SSL\/TLS, POP pop.porkbun.com:995, SMTP smtp.porkbun.com:587 STARTTLS (or 465 implicit TLS, 50587), webmail https:\/\/webmail.porkbun.com; the username is the full address"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/getMailboxes\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"get":{"tags":["Email"],"summary":"List mailboxes","description":"The domain's email hosting mailboxes, including ones bought or included as a trial that are still waiting to be set up (`status: PENDINGSETUP`, `address: null`). Also returns the price of another mailbox and the mail client settings. Supports GET with header auth.","operationId":"getEmailMailboxes","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AuthRequest"}}}},"responses":{"200":{"description":"Mailboxes","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mailboxes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","example":8095474},"address":{"type":"string","nullable":true,"description":"Null while the mailbox waits to be set up","example":"me@example.com"},"status":{"type":"string","enum":["PENDINGSETUP","ACTIVE","SUSPENDED","PAUSED"]},"trial":{"type":"boolean","description":"The free trial that comes with a registration"},"expires":{"type":"string","example":"2027-10-08 12:00:00"},"quota":{"type":"string","example":"10G"},"autoRenews":{"type":"boolean","description":"Renews on its own when due (set-up mailboxes)"},"renewalPrice":{"type":"integer","description":"A year's renewal in cents"}}}},"price":{"type":"object","properties":{"amount":{"type":"integer","example":3600},"formatted":{"type":"string"},"interval":{"type":"string","example":"year"}}},"clientSettings":{"type":"object","description":"Mail client settings: IMAP imap.porkbun.com:993 SSL\/TLS, POP pop.porkbun.com:995, SMTP smtp.porkbun.com:587 STARTTLS (or 465 implicit TLS, 50587), webmail https:\/\/webmail.porkbun.com; the username is the full address"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/getMailboxes\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\"}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/email\/setupMailbox\/{domain}":{"post":{"tags":["Email"],"summary":"Set up a waiting mailbox","description":"Give a mailbox that is already paid for, or the free trial mailbox that comes with a domain registration, its address and password. Free. `mailboxId` picks one; without it the first waiting mailbox is used (`NO_PENDING_MAILBOX` if there is none: buy one with \/email\/createMailbox). Without `password`, a strong one is generated and returned once in `password`. Puts Porkbun's MX and SPF in place, asking first (`DNS_CHANGE_CONFIRMATION_REQUIRED`) if that would replace another mail service's MX or rewrite the SPF record. Needs a verified email, and a verified phone or an approved ID check. DKIM and DMARC are set up on the website.","operationId":"emailSetupMailbox","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["address"],"properties":{"address":{"type":"string","description":"The part before the @ (`me`) or the whole address. 1-30 characters: letters, digits and + . - _","example":"me"},"password":{"type":"string","description":"Optional. 12-72 bytes with an upper case letter, a lower case letter, a number and a special character. Leave out to have a strong one generated and returned once"},"confirmDnsChanges":{"type":"boolean","description":"Agree to DNS changes a DNS_CHANGE_CONFIRMATION_REQUIRED response described"},"dryRun":{"type":"boolean"},"mailboxId":{"type":"integer","description":"Which waiting mailbox (from \/email\/getMailboxes)"}}}]}}}},"responses":{"200":{"description":"Mailbox ready","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"mailbox":{"type":"object","properties":{"id":{"type":"integer","example":8095474},"address":{"type":"string","nullable":true,"description":"Null while the mailbox waits to be set up","example":"me@example.com"},"status":{"type":"string","enum":["PENDINGSETUP","ACTIVE","SUSPENDED","PAUSED"]},"trial":{"type":"boolean","description":"The free trial that comes with a registration"},"expires":{"type":"string","example":"2027-10-08 12:00:00"},"quota":{"type":"string","example":"10G"},"autoRenews":{"type":"boolean","description":"Renews on its own when due (set-up mailboxes)"},"renewalPrice":{"type":"integer","description":"A year's renewal in cents"}}},"password":{"type":"string","description":"Only when the password was generated (no `password` in the request). Shown once; give it to the user"},"passwordGenerated":{"type":"boolean"},"dnsChanges":{"type":"object","description":"Same shape as \/email\/addForward's dnsChanges: mxRecordsRemoved and spf"},"clientSettings":{"type":"object","description":"Mail client settings: IMAP imap.porkbun.com:993 SSL\/TLS, POP pop.porkbun.com:995, SMTP smtp.porkbun.com:587 STARTTLS (or 465 implicit TLS, 50587), webmail https:\/\/webmail.porkbun.com; the username is the full address"},"orderId":{"type":"integer","description":"createMailbox only"},"charged":{"type":"integer","description":"createMailbox only, cents"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/setupMailbox\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"address\":\"me\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"email"}},"\/email\/createMailbox\/{domain}":{"post":{"tags":["Email"],"summary":"Buy and set up a mailbox","description":"Buy one email hosting mailbox ($36.00 a year, renewing automatically; the current price is in \/email\/getMailboxes) and set it up in the same call. `cost` must equal the price in cents (`COST_MISMATCH` names it; `dryRun: true` with `cost: 0` quotes it). Paid from account credit by default, or directly with `payWith: \"card\"` \/ `\"usdc\"` as for domain purchases; counts toward the monthly API spend limit. Everything else is as \/email\/setupMailbox: generated password unless one is sent, DNS confirmation, verification. Accounts under 90 days old that have paid in crypto cannot buy it (`EMAIL_HOSTING_UNAVAILABLE`), as on the website. If the purchase succeeds but setup fails, the response says `setupComplete: false` with the `mailboxId` to finish with \/email\/setupMailbox.","operationId":"emailCreateMailbox","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["address","cost","agreeToTerms"],"properties":{"address":{"type":"string","description":"The part before the @ (`me`) or the whole address. 1-30 characters: letters, digits and + . - _","example":"me"},"password":{"type":"string","description":"Optional. 12-72 bytes with an upper case letter, a lower case letter, a number and a special character. Leave out to have a strong one generated and returned once"},"confirmDnsChanges":{"type":"boolean","description":"Agree to DNS changes a DNS_CHANGE_CONFIRMATION_REQUIRED response described"},"dryRun":{"type":"boolean"},"cost":{"type":"integer","description":"The price in cents, 3600","example":3600},"agreeToTerms":{"type":"string","enum":["yes"]},"payWith":{"type":"string","enum":["card","usdc"],"description":"Pay directly instead of from account credit; see \/domain\/create"},"cardPaymentId":{"type":"string"},"usdcCheckoutId":{"type":"string"}}}]}}}},"responses":{"200":{"description":"Mailbox bought and ready","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"mailbox":{"type":"object","properties":{"id":{"type":"integer","example":8095474},"address":{"type":"string","nullable":true,"description":"Null while the mailbox waits to be set up","example":"me@example.com"},"status":{"type":"string","enum":["PENDINGSETUP","ACTIVE","SUSPENDED","PAUSED"]},"trial":{"type":"boolean","description":"The free trial that comes with a registration"},"expires":{"type":"string","example":"2027-10-08 12:00:00"},"quota":{"type":"string","example":"10G"},"autoRenews":{"type":"boolean","description":"Renews on its own when due (set-up mailboxes)"},"renewalPrice":{"type":"integer","description":"A year's renewal in cents"}}},"password":{"type":"string","description":"Only when the password was generated (no `password` in the request). Shown once; give it to the user"},"passwordGenerated":{"type":"boolean"},"dnsChanges":{"type":"object","description":"Same shape as \/email\/addForward's dnsChanges: mxRecordsRemoved and spf"},"clientSettings":{"type":"object","description":"Mail client settings: IMAP imap.porkbun.com:993 SSL\/TLS, POP pop.porkbun.com:995, SMTP smtp.porkbun.com:587 STARTTLS (or 465 implicit TLS, 50587), webmail https:\/\/webmail.porkbun.com; the username is the full address"},"orderId":{"type":"integer","description":"createMailbox only"},"charged":{"type":"integer","description":"createMailbox only, cents"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"402":{"description":"payWith: payment required","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/createMailbox\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"address\":\"me\",\"cost\":3600,\"agreeToTerms\":\"yes\"}'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/email\/renewMailbox\/{domain}":{"post":{"tags":["Email"],"summary":"Renew a mailbox for a year","description":"Add a year to a mailbox now, at its renewal price (`renewalPrice` in \/email\/getMailboxes; $36.00, or the legacy price for mailboxes that still qualify). A set-up mailbox also renews on its own when it is due (`autoRenews`), charged like any other renewal; this is for paying early, or for converting a trial to paid. Name the mailbox with `mailboxId` or `address`. `cost` must equal the price (`dryRun: true` with `cost: 0` quotes it and returns `expiresAfter`). Paid from account credit or with `payWith: \"card\"` \/ `\"usdc\"`, within the monthly API spend limit. The year is added to the current expiry date. While the domain is expired the mailbox can't be renewed, by this call or automatically: renew the domain first (`DOMAIN_EXPIRED`).","operationId":"emailRenewMailbox","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["cost","agreeToTerms"],"properties":{"mailboxId":{"type":"integer"},"address":{"type":"string","description":"The mailbox address, or the part before the @","example":"me"},"cost":{"type":"integer","example":3600},"agreeToTerms":{"type":"string","enum":["yes"]},"payWith":{"type":"string","enum":["card","usdc"]},"cardPaymentId":{"type":"string"},"usdcCheckoutId":{"type":"string"},"dryRun":{"type":"boolean"}}}]}}}},"responses":{"200":{"description":"Renewed","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mailbox":{"type":"object"},"orderId":{"type":"integer"},"charged":{"type":"integer"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"402":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/renewMailbox\/example.com \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"address\":\"me\",\"cost\":3600,\"agreeToTerms\":\"yes\"}'"}],"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"purchase"}},"\/email\/setPassword":{"post":{"tags":["Email"],"summary":"Set email hosting password","description":"Set the password for an email hosting account associated with a domain managed by your API key.","operationId":"emailSetPassword","requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["emailAddress","password"],"properties":{"emailAddress":{"type":"string","description":"The full email address (e.g. user@example.com)","example":"user@example.com"},"password":{"type":"string","description":"The new password. Must pass Porkbun password validation rules."}}}]}}}},"responses":{"200":{"description":"Password updated","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BasicResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation or permission error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/email\/setPassword \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"emailAddress\":\"user@example.com\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"email"}},"\/marketplace\/getAll":{"get":{"tags":["Marketplace"],"summary":"List marketplace domains","description":"Retrieve domains listed on the Porkbun marketplace. Two modes:\n\n- **Unfiltered (default):** paginated raw listing, up to 5000 entries per call via `start` \/ `limit`.\n- **Filtered:** when any of `query`, `tlds`, `sldLengthMin`, `sldLengthMax`, or `sortName` is provided, results are filtered server-side. Filtered mode returns up to 1000 matching listings (matches the web UI's marketplace search).\n\n`query` supports `+include` and `-exclude` prefixes per word against the SLD (e.g. `+ai -test` matches SLDs containing 'ai' but not 'test'). Token-based access is not supported.\n\nOn GET, authenticate with the `X-API-Key` and `X-Secret-API-Key` headers and pass the filters as query parameters. For multi-value `tlds`, use bracket syntax: `?tlds[]=com&tlds[]=io`.","operationId":"listMarketplaceListingsGet","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"query","in":"query","required":false,"schema":{"type":"string"},"description":"SLD substring search. Multi-word; prefix a term with `-` to exclude. Example: `ai -test`."},{"name":"tlds","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true,"description":"Filter to listings under these TLDs (without leading dot). Use bracket form: `?tlds[]=com&tlds[]=io`."},{"name":"sldLengthMin","in":"query","required":false,"schema":{"type":"integer"},"description":"Minimum SLD character length."},{"name":"sldLengthMax","in":"query","required":false,"schema":{"type":"integer"},"description":"Maximum SLD character length."},{"name":"sortName","in":"query","required":false,"schema":{"type":"string","enum":["domain","tld","price","sld_length"]},"description":"Field to sort filtered results by."},{"name":"sortDirection","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]},"description":"Sort direction."},{"name":"start","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Pagination offset (unfiltered mode only)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":5000},"description":"Page size (unfiltered mode only). Default 1000, max 5000."}],"responses":{"200":{"description":"Marketplace domain list (same shape as POST).","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"count":{"type":"integer","description":"Number of domains returned in this response"},"filtered":{"type":"boolean","description":"True when one or more filter parameters were applied (query, tlds, sldLengthMin\/Max, sortName)."},"domains":{"type":"array","items":{"type":"object","properties":{"create_date":{"type":"string","description":"Date the listing was created"},"domain":{"type":"string"},"tld":{"type":"string"},"sld_length":{"type":"integer","description":"Character length of the SLD (the part of the domain before the TLD)"},"price":{"type":"number","description":"Listing price in USD"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/marketplace\/getAll?query=ai&tlds[]=com&tlds[]=io&sldLengthMax=6&sortName=price&sortDirection=asc' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"},"post":{"tags":["Marketplace"],"summary":"List marketplace domains","description":"Retrieve domains listed on the Porkbun marketplace. Two modes:\n\n- **Unfiltered (default):** paginated raw listing, up to 5000 entries per call via `start` \/ `limit`.\n- **Filtered:** when any of `query`, `tlds`, `sldLengthMin`, `sldLengthMax`, or `sortName` is provided, results are filtered server-side. Filtered mode returns up to 1000 matching listings (matches the web UI's marketplace search).\n\n`query` supports `+include` and `-exclude` prefixes per word against the SLD (e.g. `+ai -test` matches SLDs containing 'ai' but not 'test'). Token-based access is not supported.","operationId":"listMarketplaceListings","requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","properties":{"start":{"type":"integer","description":"Pagination offset (unfiltered mode only). Default 0.","example":0},"limit":{"type":"integer","description":"Number of domains to return (unfiltered mode only). Default 1000, max 5000.","example":1000,"maximum":5000},"query":{"type":"string","description":"Search string. Each space-separated term filters by SLD substring. Prefix a term with `-` to exclude it.","example":"ai -test"},"tlds":{"type":"array","items":{"type":"string"},"description":"Filter to listings under these TLDs (without the leading dot).","example":["com","io"]},"sldLengthMin":{"type":"integer","description":"Filter to listings whose SLD has at least this many characters.","example":3},"sldLengthMax":{"type":"integer","description":"Filter to listings whose SLD has at most this many characters.","example":8},"sortName":{"type":"string","enum":["domain","tld","price","sld_length"],"description":"Field to sort filtered results by. Default `sld_length` ascending when `query` is set, otherwise `create_date` descending."},"sortDirection":{"type":"string","enum":["asc","desc"],"description":"Sort direction. Defaults vary by `sortName`."}}}]}}}},"responses":{"200":{"description":"Marketplace domain list","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"count":{"type":"integer","description":"Number of domains returned in this response"},"filtered":{"type":"boolean","description":"True when one or more filter parameters were applied (query, tlds, sldLengthMin\/Max, sortName)."},"domains":{"type":"array","items":{"type":"object","properties":{"create_date":{"type":"string","description":"Date the listing was created"},"domain":{"type":"string"},"tld":{"type":"string"},"sld_length":{"type":"integer","description":"Character length of the SLD (the part of the domain before the TLD)"},"price":{"type":"number","description":"Listing price in USD"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Authentication error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/marketplace\/getAll \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"start\":0,\"limit\":1000}'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"read"}},"\/account\/autoTopup":{"get":{"tags":["Account"],"summary":"Read auto top-up settings","operationId":"getAutoTopup","description":"What auto top-up is set to, whether a payment method is on file, and what `POST \/account\/topup` would charge right now (`effectiveAmount`).\n\nAuto top-up is how unattended work stops dead-ending on `INSUFFICIENT_FUNDS`: when an order drops the balance below `threshold`, Porkbun charges the saved payment method for `amount` and adds the credit.\n\n`cards` lists the saved payment methods, primary first, with each one's `status`: a top-up charges the primary and falls back to the backups in that order. Use it to explain a decline, or to warn the account holder before a card stops working (`expired`, or `declined` with the decline code).\n\nIf `paymentMethodOnFile` is false the settings are inert and the response says so in `warnings` \u2014 a card can only be saved on the website, never over the API. Cards are also added, removed and reordered only on the website. When every card is expired or recently declined, `warnings` says a top-up will probably fail.","responses":{"200":{"description":"Current settings","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AutoTopupResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"funding"},"post":{"tags":["Account"],"summary":"Configure auto top-up","operationId":"setAutoTopup","description":"Set the top-up amount, and\/or switch auto top-up on or off.\n\n`amount` stands on its own. It is what a top-up adds, and `POST \/account\/topup` charges the same figure on demand, so setting it without automating anything is a normal call: `{\"amount\": 10000}`.\n\n`enabled: true` additionally makes it fire by itself and requires `threshold` (the balance, in integer US cents, below which a top-up happens) plus an amount \u2014 either in the same call or already on file. `enabled: false` stops it firing on a threshold and **keeps the amount**, because on-demand top-ups still use it.\n\n**`amount` set over the API is capped at $500** ($5 minimum), because that value is also what `POST \/account\/topup` charges \u2014 an API key free to set it to anything would be setting its own limit, which is not a limit. A larger amount set by the account holder at porkbun.com\/account\/api is honoured as-is.\n\nNo card is touched here, and one cannot be added over the API. Supports `dryRun`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["enabled"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"enabled":{"type":"boolean","description":"Turn auto top-up on or off. Omit to change only the amount."},"threshold":{"type":"integer","description":"Balance in integer US cents below which a top-up fires. Required when enabling, and meaningless without it.","example":2000},"amount":{"type":"integer","description":"What a top-up adds, in integer US cents. Can be sent on its own; required when enabling if none is on file. Over the API: 500-50000.","example":5000},"dryRun":{"type":"boolean","description":"Validate only; change nothing."}}}}}},"responses":{"200":{"description":"Settings after the change","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AutoTopupResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"funding"}},"\/account\/topup":{"post":{"tags":["Account"],"summary":"Top up account credit now","operationId":"topupAccountCredit","description":"**Charges the saved payment method** and adds the money to account credit immediately. The call that unblocks work already in progress: enabling auto top-up does nothing until the next order trips the threshold, which is no help to a caller holding an `INSUFFICIENT_FUNDS` response right now.\n\n**`amount` is optional.** Omitted, it charges the account's configured top-up amount, or $50 if the account has never set one \u2014 `amountSource` in the response says which of `configured`, `default` or `request` applied. Supplied, it charges that figure for this call only and leaves the stored setting alone, which is the point: an agent could always have written the amount to `\/account\/autoTopup` first, and making it do that turns a one-off charge into a silent edit of a setting the customer owns.\n\nThe card itself can only be saved outside the API, and a supplied amount is held to the same 500\u201350000 cent range as one set through `\/account\/autoTopup`. On top of that the dollars are bounded by the month: the account's **monthly spend limit** caps top-ups as well as domain spend (it is the account saying how much the API may move, and a card charge is the API moving money), and an account that has never set one still gets a $100\/month ceiling. Card, auto top-ups set off by an API purchase, MPP and USDC all count toward it \u2014 `monthlyCeiling` and `ceilingSource` on `GET \/account\/autoTopup` say which is in force. Top-ups are also limited to 5 per day and 20 per month (`TOPUP_LIMIT_EXCEEDED`), and every successful charge emails the account holder.\n\n**Primary and backup cards.** An account can save up to three cards on the website, one primary and the rest backups. A top-up charges the primary; if it is declined, it tries each backup in turn and charges at most one card. It does not fall back after a decline that suggests fraud (Radar blocks, lost or stolen cards). The response names the card that paid (`card`), sets `usedBackup` when a backup stepped in, and lists any card declined first in `declinedFirst`, so you can tell the account holder their primary card needs attention. `GET \/account\/autoTopup` lists the saved cards.\n\nErrors that matter: `NO_PAYMENT_METHOD` (nothing saved to charge, or every card is marked as no longer chargeable \u2014 the account holder has to add a card or buy credit on the website), `CARD_DECLINED` (every card tried was declined and nothing was added; `declined` lists each card with its decline code), `TOPUP_FAILED` (nothing was charged; retry once).\n\nWith a sandbox key this grants simulated credit and charges nothing (`simulated: true`). Supports `dryRun`, which previews the amount and charges nothing in any environment. Honours `Idempotency-Key`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"amount":{"type":"integer","description":"Optional one-off amount in integer US cents (500-50000). Omit to charge the amount the account has configured, which is the common case. A supplied amount does NOT change the stored setting \u2014 use it instead of rewriting the customer's configuration for a single charge.","example":2500},"dryRun":{"type":"boolean","description":"Preview the charge without making it."}}}}}},"responses":{"200":{"description":"Charged and credited","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"charged":{"type":"integer","description":"Amount charged, in cents.","example":5000},"chargedDisplay":{"type":"string","example":"$50.00"},"balance":{"type":"integer","description":"Account credit balance after the top-up, in cents.","example":5000},"balanceDisplay":{"type":"string","example":"$50.00"},"orderId":{"type":"integer","description":"The captured order recorded for the purchase, 0 if order creation failed (the credit is granted either way)."},"usedDefaultAmount":{"type":"boolean","description":"True when the account had no configured amount and the $50 default was charged."},"chargesToday":{"type":"integer"},"chargesThisMonth":{"type":"integer"},"simulated":{"type":"boolean","description":"Sandbox only: credit was granted without charging a card."},"message":{"type":"string"},"card":{"$ref":"#\/components\/schemas\/ChargedCard"},"usedBackup":{"type":"boolean","description":"True when the primary card was declined and a backup paid. The declined card is in `declinedFirst`."},"declinedFirst":{"type":"array","items":{"$ref":"#\/components\/schemas\/DeclinedCard"},"description":"Cards tried and declined before `card` paid. Empty when the primary paid."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"No payment method, card declined, or a limit reached","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":true,"destructive":false,"reversible":false,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"funding"}},"\/account\/invite":{"post":{"tags":["Account"],"summary":"Create an account registration invite","operationId":"createAccountInvite","description":"Generates a one-time invite token and URL that you send to a prospective user. When the user visits the URL they go through Porkbun's normal account creation flow \u2014 including CAPTCHA, address collection, and TOS acceptance \u2014 in the browser. The invite expires in 48 hours. Each token can only be used once.\n\nOptionally supply an `email` to pre-fill the email field on the registration form.\n\nOptionally supply a `returnUrl` (must be HTTPS) to redirect the user back to your platform after they complete registration.\n\nAfter sending the invite URL, poll `\/account\/inviteStatus` to check whether the account was created.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string","description":"Your API key"},"secretapikey":{"type":"string","description":"Your secret API key"},"email":{"type":"string","format":"email","description":"Email address to pre-fill on the registration form (optional)"},"returnUrl":{"type":"string","format":"uri","description":"HTTPS URL to redirect the user to after successful registration (optional). Use this to send users back to your platform after they complete account creation.","example":"https:\/\/yourapp.com\/onboarding\/complete"}}}}}},"responses":{"200":{"description":"Invite created","content":{"application\/json":{"schema":{"type":"object","required":["status","inviteToken","inviteUrl","expires"],"properties":{"status":{"type":"string","example":"SUCCESS"},"inviteToken":{"type":"string","description":"Opaque token \u2014 pass to `\/account\/inviteStatus` to track completion","example":"a3f8c2..."},"inviteUrl":{"type":"string","format":"uri","description":"URL to send to the prospective user. Opens Porkbun's standard registration page.","example":"https:\/\/porkbun.com\/account\/create?invite=a3f8c2..."},"expires":{"type":"string","format":"date-time","description":"UTC datetime when the invite token expires (48 hours from creation)","example":"2024-01-16 14:30:00"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Authentication failed","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/account\/invite \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"email\":\"newuser@example.com\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/account\/inviteStatus":{"get":{"tags":["Account"],"summary":"Check account invite status","operationId":"getAccountInviteStatus","description":"Returns the current status of a registration invite created by your API key.\n\n- `PENDING` \u2014 the invite URL has not yet been used\n- `ACCEPTED` \u2014 the user completed registration; `newAccountId` contains their account ID\n- `EXPIRED` \u2014 the invite was not used within 48 hours or was canceled\n\nYou can only query invites created by your own API key. Pass credentials via `X-API-Key` and `X-Secret-API-Key` headers.","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"The `inviteToken` returned by `\/account\/invite`"},{"name":"X-API-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Your API key"},{"name":"X-Secret-API-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Your secret API key"}],"responses":{"200":{"description":"Status returned","content":{"application\/json":{"schema":{"type":"object","required":["status","inviteStatus"],"properties":{"status":{"type":"string","example":"SUCCESS"},"inviteStatus":{"type":"string","enum":["PENDING","ACCEPTED","EXPIRED"],"description":"Current state of the invite"},"newAccountId":{"type":"integer","description":"ID of the newly created account. Present only when `inviteStatus` is `ACCEPTED`.","example":12345}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Authentication failed or token not found \/ not owned by caller","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/inviteStatus?token=a3f8c2...' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/balance":{"get":{"tags":["Account"],"summary":"Get account balance","description":"Returns the available account credit balance. Authenticate using `X-API-Key` and `X-Secret-API-Key` headers, or `Authorization: Bearer <token>`.","operationId":"getBalance","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Account balance.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/BalanceResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/balance' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/invoices":{"get":{"tags":["Account"],"summary":"List invoices","description":"The account's invoices (one per order), newest first, like the orders page on porkbun.com. `total_cents` is what the customer was left paying: refunds are netted out, and `state` says whether the invoice is paid, refunded or partially refunded. Any of the account's API keys can read them.","operationId":"listInvoices","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"year","in":"query","required":false,"schema":{"type":"integer"},"description":"Only invoices from this year, e.g. 2026."},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0},"description":"Offset for paging."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"Invoices.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"date":{"type":"string","example":"2026-09-30"},"state":{"type":"string","enum":["PAID","PARTIALLY_REFUNDED","REFUNDED","UNPAID"]},"total_cents":{"type":"integer","example":1108},"total":{"type":"string","example":"$11.08"},"itemCount":{"type":"integer"},"domains":{"type":"array","items":{"type":"string"},"description":"Up to 10 domains on the invoice."},"url":{"type":"string","nullable":true,"description":"The invoice page on porkbun.com (null on a sandbox key)."}}}},"total":{"type":"integer","description":"How many invoices match (for paging)."},"start":{"type":"integer"},"limit":{"type":"integer"},"years":{"type":"array","items":{"type":"integer"},"description":"Years that have invoices."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/invoices?year=2026' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/invoice\/{orderId}":{"get":{"tags":["Account"],"summary":"Get an invoice","description":"One invoice as data: bill-to details, payment method (card brand and last four only), each line with its term and resulting domain expiry, and gross, refunded and net totals. The same figures as the PDF.","operationId":"getInvoice","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"orderId","in":"path","required":true,"schema":{"type":"integer"},"description":"Invoice (order) ID, from `GET \/account\/invoices`."}],"responses":{"200":{"description":"The invoice.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"invoice":{"type":"object","properties":{"id":{"type":"integer"},"date":{"type":"string"},"state":{"type":"string","enum":["PAID","PARTIALLY_REFUNDED","REFUNDED","UNPAID"]},"billTo":{"type":"object","properties":{"company":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"vat":{"type":"string","nullable":true}}},"paymentMethods":{"type":"array","items":{"type":"string"},"example":["Visa ending in 4242"]},"items":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"product":{"type":"string","example":"Domain Registration"},"type":{"type":"string"},"years":{"type":"integer","nullable":true},"expires":{"type":"string","nullable":true,"description":"The domain expiry this line produced (YYYY-MM-DD), on lines that buy a domain term. Stays the same after later renewals. Null on older lines where it can no longer be determined."},"status":{"type":"string","enum":["SUCCESS","NOT_PROCESSED","REFUNDED","REFUND","COUPON"]},"price_cents":{"type":"integer"},"discount_cents":{"type":"integer","description":"Discount on this line; on a REFUND line, the amount refunded."},"tags":{"type":"array","items":{"type":"string"},"example":["PREMIUM"]}}}},"gross_cents":{"type":"integer"},"refunded_cents":{"type":"integer"},"total_cents":{"type":"integer"},"total":{"type":"string"},"currency":{"type":"string","example":"USD"},"url":{"type":"string","nullable":true},"pdfUrl":{"type":"string","nullable":true},"downloadUrl":{"type":"string","nullable":true,"description":"Downloads the PDF without signing in, for 15 minutes (null on a sandbox key). The link to hand a user."},"downloadExpires":{"type":"string","nullable":true,"description":"When downloadUrl stops working (ISO 8601)."}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"`INVOICE_NOT_FOUND`: no order with that ID belongs to this account."},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/invoice\/9913779' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/invoicePdf\/{orderId}":{"get":{"tags":["Account"],"summary":"Download an invoice PDF","description":"The invoice as a PDF, base64-encoded in `contentBase64`: the same document as Download PDF on porkbun.com. Decode it and save it as `filename`.","operationId":"getInvoicePdf","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"orderId","in":"path","required":true,"schema":{"type":"integer"},"description":"Invoice (order) ID, from `GET \/account\/invoices`."}],"responses":{"200":{"description":"The PDF.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"filename":{"type":"string","example":"porkbun-invoice-9913779.pdf"},"contentType":{"type":"string","example":"application\/pdf"},"sizeBytes":{"type":"integer"},"contentBase64":{"type":"string"},"downloadUrl":{"type":"string","nullable":true,"description":"Downloads the PDF without signing in, for 15 minutes (null on a sandbox key). The link to hand a user."},"downloadExpires":{"type":"string","nullable":true,"description":"When downloadUrl stops working (ISO 8601)."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"`INVOICE_NOT_FOUND`: no order with that ID belongs to this account."},"500":{"description":"`INVOICE_PDF_FAILED`: the PDF could not be generated; retry, or read the same data from `GET \/account\/invoice\/{orderId}`."},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/invoicePdf\/9913779' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/topupCrypto":{"post":{"tags":["Account"],"summary":"Top up credit with USDC (x402)","description":"Opens a Coinbase checkout to add account credit with USDC on Base, the same as Use Crypto on porkbun.com. Nothing is charged by this call. Pay it either way:\n\n- `x402Url`: an AI agent with a wallet pays it directly over x402 (a gasless USDC transfer on Base, no page or person involved). This is how an agent gets past `INSUFFICIENT_FUNDS` on an account with no saved card.\n- `payUrl`: a Coinbase page where a person pays.\n\nCredit is added when the payment confirms (usually within a minute), less Coinbase's fee of about 1% (`estimatedFee_cents`, `estimatedCredit_cents`). To cover an `INSUFFICIENT_FUNDS` shortfall exactly, open the checkout for its `usdcAmountToCover`. Check `GET \/account\/topupCryptoStatus\/{checkoutId}`. Payments are on-chain and cannot be reversed. Needs a verified email and phone and an account that is not bank-transfer-only (`CRYPTO_NOT_AVAILABLE` otherwise). Up to 10 checkouts per account per day. USDC counts toward the same monthly ceiling as card and MPP top-ups (the monthly spend limit, or $100 when none is set), checked when the checkout is opened, with unpaid checkouts from the last day counted as if paid (`TOPUP_LIMIT_EXCEEDED`). Not available on sandbox keys (`SANDBOX_UNSUPPORTED`; use `\/sandbox\/topup`).","operationId":"topupCrypto","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","minimum":100,"maximum":50000,"description":"Checkout amount in integer US cents, $1.00 to $500.00 (100 = $1.00). Credit is this less Coinbase's fee of about 1%."},"dryRun":{"type":"boolean","description":"Check eligibility only; creates no checkout."}}}}}},"responses":{"200":{"description":"Checkout created.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"checkoutId":{"type":"string"},"amount_cents":{"type":"integer"},"estimatedFee_cents":{"type":"integer","description":"Coinbase's fee, about 1%, estimated high."},"estimatedCredit_cents":{"type":"integer","description":"Credit the account gets once it is paid."},"currency":{"type":"string","example":"USDC"},"network":{"type":"string","example":"base"},"payUrl":{"type":"string","description":"Coinbase-hosted page for a person to pay."},"x402Url":{"type":"string","nullable":true,"description":"x402 payment URL an agent's wallet pays directly."},"expiresAt":{"type":"string","nullable":true},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"funding"}},"\/account\/topupCryptoStatus\/{checkoutId}":{"get":{"tags":["Account"],"summary":"Status of a USDC top-up","description":"Whether a checkout from `POST \/account\/topupCrypto` has been paid and credited. `state`: ACTIVE (not paid), PROCESSING (paid, being credited), COMPLETED (credited), EXPIRED or FAILED. Returns the current balance too.","operationId":"topupCryptoStatus","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"name":"checkoutId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"checkoutId":{"type":"string"},"state":{"type":"string","enum":["ACTIVE","PROCESSING","COMPLETED","EXPIRED","FAILED","DEACTIVATED"]},"credited":{"type":"boolean"},"balance_cents":{"type":"integer"},"balance":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"`CHECKOUT_NOT_FOUND`"},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/account\/verifyPhone\/send":{"post":{"tags":["Account"],"summary":"Text a verification code to the account's phone","operationId":"accountVerifyPhoneSend","description":"Purchases and top-ups need a verified email address and phone number, and accounts are not asked to verify the phone at signup, so `VERIFICATION_REQUIRED` is often the first error a new account sees. This texts (or calls) a code to the phone number already on the account; ask the person for it and pass it to `POST \/account\/verifyPhone\/confirm`. The number cannot be chosen or changed over the API.\n\nOne live code at a time: a second request within 10 minutes returns `codeAlreadySent` instead of texting again (send `\"channel\": \"call\"` if the text never arrived). Up to 5 sends a day (`PHONE_VERIFY_LIMIT`). Numbers in countries where the website requires a CAPTCHA for phone verification, or accounts with no phone number, get `PHONE_VERIFY_UNAVAILABLE` and verify on the website. Already verified: `alreadyVerified: true`. Not available on sandbox keys.","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"channel":{"type":"string","enum":["sms","call"],"default":"sms","description":"`call` reads the code out by voice, for when a text does not arrive."},"dryRun":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Code sent, already sent, or already verified.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"sent":{"type":"boolean"},"codeAlreadySent":{"type":"boolean"},"alreadyVerified":{"type":"boolean"},"channel":{"type":"string"},"sentTo":{"type":"string","description":"The number, masked (`+1 \u2022\u2022\u2022 42`)."},"expiresInMinutes":{"type":"integer"},"retryAfterSeconds":{"type":"integer"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"account"}},"\/account\/verifyPhone\/confirm":{"post":{"tags":["Account"],"summary":"Confirm the phone verification code","operationId":"accountVerifyPhoneConfirm","description":"Marks the account's phone number verified with the code from `POST \/account\/verifyPhone\/send`. A wrong or expired code returns `PHONE_CODE_INVALID`; 10 wrong codes in an hour lock it for the hour (`PHONE_VERIFY_LIMIT`). The response says whether the email address is verified too, since purchases need both; verify the email the same way with `POST \/account\/verifyEmail\/send` and `\/confirm`.","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"The code the person received, exactly as texted. Case does not matter."}}}}}},"responses":{"200":{"description":"Verified.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"verified":{"type":"boolean"},"alreadyVerified":{"type":"boolean"},"emailVerified":{"type":"boolean"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"account"}},"\/account\/verifyEmail\/send":{"post":{"tags":["Account"],"summary":"Email a verification code to the account's address","operationId":"accountVerifyEmailSend","description":"Purchases and top-ups need a verified email address and phone number (`VERIFICATION_REQUIRED`). This emails the usual verification message, a link plus a code, to the address on the account (or to a new address waiting for verification). Whoever reads that inbox, the person or an agent with access to it, passes the code to `POST \/account\/verifyEmail\/confirm`; clicking the link works too. The address cannot be chosen or changed over the API, and the code is never returned by the API or in a webhook: it only proves anything because it arrives in that inbox.\n\nA second request within 10 minutes returns `codeAlreadySent` instead of emailing again; the earlier code stays valid for 3 days. Accounts subscribed to `account.verification.code_sent` get a webhook when the email goes out. Already verified: `alreadyVerified: true`. Not available on sandbox keys.","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":[]}}}},"responses":{"200":{"description":"Email sent, already sent, or already verified.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"sent":{"type":"boolean"},"codeAlreadySent":{"type":"boolean"},"alreadyVerified":{"type":"boolean"},"sentTo":{"type":"string","description":"The address, masked (`o\u2022\u2022\u2022@example.com`)."},"expiresInMinutes":{"type":"integer"},"retryAfterSeconds":{"type":"integer"},"message":{"type":"string"},"phoneVerified":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"account"}},"\/account\/verifyEmail\/confirm":{"post":{"tags":["Account"],"summary":"Confirm the email verification code","operationId":"accountVerifyEmailConfirm","description":"Marks the account's email address verified with the code from the verification email (`POST \/account\/verifyEmail\/send`, or any verification email sent in the last 3 days). The code must belong to this account and to the address being verified. A wrong, expired or superseded code returns `EMAIL_CODE_INVALID`; 10 wrong codes in an hour lock it for the hour (`EMAIL_VERIFY_LIMIT`). The response says whether the phone is verified too, since purchases need both. Accounts subscribed to `account.verification.completed` get a webhook.","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"The verification code from the email, exactly as written."}}}}}},"responses":{"200":{"description":"Verified.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"verified":{"type":"boolean"},"alreadyVerified":{"type":"boolean"},"message":{"type":"string"},"phoneVerified":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"account"}},"\/account\/topupMpp":{"post":{"tags":["Account"],"summary":"Top up credit with the user's card, paid by an agent (MPP)","description":"Returns a signed, single-use payment link (valid 15 minutes, bound to this account and amount) that an AI agent pays with the user's **card** over MPP, the Machine Payments Protocol (mpp.dev), using a Stripe Shared Payment Token from its agent wallet (Stripe's Link Agent Wallet). No card needs to be saved at Porkbun. Nothing is charged by this call.\n\nThe agent's MPP client requests `payUrl` (POST) and gets HTTP `402` with a `WWW-Authenticate: Payment` challenge (`method=\"stripe\"`, `intent=\"charge\"`); its wallet mints a token for the user's card, the client retries with `Authorization: Payment <credential>`, and the credit is added at once: the response carries `credited_cents` and the new `balance_cents`, with a `Payment-Receipt` header. For example: `npx @stripe\/link-cli mpp pay <payUrl> -X POST --context \"...\"`. Newer Link CLI versions stop after creating the spend request: once the person approves in Link, complete it with `link-cli mpp pay <payUrl> --spend-request-id <lsrq_id> -X POST`. Link refuses a `--context` shorter than 100 characters: it is what the user reads when approving the spend, so say what it is for and the dollar amount, worded fresh for each payment (Link can refuse an identical repeat).\n\nSame limits as card top-ups (`POST \/account\/topup`): $5-$500, the monthly ceiling, 5 a day and 20 a month (`TOPUP_LIMIT_EXCEEDED`). Not available on sandbox keys (`SANDBOX_UNSUPPORTED`).","operationId":"topupMpp","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","minimum":500,"maximum":50000,"description":"Credit to add, in integer US cents."},"dryRun":{"type":"boolean","description":"Check eligibility and limits only."}}}}}},"responses":{"200":{"description":"Payment link created.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"amount_cents":{"type":"integer"},"payUrl":{"type":"string","description":"Signed MPP payment URL; POST it with an MPP client."},"expiresAt":{"type":"string"},"protocol":{"type":"string","example":"mpp"},"methods":{"type":"array","items":{"type":"string"}},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"funding"}},"\/account\/apiSettings":{"get":{"tags":["Account"],"summary":"Get API spend settings","description":"Returns the account's API spend control settings, this month's spend, and the limits actually enforced: `spendLimit` on purchases and `topupLimit` on credit added over the API. The account holder sets the limit on the website (https:\/\/porkbun.com\/account\/api); it cannot be changed over the API. All amounts are in cents. Authenticate using `X-API-Key` and `X-Secret-API-Key` headers, or `Authorization: Bearer <token>`.","operationId":"getApiSettings","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"API spend settings and current month spend.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ApiSettingsResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/account\/apiSettings' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/eventTypes":{"get":{"tags":["Webhooks"],"summary":"List subscribable event types","description":"Return the catalog of event types a webhook endpoint can subscribe to. Read-only; supports GET (header auth) or POST (body or header auth).","operationId":"webhookEventTypes","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Event type catalog.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookEventTypesResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/eventTypes' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/list":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints","description":"List all webhook endpoints registered on the authenticated account, with delivery health. Each endpoint's signing secret is included for keys with full access or the account scope; read-only keys get it omitted. Read-only; supports GET (header auth) or POST (body or header auth).","operationId":"webhookList","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Your webhook endpoints.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookListResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/list' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/get\/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook endpoint","description":"Fetch a single webhook endpoint by id, with delivery health. The signing secret is included for keys with full access or the account scope; read-only keys get it omitted. Read-only; supports GET (header auth) or POST (body or header auth).","operationId":"webhookGet","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"example":42},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"The webhook endpoint.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookEndpointResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/get\/42' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/create":{"post":{"tags":["Webhooks"],"summary":"Create a webhook endpoint","description":"Register an HTTPS endpoint to receive signed event payloads. The response includes the generated `secret` \u2014 store it securely; it is the HMAC key used to verify the `X-Porkbun-Signature` header. Omit `events` (or pass `[\"*\"]`) to subscribe to all event types. Maximum 20 endpoints per account. **URL requirements:** the endpoint must be an `https:\/\/` URL on the standard port 443, with a hostname that resolves to a public internet address. Private, loopback, link-local, CGNAT and other reserved ranges are refused (`INVALID_WEBHOOK_URL`) because Porkbun delivers from inside its own network. Credentials in the URL (`https:\/\/user:pass@host`) are also refused \u2014 authenticate the receiver by verifying the `X-Porkbun-Signature` HMAC instead. A hostname that does not resolve yet is accepted, so you can register the endpoint before the receiver is deployed, but delivery is re-checked against these rules immediately before each request.","operationId":"webhookCreate","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateWebhookRequest"}}}},"responses":{"200":{"description":"Endpoint created (includes the signing secret).","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookEndpointResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Validation error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/create \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"url\":\"https:\/\/example.com\/porkbun\/webhook\",\"events\":[\"*\"]}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/webhook\/update":{"post":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","description":"Update an endpoint's URL, event subscriptions, and\/or status. Only the supplied fields change. Set `status` to `DISABLED` to pause deliveries or `ACTIVE` to resume (resuming also resets the consecutive-failure counter). **URL requirements:** the endpoint must be an `https:\/\/` URL on the standard port 443, with a hostname that resolves to a public internet address. Private, loopback, link-local, CGNAT and other reserved ranges are refused (`INVALID_WEBHOOK_URL`) because Porkbun delivers from inside its own network. Credentials in the URL (`https:\/\/user:pass@host`) are also refused \u2014 authenticate the receiver by verifying the `X-Porkbun-Signature` HMAC instead. A hostname that does not resolve yet is accepted, so you can register the endpoint before the receiver is deployed, but delivery is re-checked against these rules immediately before each request.","operationId":"webhookUpdate","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"Updated endpoint.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookEndpointResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/update \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"id\":42,\"status\":\"DISABLED\"}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/webhook\/rotateSecret":{"post":{"tags":["Webhooks"],"summary":"Rotate the signing secret","description":"Generate a new signing secret for an endpoint and return the endpoint with the new secret. Deliveries are signed with the new secret immediately, so update your verifier as part of the same operation.","operationId":"webhookRotateSecret","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookIdRequest"}}}},"responses":{"200":{"description":"Endpoint with the rotated secret.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookEndpointResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/rotateSecret \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"id\":42}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":false,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/webhook\/test":{"post":{"tags":["Webhooks"],"summary":"Send a test event","description":"Enqueue a `webhook.test` event to the endpoint so you can confirm reachability and that your signature verification works. The endpoint must be ACTIVE. Delivery is asynchronous (usually within a minute).","operationId":"webhookTest","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookIdRequest"}}}},"responses":{"200":{"description":"Test event queued.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookTestResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Endpoint disabled or invalid","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/test \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"id\":42}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/webhook\/delete":{"post":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","description":"Delete a webhook endpoint by id. Deliveries stop immediately.","operationId":"webhookDelete","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookIdRequest"}}}},"responses":{"200":{"description":"Endpoint deleted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"message":{"type":"string","example":"Webhook endpoint deleted."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/delete \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"id\":42}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":true,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/webhook\/deliveries":{"get":{"tags":["Webhooks"],"summary":"List webhook deliveries","description":"List recent delivery attempts across the account (newest first), optionally filtered by endpoint or status. The bulky payload is omitted \u2014 use GET \/webhook\/delivery\/{id} for it. History is retained about 30 days. Read-only; supports GET (header auth) or POST (body or header auth).","operationId":"webhookDeliveries","parameters":[{"name":"endpointId","in":"query","required":false,"schema":{"type":"integer"},"description":"Only deliveries for this endpoint."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["PENDING","PROCESSING","DELIVERED","FAILED"]},"description":"Filter by delivery status."},{"name":"start","in":"query","required":false,"schema":{"type":"integer"},"description":"Pagination offset (default 0)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"},"description":"Page size, 1-200 (default 50)."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Recent deliveries.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookDeliveryListResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Endpoint not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/deliveries?status=FAILED&limit=50' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/delivery\/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook delivery","description":"Fetch a single delivery including the full event payload that was (or will be) sent and its delivery status. Read-only; supports GET (header auth) or POST (body or header auth).","operationId":"webhookDelivery","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"example":9001},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"The delivery (includes payload).","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookDeliveryResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"404":{"description":"Delivery not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/delivery\/9001' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/webhook\/resend":{"post":{"tags":["Webhooks"],"summary":"Resend a delivery","description":"Re-queue a past delivery to its endpoint. Clones it into a fresh attempt reusing the ORIGINAL event id (so consumers that dedupe on X-Porkbun-Webhook-Id treat it as the same event). The endpoint must still exist and be ACTIVE. The original delivery row is left intact as history.","operationId":"webhookResend","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookIdRequest"}}}},"responses":{"200":{"description":"Delivery re-queued.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/WebhookResendResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Endpoint missing or disabled","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Delivery not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https:\/\/api.porkbun.com\/api\/json\/v3\/webhook\/resend \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"apikey\":\"pk1_...\",\"secretapikey\":\"sk1_...\",\"id\":9001}'"}],"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/domain\/getRegistrationRequirements\/{tld}":{"get":{"tags":["Domain"],"summary":"Get TLD registration requirements (JSON Schema)","description":"Machine-readable registration requirements for a TLD. Returns whether the TLD is registerable via the API (`apiRegisterable`), the `\/domain\/create` request body as a JSON Schema, the fixed registration term, WHOIS-privacy\/validated-address\/registrant-only flags, and \u2014 for TLDs with registry eligibility rules (e.g. .us nexus, .ca legal type) \u2014 a second JSON Schema (`registryRequirements`) enumerating those fields with allowed values and labels. Call this before \/domain\/create to know upfront whether and how a TLD can be registered. Read-only; GET (header auth) or POST (body or header auth).","operationId":"domainGetRegistrationRequirements","parameters":[{"name":"tld","in":"path","required":true,"schema":{"type":"string"},"example":"us","description":"TLD without a leading dot, e.g. `com`, `us`, `ca`."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"API key header auth (use with X-Secret-API-Key)"},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Secret API key header auth (use with X-API-Key)"}],"responses":{"200":{"description":"Registration requirements for the TLD.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"tld":{"type":"string","example":"us"},"apiRegisterable":{"type":"boolean","description":"Whether this TLD can be registered via the API. False for TLDs with registry eligibility requirements the API cannot submit (register those on the website).","example":false},"registrationDurationYears":{"type":"integer","description":"Fixed registration term the API uses for this TLD.","example":1},"maxRegistrationYears":{"type":"integer","nullable":true,"description":"Maximum years the registry allows, or null if unspecified."},"whoisPrivacySupported":{"type":"boolean","example":true},"requiresValidatedAddress":{"type":"boolean","example":true},"registrantOnly":{"type":"boolean","description":"TLD uses only the registrant contact (no admin\/tech\/billing).","example":false},"requestSchema":{"type":"object","description":"JSON Schema (Draft 2020-12) for the \/domain\/create request body this TLD accepts (cost, agreeToTerms, whoisPrivacy, credentials), including the fixed registration term.","example":{"$schema":"https:\/\/json-schema.org\/draft\/2020-12\/schema","title":"domain\/create request for .com","type":"object","required":["apikey","secretapikey","cost","agreeToTerms"],"additionalProperties":false,"properties":{"apikey":{"type":"string","description":"Public API key (pk1_...). Omit if using the X-API-Key header."},"secretapikey":{"type":"string","description":"Secret API key (sk1_...). Omit if using the X-Secret-API-Key header."},"cost":{"type":"integer","minimum":0,"description":"Total price in US cents; must match the quote from \/domain\/checkDomain."},"agreeToTerms":{"type":"string","enum":["yes","1"],"description":"Must be \"yes\" to accept the registration agreement and terms."},"whoisPrivacy":{"type":"string","enum":["yes","no"],"description":"Optional WHOIS privacy override; defaults to enabled where supported."}}}},"registryRequirements":{"type":"object","nullable":true,"description":"JSON Schema of extra registry\/eligibility fields the TLD requires (e.g. .us purpose+category, .ca legal type) with enums and human labels, plus an x-policyNote. Null when the TLD has no structured extra data. These fields are documented for eligibility; they are not accepted by \/domain\/create today.","example":{"$schema":"https:\/\/json-schema.org\/draft\/2020-12\/schema","title":"Registry eligibility data for .us","description":"Extra registrant data the registry requires for this TLD. These fields are not accepted by \/domain\/create today (such TLDs are website-only); this schema documents the requirements.","type":"object","properties":{"purpose":{"type":"string","title":"Purpose","x-appliesTo":"registrant contact","enum":["P1","P2","P3","P4","P5"],"x-enumDescriptions":{"P1":"Business use for profit.","P2":"Non-profit organization.","P3":"Personal use.","P4":"Education purposes.","P5":"Government purposes."}},"category":{"type":"string","title":"Category","x-appliesTo":"registrant contact","enum":["C11","C12","C21","C31","C32"],"x-enumDescriptions":{"C11":"A US citizen.","C12":"A permanent resident of the US.","C21":"A US-based organization or company.","C31":"A foreign entity with a bona fide US presence.","C32":"An entity with a US office or facility."}}},"x-policyNote":"To register a .us domain you must accurately complete the Purpose and Category fields; inaccurate information can lead to seizure without refund."}},"notApiRegisterableReason":{"type":"string","description":"Present only when apiRegisterable is false."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Unknown\/unsupported TLD or missing TLD.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl 'https:\/\/api.porkbun.com\/api\/json\/v3\/domain\/getRegistrationRequirements\/us' \\\n  -H 'X-API-Key: pk1_...' \\\n  -H 'X-Secret-API-Key: sk1_...'"}],"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/makeDir\/{domain}":{"post":{"tags":["Hosting"],"summary":"Create a directory (static hosting only)","description":"**Applies to:** Secure Static Hosting only \u2014 a WordPress site returns `NOT_SUPPORTED_FOR_PRODUCT`.\n\nCreate a directory (and any missing parent directories) at `path` in the domain\u2019s hosting space. Deploy already auto-creates directories in a file\u2019s path, so use this to stand up an empty directory explicitly. Path is sanitized segment-by-segment (no traversal \/ control chars).","operationId":"hostingMakeDir","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["path"],"properties":{"path":{"type":"string","example":"assets\/img"}}}]}}}},"responses":{"200":{"description":"Directory created.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"created":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting"}},"\/sandbox\/topup":{"post":{"tags":["Sandbox"],"summary":"Sandbox: add fake credit","operationId":"sandboxTopup","description":"Sandbox only (requires a `pk1_sb_` key). Grants fake account credit so paid operations can keep being exercised after funds run out. Optional `amount` in US cents (default 100000 = $1000, capped 1,000,000).","requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","properties":{"amount":{"type":"integer","description":"Fake credit to add in US cents (default 100000; max 1000000)."}}}]}}}},"responses":{"200":{"description":"Credit granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"granted":{"type":"integer"},"balance":{"type":"integer"},"display":{"type":"string"},"sandbox":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/sandbox\/reset":{"post":{"tags":["Sandbox"],"summary":"Sandbox: reset to a clean slate","operationId":"sandboxReset","description":"Sandbox only (requires a `pk1_sb_` key). Wipes the sandbox account's simulated state (domains, DNS, orders, credit) and re-grants $1000 fake credit.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"}]}}}},"responses":{"200":{"description":"Sandbox reset.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"},"domainsCleared":{"type":"integer"},"balance":{"type":"integer"},"sandbox":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":false,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/sandbox\/triggerWebhook":{"post":{"tags":["Sandbox"],"summary":"Sandbox: fire a sample webhook event","operationId":"sandboxTriggerWebhook","description":"Sandbox only (requires a `pk1_sb_` key). Enqueues a signed sample webhook event to your registered endpoints so you can test your handler and signature verification for any event type on demand \u2014 including cron-driven events like `domain.expiring` that don't result from a single API call. Register an endpoint first with `POST \/webhook\/create`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","required":["eventType"],"properties":{"eventType":{"type":"string","enum":["domain.registered","domain.renewed","domain.transfer.completed","domain.expiring","dns.record.created","dns.record.updated","dns.record.deleted"],"description":"Event type to emit."},"domain":{"type":"string","description":"Domain used in the sample payload (default example.com)."}}}]}}}},"responses":{"200":{"description":"Event enqueued for delivery.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"event":{"type":"string"},"data":{"type":"object"},"endpointsRegistered":{"type":"integer"},"message":{"type":"string"},"sandbox":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"account"}},"\/mock":{"get":{"tags":["Mock"],"summary":"Mock server: list mockable endpoints","operationId":"mockDirectory","description":"Credential-free. Returns a directory of every endpoint that can be mocked, each with a ready-to-call mock URL.","responses":{"200":{"description":"Directory of mockable endpoints.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mock":{"type":"boolean"},"count":{"type":"integer"},"endpoints":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"summary":{"type":"string"},"mockUrl":{"type":"string"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/mock\/{path}":{"get":{"tags":["Mock"],"summary":"Mock server: example response for an endpoint","operationId":"mockEndpoint","description":"Credential-free. Mirror any real endpoint path after `\/mock` (e.g. `\/mock\/domain\/listAll`, `\/mock\/dns\/create\/example.com`) to get a schema-accurate example success response. Append `?status=error` for the error-response shape. Touches no datastore; signalled by the `X-Porkbun-Mock: true` header.","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string"},"description":"The real endpoint path to mock, e.g. `domain\/listAll`."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["error"]},"description":"Set to `error` to return the error-response shape."}],"responses":{"200":{"description":"Schema-accurate example response.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/createWpCredentials\/{domain}":{"post":{"tags":["Hosting"],"summary":"Mint WordPress REST API credentials (WordPress only)","description":"**Applies to:** Cloud for WordPress only.\n\nCreates a WordPress **Application Password** so an agent or integration can drive the site over the WP REST API at `https:\/\/{domain}\/wp-json\/` using HTTP Basic auth. The password is returned **once** \u2014 WordPress stores only a hash.\n\nDefaults to a dedicated least-privilege `porkbun-agent` user with the `editor` role (created on first use), which can manage content but not install code. `role: \"administrator\"` grants full site control **including plugin installation (arbitrary code execution on the site)** and therefore requires `acknowledgeFullAccess: true`.\n\nRevoke any time via `\/hosting\/deleteWpCredentials\/{domain}` or in wp-admin under Users \u2192 Profile. Requires the site to be provisioned and ACTIVE (poll `\/hosting\/get\/{domain}`).\n\nFree **preview** sites (the $0 parked plan) are excluded \u2014 they return `PREVIEW_SITE_NOT_SUPPORTED`; upgrade to a paid plan first. Works on any Cloud for WordPress site in the account regardless of whether it was provisioned via the API or the website, including sites migrated onto WP Cloud from the legacy WordPress product. For `role: \"administrator\"` the site's actual administrator account is looked up rather than assumed, so a renamed admin user is handled.\n\n**Rate limit:** 20 mints per account per hour (`dryRun` calls are free).","operationId":"hostingCreateWpCredentials","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","properties":{"role":{"type":"string","enum":["editor","administrator"],"default":"editor","description":"Least privilege by default. `editor` = content only (recommended for agents). `administrator` = full control incl. plugin install; requires acknowledgeFullAccess."},"acknowledgeFullAccess":{"type":"boolean","description":"Required when role=administrator: confirms you understand the credential can run arbitrary code on the site."},"name":{"type":"string","maxLength":40,"description":"Label shown in wp-admin (sanitized to letters, digits and dashes).","example":"Acme-Agent"},"dryRun":{"type":"boolean","description":"Validate without creating anything."}}}]}}}},"responses":{"200":{"description":"Application password created (returned once).","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"credentials":{"type":"object","properties":{"username":{"type":"string","example":"porkbun-agent"},"applicationPassword":{"type":"string","example":"abcd EFGH ijkl MNOP qrst UVWX"},"role":{"type":"string","example":"editor"},"name":{"type":"string","example":"Porkbun-API"},"restUrl":{"type":"string","example":"https:\/\/example.com\/wp-json\/"},"authorization":{"type":"string","description":"Ready-to-use HTTP Basic header value."}}},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}},"\/hosting\/getWpCredentials\/{domain}":{"get":{"tags":["Hosting"],"summary":"List WordPress application passwords (WordPress only)","description":"**Applies to:** Cloud for WordPress only.\n\nLists the application passwords on the site (uuid, name, created, last used) so you can audit or pick one to revoke. Metadata only \u2014 WordPress stores just a hash, so a password can never be re-read. Optional `wpUser` (defaults to the dedicated `porkbun-agent` user).\n\nFree preview sites are excluded (`PREVIEW_SITE_NOT_SUPPORTED`).","operationId":"hostingGetWpCredentials","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"wpUser","in":"query","required":false,"schema":{"type":"string"},"example":"porkbun-agent"}],"responses":{"200":{"description":"Application passwords for the user.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"wpUser":{"type":"string"},"credentials":{"type":"array","items":{"type":"object"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/hosting\/deleteWpCredentials\/{domain}":{"post":{"tags":["Hosting"],"summary":"Revoke WordPress application passwords (WordPress only)","description":"**Applies to:** Cloud for WordPress only.\n\nRevokes an application password by `uuid` (from `\/hosting\/getWpCredentials`), or every one for the user with `all: true`. Any integration using it stops authenticating immediately.\n\nFree preview sites are excluded (`PREVIEW_SITE_NOT_SUPPORTED`).","operationId":"hostingDeleteWpCredentials","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/AuthRequest"},{"type":"object","properties":{"uuid":{"type":"string","description":"The application password uuid to revoke."},"all":{"type":"boolean","description":"Revoke every application password for the user."},"wpUser":{"type":"string","default":"porkbun-agent"},"dryRun":{"type":"boolean"}}}]}}}},"responses":{"200":{"description":"Revoked.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"revoked":{"type":"string"},"message":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}},"\/cloudflare\/getConnection":{"get":{"tags":["Cloudflare"],"summary":"Check the Cloudflare account connection (poll target)","description":"Whether this account has an active Cloudflare grant, and which Cloudflare account it points at.\n\n**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` \u2014 send the account owner there, then poll this endpoint until `connected` is `true`.\n\nAlso available via POST.","operationId":"cloudflareGetConnection","responses":{"200":{"description":"Connection state, plus connectUrl\/nextStep when a human still has to authorize.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/inventory":{"get":{"tags":["Cloudflare"],"summary":"List every domain with its Cloudflare eligibility","description":"Every domain in the account with a `state` (`eligible`, `warn`, `blocked`, `connected`, `inprogress`) and a human-readable `reason`.\n\nRead 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.","operationId":"cloudflareInventory","responses":{"200":{"description":"Per-domain eligibility plus a count by state.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/connect":{"post":{"tags":["Cloudflare"],"summary":"Queue domains to move to the customer's Cloudflare account","description":"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.\n\n**Asynchronous.** Work runs on a background job over the next few minutes, so a successful call means *queued*, never *connected* \u2014 poll `\/cloudflare\/getQueue` or `\/cloudflare\/get\/{domain}`.\n\n**`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.\n\nEligibility, ownership and nameserver state are re-checked immediately before each domain is acted on, so a domain accepted here can still be skipped later.\n\nRe-submitting a domain is safe: the queue row is the unit of truth and is updated in place.\n\nSupports `dryRun: true`, which returns the same per-domain verdicts without queueing anything.\n\nRequires an active Cloudflare connection (`CLOUDFLARE_NOT_CONNECTED` otherwise). Limits: 500 domains per call, 2000 domains per account per hour.\n\nAfter queueing, poll `\/cloudflare\/get\/{domain}`: the row moves `queued` \u2192 `working` \u2192 `setup` \u2192 `activating` \u2192 `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.","operationId":"cloudflareConnect","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"},"description":"Domain names to move. A comma-separated string is also accepted.","example":["example.com","example.net"]},"dryRun":{"type":"boolean","description":"Validate and return per-domain verdicts without queueing."}},"required":["domains"]}}}},"responses":{"200":{"description":"Per-domain queued\/skipped\/alreadyQueued maps with reasons, plus a summary.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"domains"}},"\/cloudflare\/getQueue":{"get":{"tags":["Cloudflare"],"summary":"List every Cloudflare move for the account","description":"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.\n\n**Status values** (poll until one of the terminal ones):\n\n| status | meaning | terminal |\n|--------|---------|----------|\n| `queued` | accepted, waiting for the worker | no |\n| `working` | a run is touching this row right now | no |\n| `setup` | the zone exists in the customer's Cloudflare account but Cloudflare has not provisioned it yet (`initializing`). **The nameservers have not been touched** \u2014 the domain still resolves from Porkbun. Cloudflare can sit here for hours when an account has a backlog of zones it never activated | no |\n| `activating` | nameservers repointed; waiting for Cloudflare to mark the zone active. Legitimately slow (registry + resolver propagation) \u2014 allow up to 72h, and a real move has been observed taking 59h | no |\n| `connected` \/ `done` | the move finished | **yes** |\n| `skipped` | not moved, and `message` says why (DNSSEC live, custom nameservers, no longer in the account) | **yes** |\n| `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 |\n| `undone` | the nameservers were put back on Porkbun \u2014 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** |\n\nPoll on a sensible interval (a few seconds early on, then back off) \u2014 a zone typically leaves `queued` within seconds but can sit in `setup` or `activating` while Cloudflare provisions the zone and DNS propagates.","operationId":"cloudflareGetQueue","responses":{"200":{"description":"All queue rows plus a count by status.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/get\/{domain}":{"get":{"tags":["Cloudflare"],"summary":"Get the Cloudflare move status for one domain","description":"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.\n\n**Status values** (poll until one of the terminal ones):\n\n| status | meaning | terminal |\n|--------|---------|----------|\n| `queued` | accepted, waiting for the worker | no |\n| `working` | a run is touching this row right now | no |\n| `setup` | the zone exists in the customer's Cloudflare account but Cloudflare has not provisioned it yet (`initializing`). **The nameservers have not been touched** \u2014 the domain still resolves from Porkbun. Cloudflare can sit here for hours when an account has a backlog of zones it never activated | no |\n| `activating` | nameservers repointed; waiting for Cloudflare to mark the zone active. Legitimately slow (registry + resolver propagation) \u2014 allow up to 72h, and a real move has been observed taking 59h | no |\n| `connected` \/ `done` | the move finished | **yes** |\n| `skipped` | not moved, and `message` says why (DNSSEC live, custom nameservers, no longer in the account) | **yes** |\n| `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 |\n| `undone` | the nameservers were put back on Porkbun \u2014 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** |\n\nPoll on a sensible interval (a few seconds early on, then back off) \u2014 a zone typically leaves `queued` within seconds but can sit in `setup` or `activating` while Cloudflare provisions the zone and DNS propagates.","operationId":"cloudflareGet","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"One queue row.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/retry\/{domain}":{"post":{"tags":["Cloudflare"],"summary":"Retry a failed or skipped domain","description":"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`.","operationId":"cloudflareRetry","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Updated queue row.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/cloudflare\/rollback\/{domain}":{"post":{"tags":["Cloudflare"],"summary":"Undo a completed move (restore Porkbun nameservers)","description":"Point the domain's nameservers back at Porkbun, restoring the DNS we still hold.\n\nThe Cloudflare zone is deliberately left in place \u2014 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`.","operationId":"cloudflareRollback","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Updated queue row.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"domains"}},"\/cloudflare\/disconnect":{"post":{"tags":["Cloudflare"],"summary":"Remove the stored Cloudflare connection","description":"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`.","operationId":"cloudflareDisconnect","responses":{"200":{"description":"Confirmation.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":true,"reversible":true,"requiresConfirmation":true},"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"x-porkbun-scope":"domains"}},"\/cloudflare\/setProxy\/{domain}":{"post":{"tags":["Cloudflare"],"summary":"Turn the Cloudflare proxy (orange cloud) on or off","description":"Set `proxied` on the domain's Cloudflare DNS records.\n\n**The move itself always imports records DNS-only (grey cloud), on purpose** \u2014 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.\n\nDefaults 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 \u2014 anything else is reported under `skipped` with a reason rather than failing the call. Records already in the requested state are skipped too.\n\nProxying 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`.\n\nRate limit: 60 changes per account per hour (these calls go to Cloudflare under Porkbun's OAuth client).","operationId":"cloudflareSetProxy","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"true = proxy through Cloudflare (orange cloud); false = DNS-only (grey cloud)."},"records":{"type":"array","items":{"type":"string"},"description":"Optional. Limit to these names; \"@\" = apex, bare labels are expanded.","example":["@","www"]},"dryRun":{"type":"boolean"}},"required":["enabled"]}}}},"responses":{"200":{"description":"Per-record applied\/skipped plus any warnings.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/cloudflare\/getRecords\/{domain}":{"get":{"tags":["Cloudflare"],"summary":"List the domain's live DNS records at Cloudflare","description":"The domain's DNS records **as Cloudflare currently holds them**, each with its `proxied` flag and whether it is `proxiable` at all.\n\nOnce a domain has moved, this is the authoritative record set \u2014 `\/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.","operationId":"cloudflareGetRecords","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Per-record proxy state.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/preview\/{domain}":{"get":{"tags":["Cloudflare"],"summary":"Preview exactly which records a move would copy","description":"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.\n\nRecords are always created DNS-only (grey cloud); use `\/cloudflare\/setProxy` afterwards to turn the proxy on. Also available via POST.","operationId":"cloudflarePreview","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Records that would be copied and skipped.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/getZone\/{domain}":{"get":{"tags":["Cloudflare"],"summary":"Get live zone state from Cloudflare (and detect nameserver drift)","description":"What **Cloudflare** says about the zone right now \u2014 status, paused, its nameservers, activation date \u2014 as opposed to what our queue row remembers.\n\nThese 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.","operationId":"cloudflareGetZone","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Cloudflare's own view of the zone, plus drift detection.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/getZoneSettings\/{domain}":{"get":{"tags":["Cloudflare"],"summary":"Read the zone settings that matter after a move","description":"The Cloudflare zone settings worth caring about post-migration: `ssl`, `always_use_https`, `automatic_https_rewrites`, `min_tls_version`, `development_mode`, `cache_level`.\n\nThe 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.\n\nIf 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.","operationId":"cloudflareGetZoneSettings","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"}],"responses":{"200":{"description":"Current settings plus warnings.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read"}},"\/cloudflare\/setZoneSettings\/{domain}":{"post":{"tags":["Cloudflare"],"summary":"Change zone settings (allowlisted)","description":"Set one or more of the allowlisted zone settings. This is an allowlist rather than a passthrough \u2014 Cloudflare exposes hundreds of settings and WAF\/firewall\/security controls are deliberately out of scope for this API.\n\nPrefer `ssl: \"full\"`; `flexible` is an invisible downgrade for visitors. Supports `dryRun`. Rate limit: shares the 60\/hour Cloudflare-write budget.\n\nIf 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.","operationId":"cloudflareSetZoneSettings","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"ssl":{"type":"string","enum":["off","flexible","full","strict"]},"always_use_https":{"type":"string","enum":["on","off"]},"automatic_https_rewrites":{"type":"string","enum":["on","off"]},"min_tls_version":{"type":"string","enum":["1.0","1.1","1.2","1.3"]},"development_mode":{"type":"string","enum":["on","off"]},"cache_level":{"type":"string","enum":["aggressive","basic","simplified"]},"dryRun":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Applied and failed settings.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/cloudflare\/createRecord\/{domain}":{"post":{"tags":["Cloudflare"],"summary":"Create a DNS record in the domain's Cloudflare zone","description":"**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.\n\n`name` accepts `@` for the apex or a bare label. MX requires `priority`. `proxied` applies to A\/AAAA\/CNAME only. Supports `dryRun`.","operationId":"cloudflareCreateRecord","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"A, AAAA, CNAME, TXT, MX, NS, PTR or SPF for a plain `content` value. Structured types (SRV, CAA, TLSA\u2026) need `data` instead.","example":"TXT"},"name":{"type":"string","description":"`@` for the apex, a bare label (`www`) is expanded, or a full hostname.","example":"www"},"content":{"type":"string","description":"The value the record points at.","example":"192.0.2.1"},"ttl":{"type":"integer","description":"1 = automatic (Cloudflare's default), otherwise 60\u201386400.","example":1},"priority":{"type":"integer","description":"Required for MX; lower is preferred.","example":10},"proxied":{"type":"boolean","description":"Orange cloud. A\/AAAA\/CNAME only."},"comment":{"type":"string","description":"Free-text note stored on the record (100 chars)."},"data":{"type":"object","description":"Structured value for record types Cloudflare models as an object (SRV, CAA\u2026), passed through as given."},"dryRun":{"type":"boolean"}},"required":["type"]}}}},"responses":{"200":{"description":"The created record.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/cloudflare\/editRecord\/{domain}\/{recordId}":{"post":{"tags":["Cloudflare"],"summary":"Update a DNS record in the domain's Cloudflare zone","description":"**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.\n\nPartial update: fields you omit keep their current value. The response carries both the new record and the `previous` one. Supports `dryRun`.","operationId":"cloudflareEditRecord","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"recordId","in":"path","required":true,"schema":{"type":"string"},"description":"Cloudflare record id from \/cloudflare\/getRecords.","example":"59a5742f71ceeac69b2b3510356d8672"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"A, AAAA, CNAME, TXT, MX, NS, PTR or SPF for a plain `content` value. Structured types (SRV, CAA, TLSA\u2026) need `data` instead.","example":"TXT"},"name":{"type":"string","description":"`@` for the apex, a bare label (`www`) is expanded, or a full hostname.","example":"www"},"content":{"type":"string","description":"The value the record points at.","example":"192.0.2.1"},"ttl":{"type":"integer","description":"1 = automatic (Cloudflare's default), otherwise 60\u201386400.","example":1},"priority":{"type":"integer","description":"Required for MX; lower is preferred.","example":10},"proxied":{"type":"boolean","description":"Orange cloud. A\/AAAA\/CNAME only."},"comment":{"type":"string","description":"Free-text note stored on the record (100 chars)."},"data":{"type":"object","description":"Structured value for record types Cloudflare models as an object (SRV, CAA\u2026), passed through as given."},"dryRun":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The updated record, plus the previous values.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/cloudflare\/deleteRecord\/{domain}\/{recordId}":{"post":{"tags":["Cloudflare"],"summary":"Delete a DNS record from the domain's Cloudflare zone","description":"**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.\n\nThe record is read before deletion, so the response reports exactly what was removed and a bad id fails before anything is destroyed. Supports `dryRun`.","operationId":"cloudflareDeleteRecord","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"name":"recordId","in":"path","required":true,"schema":{"type":"string"},"description":"Cloudflare record id from \/cloudflare\/getRecords.","example":"59a5742f71ceeac69b2b3510356d8672"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The deleted record.","content":{"application\/json":{"schema":{"type":"object"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":"none","destructive":true,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"dns"}},"\/linkinbio\/get\/{domain}":{"get":{"tags":["Link in Bio"],"summary":"Get a Link in Bio page","description":"The page on a domain: its public URL, plan and what the plan allows, profile, links in display order (with ids), social icons, appearance and settings. Use it before editing, and to read link ids.\n\n**Errors:** `LINKINBIO_NOT_FOUND` (no page; the message says what the domain has instead).","operationId":"linkInBioGet","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Secret-API-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"}}}}}}},"post":{"tags":["Link in Bio"],"summary":"Get a Link in Bio page","description":"The page on a domain: its public URL, plan and what the plan allows, profile, links in display order (with ids), social icons, appearance and settings. Use it before editing, and to read link ids.\n\n**Errors:** `LINKINBIO_NOT_FOUND` (no page; the message says what the domain has instead).","operationId":"linkInBioGetPost","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":true,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"read","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"}}}}}}}},"\/linkinbio\/create\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Turn on a free Link in Bio page","description":"Turns on a free page served on the domain itself (plan PIXIELINKS2M1: up to 50 links; no scheduling, animation, passcode protection or analytics IDs). Free, nothing is ordered. It starts with a placeholder title, bio and link: replace them with `updateProfile`, `editLink` and `addLink`.\n\nThe domain is pointed at the page with DNS records, which needs Porkbun nameservers: if the domain uses other ones, send `agreeToNameserverChange: true` to switch them. A free WordPress preview on the domain is replaced; any other hosting is not (`HOSTING_ALREADY_EXISTS`).\n\nMany .com domains registered at Porkbun already have a free page, forwarded to `<domain>.l.ink`; check with `get` first. For a paid plan (1000 links, every feature), use `\/hosting\/create` with sku `PIXIELINKSM1` or `PIXIELINKSY1`: it upgrades an existing free page and keeps its content. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `LINKINBIO_ALREADY_EXISTS`, `HOSTING_ALREADY_EXISTS`, `NAMESERVER_CHANGE_REQUIRED`, `LINKINBIO_CREATE_FAILED`.","operationId":"linkInBioCreate","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"agreeToNameserverChange":{"type":"boolean","description":"Required when the domain is not on Porkbun nameservers."}}}}}}}},"\/linkinbio\/updateProfile\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Update the page title, bio and avatar","description":"Only the fields sent change; an empty title or bio removes it. `avatar` is a base64 JPEG or PNG (a `data:` URL works too), up to 5 MB and 4000x4000; null removes it. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `INVALID_INPUT`, `LINKINBIO_SAVE_FAILED`.","operationId":"linkInBioUpdateProfile","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"title":{"type":"string","description":"Up to 100 characters.","example":"Jane Baker"},"bio":{"type":"string","description":"Up to 500 characters."},"avatar":{"type":"string","nullable":true,"description":"Base64 JPEG or PNG, or null to remove."}}}}}}}},"\/linkinbio\/addLink\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Add a link","description":"Adds a link, featured link or category heading. `position`: `bottom` (default) or `top`. Returns the new link (with its id) and the page. Fields marked \"paid plans\" return `FEATURE_NOT_IN_PLAN` on the free on-domain plan, and nothing is saved. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `LINK_LIMIT_REACHED`, `DUPLICATE_LINK` (same title and URL as another link), `FEATURE_NOT_IN_PLAN`, `INVALID_INPUT`.","operationId":"linkInBioAddLink","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioLinkResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey","title"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"type":{"type":"string","description":"`link` (default), `featured` or `category` (a heading; no url).","enum":["link","featured","category"]},"title":{"type":"string","description":"Up to 255 characters.","example":"Shop my store"},"url":{"type":"string","description":"http(s) address, up to 255 characters; a bare host gets https:\/\/ added. Not for a category.","example":"https:\/\/example.com\/shop"},"enabled":{"type":"boolean","description":"Default true."},"startDate":{"type":"string","nullable":true,"description":"Show from this time: YYYY-MM-DD, or ISO 8601; UTC unless an offset is given. null clears it. Paid plans."},"endDate":{"type":"string","nullable":true,"description":"Hide after this time; same format. Paid plans."},"animation":{"type":"string","nullable":true,"description":"`pulse`, `bounce`, `shake`, or null for none. Paid plans.","enum":["pulse","bounce","shake"]},"protection":{"type":"object","nullable":true,"description":"null removes it. `code` (passcode; paid plans) needs value; `birthday` takes value 18, 21 or 25; `sensitive` shows a warning.","properties":{"type":{"type":"string","description":"","enum":["code","birthday","sensitive"]},"value":{"type":"string","description":""},"explanation":{"type":"string","description":"Shown to the visitor, up to 255 characters."}}},"position":{"type":"string","description":"","enum":["bottom","top"]}}}}}}}},"\/linkinbio\/editLink\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Edit a link","description":"Changes any of a link's fields; `id` is required and only the fields sent change. Set `enabled: false` to hide a link without deleting it. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `LINK_NOT_FOUND`, `DUPLICATE_LINK`, `FEATURE_NOT_IN_PLAN`, `INVALID_INPUT`.","operationId":"linkInBioEditLink","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioLinkResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey","id"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"type":{"type":"string","description":"`link` (default), `featured` or `category` (a heading; no url).","enum":["link","featured","category"]},"title":{"type":"string","description":"Up to 255 characters.","example":"Shop my store"},"url":{"type":"string","description":"http(s) address, up to 255 characters; a bare host gets https:\/\/ added. Not for a category.","example":"https:\/\/example.com\/shop"},"enabled":{"type":"boolean","description":"Default true."},"startDate":{"type":"string","nullable":true,"description":"Show from this time: YYYY-MM-DD, or ISO 8601; UTC unless an offset is given. null clears it. Paid plans."},"endDate":{"type":"string","nullable":true,"description":"Hide after this time; same format. Paid plans."},"animation":{"type":"string","nullable":true,"description":"`pulse`, `bounce`, `shake`, or null for none. Paid plans.","enum":["pulse","bounce","shake"]},"protection":{"type":"object","nullable":true,"description":"null removes it. `code` (passcode; paid plans) needs value; `birthday` takes value 18, 21 or 25; `sensitive` shows a warning.","properties":{"type":{"type":"string","description":"","enum":["code","birthday","sensitive"]},"value":{"type":"string","description":""},"explanation":{"type":"string","description":"Shown to the visitor, up to 255 characters."}}},"id":{"type":"integer","description":"From `get`."}}}}}}}},"\/linkinbio\/deleteLink\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Delete a link","description":"Deletes a link with its image, animation and protection. To hide a link instead, use editLink with `enabled: false`. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `LINK_NOT_FOUND`.","operationId":"linkInBioDeleteLink","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":true,"reversible":false,"requiresConfirmation":true},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey","id"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"id":{"type":"integer","description":"From `get`."}}}}}}}},"\/linkinbio\/reorderLinks\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Reorder links","description":"`ids` lists every link id on the page, once each, in the order to show them. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `INVALID_INPUT` (the message lists the current ids).","operationId":"linkInBioReorderLinks","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey","ids"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"ids":{"type":"array","items":{"type":"integer"},"description":"Every link id, in display order."}}}}}}}},"\/linkinbio\/updateSocials\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Update social icons","description":"`location` (`top` or `bottom`) and `socials`, an object of email, instagram, facebook, tiktok, youtube, twitter and linkedin: a handle or a profile URL. Only the keys sent change; \"\" or null removes one. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `INVALID_INPUT`.","operationId":"linkInBioUpdateSocials","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"location":{"type":"string","description":"","enum":["top","bottom"]},"socials":{"type":"object","additionalProperties":{"type":"string","nullable":true},"example":{"instagram":"janebakes","email":"jane@example.com"}}}}}}}}},"\/linkinbio\/updateAppearance\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Update the page appearance","description":"Only the keys sent change; null resets one to the editor default. `preset` applies a ready-made look in the editor; set individual colours to fine-tune. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `INVALID_INPUT`.","operationId":"linkInBioUpdateAppearance","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"preset":{"type":"string","description":"","enum":["classic","ocean","midnight","paper","forest","sunset"]},"fontFamily":{"type":"string","description":"","enum":["inter","poppins","lora","mono","serif"]},"buttonStyle":{"type":"string","description":"","enum":["classic","pill","floating","hardShadow"]},"radius":{"type":"integer","description":"Button corner radius in pixels, 0-50."},"pageBackgroundMode":{"type":"string","description":"`gradient` blends pageBackgroundColor into pageBackgroundColor2.","enum":["solid","gradient"]},"textAlign":{"type":"string","description":"","enum":["left","center","right"]},"pageBackgroundColor":{"type":"string","description":"Hex colour, e.g. #1b5a8a."},"pageBackgroundColor2":{"type":"string","description":"Hex colour, e.g. #1b5a8a."},"pageColor":{"type":"string","description":"Hex colour, e.g. #1b5a8a."},"boxBackgroundColor":{"type":"string","description":"Hex colour, e.g. #1b5a8a."},"borderColor":{"type":"string","description":"Hex colour, e.g. #1b5a8a."}}}}}}}},"\/linkinbio\/updateSettings\/{domain}":{"post":{"tags":["Link in Bio"],"summary":"Update page settings","description":"`sensitiveContent`: a warning (`generic`) or an age check (`18`, `21`, `25`) shown before the page, or null to turn it off. `analytics`: tracking IDs (googleAnalyticsId, facebookPixelId, twitterPixelId, tikTokPixelId) and UTM parameters (utmSource, utmMedium) added to outgoing links; paid plans; \"\" removes one. The mailing-list signup form is set up in the editor only. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `FEATURE_NOT_IN_PLAN`, `INVALID_INPUT`.","operationId":"linkInBioUpdateSettings","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LinkInBioResponse"}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":false},"x-porkbun-scope":"hosting","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"sensitiveContent":{"type":"string","nullable":true,"description":"","enum":["generic","18","21","25"]},"analytics":{"type":"object","additionalProperties":{"type":"string"},"example":{"googleAnalyticsId":"G-ABC123"}}}}}}}}},"\/hosting\/updateAutoRenew\/{domain}":{"post":{"tags":["Hosting"],"summary":"Turn hosting auto-renew on or off","description":"**Applies to:** every paid hosting product (Secure Static Hosting, Cloud for WordPress, Link in Bio).\n\nThe same switch as on the hosting page on porkbun.com. Off: the plan stays active until `expireDate` and is not charged again (a trial ends without a charge); after that the hosting is removed, except a paid Link in Bio page, which moves to the free plan with its content kept. On: it renews before `expireDate` at the plan price. `GET \/hosting\/get` shows the current setting (`autoRenew`).\n\nFree plans never renew, so they return `NOT_SUPPORTED_FOR_PRODUCT`. Supports `dryRun`. Not available with sandbox keys.\n\n**Errors:** `HOSTING_NOT_FOUND`, `NOT_SUPPORTED_FOR_PRODUCT`, `INVALID_INPUT`, `UPDATE_FAILED`.","operationId":"hostingUpdateAutoRenew","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"example":"example.com"},{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["apikey","secretapikey","autoRenew"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"autoRenew":{"type":"boolean","description":"true to renew automatically, false to let the plan end at expireDate.","example":false}}}}}},"responses":{"200":{"description":"Setting changed (or already as requested).","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string"},"product":{"type":"string","example":"managedLinks"},"autoRenew":{"type":"boolean"},"isTrial":{"type":"boolean"},"expireDate":{"type":"string","description":"When the current term (or trial) ends."},"cost":{"type":"object","properties":{"amount":{"type":"integer"},"formatted":{"type":"string"},"interval":{"type":"string"}}},"message":{"type":"string","description":"What happens next, in words to relay to the user."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}}},"400":{"description":"Error","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"429":{"$ref":"#\/components\/responses\/RateLimited"}},"x-porkbun-agent":{"safe":false,"cost":false,"destructive":false,"reversible":true,"requiresConfirmation":true},"x-porkbun-scope":"hosting"}}},"components":{"schemas":{"IpResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"yourIp":{"type":"string","example":"1.2.3.4","description":"The caller's public IP address"},"xForwardedFor":{"type":"string","example":"1.2.3.4","description":"Raw value of the X-Forwarded-For header"}},"required":["status","yourIp"]},"DnsPreflightResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","example":"example.com"},"intent":{"type":"string","example":"move-nameservers"},"safe":{"type":"boolean","description":"True only when there are no blockers AND no warnings.","example":false},"blockers":{"type":"array","description":"Check ids that will break something. Read these first.","items":{"type":"string"}},"warnings":{"type":"array","description":"Will not break outright, but usually what the customer asks about next.","items":{"type":"string"}},"checks":{"type":"array","description":"Every check that ran, passing ones included, so a caller can show what was verified rather than only what failed.","items":{"type":"object","properties":{"id":{"type":"string","example":"dnssec-active"},"severity":{"type":"string","enum":["blocker","warning","info"]},"ok":{"type":"boolean"},"title":{"type":"string"},"detail":{"type":"string","description":"The reasoning, including the rule it comes from."},"next_action":{"type":"string","description":"What to do about it. Absent on checks that passed."}}}}}},"DnsHistoryResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","example":"example.com"},"restorePoints":{"type":"array","description":"Newest first, up to 50.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Pass this to \/dns\/diff and \/dns\/restore.","example":1234},"takenAt":{"type":"string","example":"2026-09-16 22:14:19"},"reason":{"type":"string","description":"Why it was taken: before-create, before-edit, before-delete, before-wipe, before-restore-{id}, or a reason supplied by support.","example":"before-edit"},"source":{"type":"string","description":"auto (taken by the platform before a write), api, cli or archiveDns.","example":"auto"},"recordCount":{"type":"integer","description":"The size of the zone AS IT WAS at that moment, not now.","example":9},"matchesLive":{"type":"boolean","description":"True when the live zone is identical to this point, so you can see where you are without diffing.","example":false}}}}}},"DnsDiffResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","example":"example.com"},"snapshot":{"type":"object","properties":{"id":{"type":"integer","example":1234},"takenAt":{"type":"string","example":"2026-09-16 22:14:19"},"reason":{"type":"string","example":"before-edit"}}},"missing":{"type":"array","description":"In the restore point, not live. A restore adds these back.","items":{"$ref":"#\/components\/schemas\/DnsHistoryRecord"}},"extra":{"type":"array","description":"Live, not in the restore point. A restore leaves these alone unless prune is true.","items":{"$ref":"#\/components\/schemas\/DnsHistoryRecord"}},"inSync":{"type":"boolean","example":false}}},"DnsHistoryRecord":{"type":"object","properties":{"name":{"type":"string","description":"Fully qualified.","example":"www.example.com"},"type":{"type":"string","example":"A"},"content":{"type":"string","description":"For masked records (parking, ALIAS, HTTPS) this is the value you configured, not the internal host it resolves to.","example":"203.0.113.10"},"ttl":{"type":"integer","example":600},"prio":{"type":"integer","nullable":true,"example":null}}},"DnsRestoreResponse":{"type":"object","properties":{"warnings":{"$ref":"#\/components\/schemas\/DnsWriteWarnings"},"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","example":"example.com"},"restoredFrom":{"type":"integer","description":"The restore point used.","example":1234},"previousStateSavedAs":{"type":"integer","nullable":true,"description":"A new restore point holding the state from immediately BEFORE this restore. Restore that to undo this.","example":1240},"restored":{"type":"integer","description":"Records added back. A true count -- anything that could not be recreated is in `failed` instead.","example":2},"removed":{"type":"integer","description":"Records deleted, which is always 0 unless prune was true.","example":0},"failed":{"type":"array","description":"Records that could not be recreated, with the reason. Parking and other masked types are managed elsewhere and cannot be restored this way.","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"reason":{"type":"string"}}}}}},"AuthRequest":{"type":"object","description":"API credentials. Provide `apikey`\/`secretapikey` in the request body, OR supply them as `X-API-Key`\/`X-Secret-API-Key` request headers. One of the two methods is required.","properties":{"apikey":{"type":"string","description":"Your API key. Required in body when not using header auth.","example":"pk1_..."},"secretapikey":{"type":"string","description":"Your secret API key. Required in body when providing apikey.","example":"sk1_..."}}},"DomainContact":{"type":"object","description":"A domain contact. On update, a provided role requires firstName, address1, city, postalCode, country, phone, phoneCountryCode, and email. Country is an ISO 3166-1 alpha-2 code; phoneCountryCode is the numeric calling code (e.g. \"1\", \"44\"). state may be empty where not applicable.","properties":{"firstName":{"type":"string","example":"Ada"},"lastName":{"type":"string","example":"Lovelace"},"organization":{"type":"string","example":"Analytical Engines"},"address1":{"type":"string","example":"1 Countess Rd"},"address2":{"type":"string"},"address3":{"type":"string"},"city":{"type":"string","example":"London"},"state":{"type":"string","description":"State\/province; may be empty where not applicable.","example":""},"postalCode":{"type":"string","example":"NW5 1AA"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code.","example":"GB"},"phone":{"type":"string","description":"National phone number, digits only.","example":"2071234567"},"phoneCountryCode":{"type":"string","description":"Numeric international calling code.","example":"44"},"fax":{"type":"string"},"email":{"type":"string","format":"email","example":"ada@example.com"}}},"DnsWriteWarnings":{"type":"array","description":"Advisory, and present only when there is something to say. It never means the call failed. The one to handle: a DNS write is accepted and stored even when the domain is NOT delegated to our nameservers -- we keep the zone ready in case the delegation comes back -- so the write changed nothing that resolves, and this field says so. Show these to the user as written.","items":{"type":"string"},"example":["example.com is delegated to nameservers we do not operate (ns1.example-dns.net), so this change was saved to your Porkbun DNS zone but does NOT affect what resolves. Make the same change wherever those nameservers are managed, or point the domain back at ours (maceio.ns.porkbun.com, curitiba.ns.porkbun.com, salvador.ns.porkbun.com, fortaleza.ns.porkbun.com) with POST \/domain\/updateNs\/example.com -- the zone you just edited is what we would start serving."]},"AutoTopupResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"enabled":{"type":"boolean"},"threshold":{"type":"integer","nullable":true,"description":"Balance in cents below which a top-up fires; null when auto top-up is off.","example":2000},"amount":{"type":"integer","nullable":true,"description":"Configured amount to add, in cents; null when never set.","example":5000},"effectiveAmount":{"type":"integer","description":"What POST \/account\/topup would charge right now: the configured amount, or the $50 default.","example":5000},"defaultAmount":{"type":"integer","description":"The platform default used when the account has configured nothing.","example":5000},"paymentMethodOnFile":{"type":"boolean","description":"False means nothing can be charged and auto top-up cannot fire, whatever the settings say."},"cards":{"type":"array","items":{"$ref":"#\/components\/schemas\/SavedCard"},"description":"Saved payment methods, primary first. A top-up charges the primary and, if it is declined, the backups in order, at most one card per top-up. Always empty with a sandbox key. Cards are added, removed and reordered on the website only."},"balance":{"type":"integer","description":"Current account credit, in cents."},"chargesToday":{"type":"integer"},"chargesThisMonth":{"type":"integer"},"maxChargesPerDay":{"type":"integer"},"maxChargesPerMonth":{"type":"integer"},"warnings":{"type":"array","description":"Advisory. Present when the settings cannot do anything as they stand, e.g. no payment method is saved.","items":{"type":"string"}}}},"BasicResponse":{"type":"object","properties":{"warnings":{"$ref":"#\/components\/schemas\/DnsWriteWarnings"},"status":{"type":"string","enum":["SUCCESS","ERROR"],"example":"SUCCESS"},"message":{"type":"string","description":"Human-readable message. Present on ERROR, sometimes on SUCCESS."},"code":{"type":"string","description":"Machine-readable error code. Present when status is ERROR."}},"required":["status"]},"ErrorResponse":{"type":"object","required":["status","message"],"properties":{"status":{"type":"string","enum":["ERROR"],"example":"ERROR"},"message":{"type":"string","description":"Human-readable description of the error","example":"Invalid domain."},"code":{"type":"string","description":"Machine-readable error code. Use for programmatic handling; use `message` for display. See the Error codes section in the API description for a full list.","example":"INVALID_DOMAIN"},"account":{"type":"string","description":"On DOMAIN_NOT_FOUND, DOMAIN_NOT_REGISTERED, API_ACCESS_DISABLED and the \/dns\/* INVALID_DOMAIN: the username of the account the key belongs to"},"next_action":{"type":"object","description":"Present on many errors: a machine-readable remediation hint so an agent can self-correct without parsing the message. Absent when no specific action applies.","properties":{"type":{"type":"string","description":"Stable action category an agent can branch on.","enum":["fix_request","authenticate","enable_setting","verify_account","add_funds","top_up","retry","wait_and_retry","pay","use_website","choose_alternative","contact_support","none"],"example":"fix_request"},"hint":{"type":"string","description":"Human-readable instruction.","example":"Re-fetch the price via \/domain\/checkDomain and resend the matching cost."},"url":{"type":"string","description":"Where to take the action, when applicable.","example":"https:\/\/porkbun.com\/account\/api"}}}}},"RateLimitExceeded":{"type":"object","required":["status","message"],"properties":{"status":{"type":"string","enum":["ERROR"],"example":"ERROR"},"message":{"type":"string","example":"Rate limit exceeded. Please try again later."},"code":{"type":"string","example":"RATE_LIMIT_EXCEEDED"},"ttlRemaining":{"type":"integer","description":"Seconds until the rate limit window resets. Present on some endpoints."}}},"ListAllRequest":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"start":{"type":"integer","default":0,"description":"Zero-based offset for pagination (default: 0). Returns up to 1000 domains per call."},"includeLabels":{"type":"string","enum":["yes","no"],"description":"Return label metadata for each domain. Defaults to no."},"domain":{"type":"string","description":"Exact domain name match. Returns 0 or 1 result. Useful as an alternative to `\/domain\/get\/{domain}`.","example":"example.com"},"nameContains":{"type":"string","description":"Substring match against the full domain name. Case-insensitive.","example":"shop"},"expiringWithinDays":{"type":"integer","description":"Filter to domains expiring within this many days (relative to now).","example":30},"addedAfter":{"type":"string","description":"Only domains added to this account at Porkbun (registered or transferred in) at or after this date or date-time, UTC. Compare `addedDate` in the response. To sync only new domains, pass the newest `addedDate` you have seen.","example":"2026-10-01"},"addedBefore":{"type":"string","description":"Only domains added to this account at Porkbun before this date or date-time, UTC.","example":"2026-11-01"},"createdAfter":{"type":"string","description":"Only domains whose registry creation date (`createDate`) is at or after this date or date-time, UTC. A transferred-in domain keeps its original creation date, so use `addedAfter` for \"new in my account\".","example":"2024-01-01"},"createdBefore":{"type":"string","description":"Only domains whose registry creation date is before this date or date-time, UTC.","example":"2025-01-01"},"expiresAfter":{"type":"string","description":"Only domains expiring at or after this date or date-time, UTC.","example":"2027-01-01"},"expiresBefore":{"type":"string","description":"Only domains expiring before this date or date-time, UTC.","example":"2027-07-01"},"tlds":{"type":"array","items":{"type":"string"},"description":"Limit to these TLDs (without leading dot).","example":["com","io"]},"autoRenew":{"type":"string","enum":["yes","no"],"description":"Filter to domains with auto-renew on or off."},"apiAccess":{"type":"string","enum":["yes","no"],"description":"Filter to domains opted in to API access (yes) or not (no). Useful for finding domains an API key can actually operate on."},"sortName":{"type":"string","enum":["domain","tld","create_date","added_date","expire_date"],"description":"Field to sort by. Default: `expire_date` ascending. `added_date` sorts by `addedDate`."},"sortDirection":{"type":"string","enum":["asc","desc"],"description":"Sort direction. Default: asc."}}},"DomainListAllResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"count":{"type":"integer","description":"Number of domains returned in this page."},"domains":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string","example":"example.com"},"status":{"type":"string","description":"Domain registration status (e.g. ACTIVE)","example":"ACTIVE"},"tld":{"type":"string","example":"com"},"createDate":{"type":"string","example":"2021-01-15 10:00:00","description":"Registry creation date, UTC. A transferred-in domain keeps its original one."},"addedDate":{"type":"string","description":"When the domain joined Porkbun, by registration or transfer, UTC. Filter with `addedAfter` \/ `addedBefore`.","example":"2026-10-16 07:06:17"},"expireDate":{"type":"string","example":"2027-01-15 10:00:00"},"securityLock":{"type":"integer","description":"1 if transfer lock is enabled, 0 otherwise","example":1},"whoisPrivacy":{"type":"integer","description":"1 if WHOIS privacy is enabled, 0 otherwise","example":1},"autoRenew":{"type":"integer","description":"1 if auto-renew is enabled, 0 otherwise","example":1},"apiAccess":{"type":"integer","description":"1 if the domain is opted in to API access, 0 otherwise. Per-domain API access can be toggled at porkbun.com\/account or globally via the `Opt In All Domains` setting.","example":1},"notLocal":{"type":"integer","description":"1 if the domain is externally managed (not authoritative), 0 otherwise","example":0},"labels":{"type":"array","description":"Only present when includeLabels=yes","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"color":{"type":"string"}}}}}}}},"required":["status","domains"]},"UpdateNsRequest":{"type":"object","required":["apikey","secretapikey","ns"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"ns":{"type":"array","items":{"type":"string"},"description":"Ordered array of nameserver hostnames","example":["ns1.example.com","ns2.example.com"]}}},"UpdateAutoRenewRequest":{"type":"object","required":["apikey","secretapikey","status"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"status":{"type":"string","enum":["on","off"],"description":"Auto-renew status to set"},"domains":{"type":"array","items":{"type":"string"},"description":"Array of additional domain names to update. Combined with the domain in the URL path if provided."}}},"CheckDomainResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"response":{"type":"object","properties":{"avail":{"type":"string","enum":["yes","no"],"description":"Whether the domain is available for registration"},"type":{"type":"string","example":"registration","description":"The price type (always 'registration' for the primary price)"},"price":{"type":"string","description":"Registration price per year in USD","example":"9.73"},"firstYearPromo":{"type":"string","enum":["yes","no"],"description":"Whether the returned price is a first-year promotional price"},"regularPrice":{"type":"string","description":"Standard (non-promo) registration price in USD","example":"9.73"},"premium":{"type":"string","enum":["yes","no"],"description":"Whether this is a premium domain"},"minDuration":{"type":"integer","description":"Minimum registration duration in years required by the registry","example":1},"idnLanguages":{"type":"array","description":"Present for an internationalized domain name whose registry needs its language: the accepted languages. Send one as `idnLanguage` to \/domain\/create","items":{"type":"object","properties":{"name":{"type":"string","example":"French"},"code":{"type":"string","example":"FRE"}}}},"inYourAccount":{"type":"boolean","description":"Present and true when the domain is already in the caller's account. `price` is still the registration price; renewal is `additional.renewal.price`"},"aftermarketSearch":{"type":"string","description":"Present when the domain is registered to someone else: the `\/aftermarket\/search` path that shows whether it is for sale, at what price, and where to buy it.","example":"\/aftermarket\/search?query=coffee.com"},"note":{"type":"string","description":"Present with inYourAccount (which price applies to renewing) or with aftermarketSearch (the name may be for sale)"},"additional":{"type":"object","description":"Renewal and transfer pricing for this domain","properties":{"renewal":{"type":"object","properties":{"type":{"type":"string"},"price":{"type":"string"},"regularPrice":{"type":"string"}}},"transfer":{"type":"object","properties":{"type":{"type":"string"},"price":{"type":"string"},"regularPrice":{"type":"string"}}}}}}},"limits":{"type":"object","description":"Current rate limit usage for this account","properties":{"TTL":{"type":"integer","description":"Rate limit window in seconds"},"limit":{"type":"integer","description":"Maximum checks allowed in the window"},"used":{"type":"integer","description":"Number of checks used so far in the current window"},"naturalLanguage":{"type":"string","description":"Human-readable rate limit summary","example":"1 out of 10 checks within 10 seconds used."}}},"ttlRemaining":{"type":"integer","description":"Seconds remaining until the rate limit window resets"}},"required":["status","response"]},"DryRunPreviewResponse":{"type":"object","description":"Returned (HTTP 200) by \/domain\/create, \/domain\/renew, and \/domain\/transfer when the request includes `dryRun: true`. All validation has run but nothing was created or charged. All money amounts are in pennies (USD cents).","example":{"status":"SUCCESS","dryRun":true,"wouldSucceed":true,"operation":"registration","domain":"example.com","tld":"com","available":"available","premium":false,"duration":1,"cost":973,"costDisplay":"$9.73","balance":5000,"sufficientFunds":true,"monthlySpendLimit":50000,"monthlySpendSoFar":12000,"withinMonthlySpendLimit":true,"message":"Dry run: this registration would succeed and cost $9.73. No order was created and no charge was made."},"properties":{"status":{"type":"string","example":"SUCCESS"},"dryRun":{"type":"boolean","example":true,"description":"Always true on a dry-run preview \u2014 distinguishes it from a real success response."},"wouldSucceed":{"type":"boolean","description":"True if the operation would complete given current funds and spend-limit state. (Hard validation failures \u2014 unavailable, bad price, ineligible \u2014 return a normal error response instead of a preview.)"},"operation":{"type":"string","enum":["registration","renewal","transfer"]},"domain":{"type":"string","example":"example.com"},"tld":{"type":"string","example":"com"},"available":{"type":"string","description":"Availability as reported by the registry check (e.g. `available` \/ `unavailable`).","example":"available"},"premium":{"type":"boolean","description":"Whether the domain is a premium\/aftermarket name."},"duration":{"type":"integer","description":"Term in years that would be purchased.","example":1},"cost":{"type":"integer","description":"Total cost in pennies that would be charged.","example":973},"costDisplay":{"type":"string","description":"Human-readable cost.","example":"$9.73"},"balance":{"type":"integer","description":"Current account credit balance in pennies."},"sufficientFunds":{"type":"boolean","description":"Whether the balance covers the cost."},"monthlySpendLimit":{"type":"integer","description":"The account's monthly API spend cap in pennies. Present only if a cap is configured."},"monthlySpendSoFar":{"type":"integer","description":"API spend so far this calendar month in pennies. Present only if a cap is configured."},"withinMonthlySpendLimit":{"type":"boolean","description":"Whether this cost stays within the monthly cap. Present only if a cap is configured."},"message":{"type":"string","description":"Human-readable summary of the preview outcome."},"requestId":{"type":"string","description":"Per-request UUID (also in the X-Request-Id header). Present on every API response."}},"required":["status","dryRun","wouldSucceed","operation","domain","cost"]},"CreateDomainRequest":{"type":"object","required":["apikey","secretapikey","cost","agreeToTerms"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"cost":{"type":"integer","description":"The registration cost in pennies (USD cents). Must exactly equal the total price for the domain at its minimum registration duration. Obtain this from \/domain\/checkDomain first."},"agreeToTerms":{"type":"string","enum":["yes","1"],"description":"Must be 'yes' or '1' to confirm agreement to the Domain Name Registration Agreement, Product Terms of Service, Data Processing Addendum, Privacy Policy, and automatic renewal terms."},"idnLanguage":{"type":"string","description":"Required for an internationalized domain name (IDN) under a TLD whose registry needs its language: a code or language name from `idnLanguages`, as returned by \/domain\/checkDomain or by the IDN_LANGUAGE_REQUIRED error (any case). Ignored for plain ASCII names.","example":"FRE"},"whoisPrivacy":{"type":"boolean","description":"Optional. Override WHOIS privacy for this registration. When omitted, the account-level default is used (set under Account Security Settings on porkbun.com\/account \u2014 defaults to enabled). Pass `true` to force-enable privacy or `false` to register with public contact info. Strings `\"on\"`\/`\"off\"`, `\"true\"`\/`\"false\"`, `\"yes\"`\/`\"no\"`, `\"1\"`\/`\"0\"` are also accepted. Has no effect on TLDs that don't support WHOIS privacy (privacy stays off regardless)."},"payWith":{"type":"string","enum":["usdc","card"],"description":"Optional. `\"usdc\"` pays for this purchase directly in USDC on Base over x402 instead of from account credit. The first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with a `PAYMENT-SIGNATURE` header, and the purchase completes in that call. If it then fails, the payment stays on the account as account credit; it is not sent back to the wallet. If your wallet pays URLs itself instead, pay the 402's `x402Url` (or have a person pay `payUrl`) and repeat the request with `usdcCheckoutId`. See the guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402. `\"card\"` pays by card through a Stripe Link agent wallet over MPP instead, with a real hold: the 402 carries a single-use `payUrl` and a `cardPaymentId`; pay the link (it authorizes, not charges), then repeat with `cardPaymentId`; the hold is captured if the purchase succeeds and released if it fails. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp"},"usdcCheckoutId":{"type":"string","description":"With `payWith: \"usdc\"`: the `checkoutId` from an earlier `PAYMENT_REQUIRED`, once its `x402Url` or `payUrl` has been paid. Send it with otherwise the same request; the purchase completes if the checkout is paid and was opened for this purchase."},"cardPaymentId":{"type":"string","description":"With `payWith: \"card\"`: the `cardPaymentId` from an earlier `PAYMENT_REQUIRED`, once its `payUrl` has been paid (the card is then held). Send it with otherwise the same request; the hold is captured if the purchase succeeds and released if not."},"dryRun":{"type":"boolean","description":"Optional. When true, runs all pre-flight validation (availability, pricing, cost match, eligibility, funds, monthly spend limit, including the $100 default when none is set) and returns a preview with `dryRun: true` and `wouldSucceed` WITHOUT creating an order or charging. Nothing is registered and the rate-limit budget is not consumed. Use it to safely confirm an operation before committing."}}},"CreateDomainResponse":{"type":"object","example":{"status":"SUCCESS","domain":"example.com","cost":973,"orderId":12345678,"limits":{"attempts":{"TTL":1,"limit":1,"used":1,"naturalLanguage":"1 out of 1 create attempts within 1 seconds used."},"success":{"TTL":86400,"limit":50,"used":1,"naturalLanguage":"1 out of 1000 successful creates within 86400 seconds used."}},"balance":4027,"ttlRemaining":86400,"requestId":"019e04fa-258d-7d11-aa86-4d5795c3fe8f"},"properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","description":"The registered domain name","example":"example.com"},"cost":{"type":"integer","description":"The total amount charged in pennies"},"orderId":{"type":"integer","description":"Internal Porkbun order ID"},"limits":{"type":"object","description":"Current rate limit state for both attempt and success limits","properties":{"attempts":{"type":"object","description":"Create-attempt rate limit state","properties":{"TTL":{"type":"integer","description":"Window in seconds"},"limit":{"type":"integer","description":"Max attempts allowed"},"used":{"type":"integer","description":"Attempts used"},"naturalLanguage":{"type":"string"}}},"success":{"type":"object","description":"Successful-registration rate limit state","properties":{"TTL":{"type":"integer","description":"Window in seconds"},"limit":{"type":"integer","description":"Max successes allowed"},"used":{"type":"integer","description":"Successes used"},"naturalLanguage":{"type":"string"}}}}},"balance":{"type":"integer","description":"Remaining account credit balance in pennies after the charge"},"ttlRemaining":{"type":"integer","description":"Seconds until the success rate limit window resets"},"requestId":{"type":"string","description":"Per-request UUID (also in the X-Request-Id header). Present on every API response."}},"required":["status","domain","orderId"]},"AddUrlForwardRequest":{"type":"object","required":["apikey","secretapikey","location","type","includePath","wildcard"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"subdomain":{"type":"string","description":"Subdomain to forward (optional, leave blank or omit for root domain). Alphanumeric and hyphens only.","example":"www"},"location":{"type":"string","description":"Destination URL to forward to","example":"https:\/\/destination.example.com"},"type":{"type":"string","enum":["temporary","permanent","masked"],"description":"Redirect kind. 'permanent' = HTTP 301; 'temporary' = HTTP 302 (default); 'masked' = loads the destination in a frame (URL masking). For a precise code \u2014 including a 307 temporary redirect \u2014 use `redirectType`."},"redirectType":{"type":"string","enum":["301","302","307","masked"],"description":"Optional. The exact redirect type; takes precedence over `type` when supplied. 301 = permanent, 302 or 307 = temporary (this is how you request 307), masked = URL masking. Omit to derive from `type` (temporary->302, permanent->301).","example":"307"},"includePath":{"type":"string","enum":["yes","no"],"description":"Whether to append the request URI path to the forwarding destination"},"wildcard":{"type":"string","enum":["yes","no"],"description":"Whether to also forward all subdomains of the forwarded subdomain"}}},"GetUrlForwardingResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"forwards":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Numeric record ID"},"subdomain":{"type":"string","description":"Subdomain portion (empty string for root)"},"location":{"type":"string"},"type":{"type":"string","enum":["permanent","temporary","masked"],"description":"Redirect kind: 'permanent' (HTTP 301), 'temporary' (HTTP 302 or 307), or 'masked' (URL masking). Use `redirectType` for the exact code."},"redirectType":{"type":"string","enum":["301","302","307","masked"],"description":"The exact stored redirect type. Distinguishes 302 from 307 (both report as 'temporary' in `type`); round-trips directly into addUrlForward's `redirectType`."},"includePath":{"type":"string","enum":["yes","no"]},"wildcard":{"type":"string","enum":["yes","no"]}}}}},"required":["status","forwards"]},"GlueRecordRequest":{"type":"object","required":["apikey","secretapikey","ips"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"ips":{"type":"array","items":{"type":"string"},"description":"Array of IP addresses (IPv4 and\/or IPv6) to associate with the host record","example":["1.2.3.4","2001:db8::1"]}}},"DnsRecordsResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"cloudflare":{"type":"string","enum":["enabled","disabled"],"description":"Whether Cloudflare proxy is enabled for this domain"},"records":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Numeric record ID"},"name":{"type":"string","description":"Fully-qualified record name","example":"www.example.com"},"type":{"type":"string","description":"DNS record type","example":"A"},"content":{"type":"string","description":"Record value","example":"1.2.3.4"},"ttl":{"type":"string","description":"Time to live in seconds","example":"600"},"prio":{"type":"string","nullable":true,"description":"Priority (used for MX, SRV records). Null if not applicable.","example":"10"},"notes":{"type":"string","nullable":true,"description":"Optional notes attached to the record. Null if not set."}}}}},"required":["status","records"]},"CreateDnsRequest":{"type":"object","required":["apikey","secretapikey","type","content"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"name":{"type":"string","description":"Subdomain for the record (e.g. 'www', '*' for wildcard, blank for root). Do not include the domain name itself.","example":"www"},"type":{"type":"string","description":"DNS record type","enum":["A","AAAA","MX","CNAME","ALIAS","TXT","NS","SRV","TLSA","CAA","SSHFP","HTTPS","SVCB","DS"],"example":"A"},"content":{"type":"string","description":"The record value","example":"1.2.3.4"},"ttl":{"type":"integer","description":"Time to live in seconds. Minimum is determined by account settings (typically 600). Defaults to the account minimum if omitted or 0.","example":600},"prio":{"type":"integer","description":"Priority for MX and SRV records. Defaults to 0 if omitted.","example":10},"notes":{"type":"string","description":"Optional notes to store with the record (not served in DNS)"},"dryRun":{"type":"boolean","description":"If true, validate only \u2014 checks ownership\/type\/permissions and returns wouldSucceed without creating the record.","example":false}}},"EditDnsRequest":{"type":"object","required":["apikey","secretapikey"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"name":{"type":"string","description":"Subdomain for the record. Do not include the domain name itself."},"type":{"type":"string","description":"DNS record type","enum":["A","AAAA","MX","CNAME","ALIAS","TXT","NS","SRV","TLSA","CAA","SSHFP","HTTPS","SVCB","DS"],"example":"A"},"content":{"type":"string","description":"The record value","example":"5.6.7.8"},"ttl":{"type":"integer","description":"Time to live in seconds (optional)"},"prio":{"type":"integer","description":"Priority for MX\/SRV records (optional)"},"notes":{"type":"string","description":"Notes (optional). Pass empty string to clear notes; omit or pass null to leave unchanged."}}},"EditDnsByNameTypeRequest":{"type":"object","required":["apikey","secretapikey","content"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"content":{"type":"string","description":"New record value to set on all matching records","example":"5.6.7.8"},"ttl":{"type":"integer","description":"Time to live in seconds (optional)"},"prio":{"type":"integer","description":"Priority (optional)"},"notes":{"type":"string","description":"Notes to store with the record (not served in DNS). Pass an empty string to clear existing notes; pass null or omit this field to leave notes unchanged."}}},"CreateDnssecRequest":{"type":"object","required":["apikey","secretapikey","keyTag","alg","digestType","digest"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"keyTag":{"type":"string","description":"DNSSEC key tag","example":"12345"},"alg":{"type":"string","description":"DS Data algorithm number (e.g. 13 for ECDSA P-256 SHA-256)","example":"13"},"digestType":{"type":"string","description":"Digest type number (e.g. 2 for SHA-256)","example":"2"},"digest":{"type":"string","description":"Hex-encoded digest value","example":"ABCD1234..."},"maxSigLife":{"type":"string","description":"Maximum signature lifetime in seconds (optional, registry-specific)"},"keyDataFlags":{"type":"string","description":"Key data flags (optional, used when submitting full key data)"},"keyDataProtocol":{"type":"string","description":"Key data protocol (optional)"},"keyDataAlgo":{"type":"string","description":"Key data algorithm (optional)"},"keyDataPubKey":{"type":"string","description":"Key data public key in base64 (optional)"}}},"RenewDomainRequest":{"type":"object","required":["apikey","secretapikey","cost"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"cost":{"type":"integer","description":"The renewal cost in pennies (USD cents). Must exactly equal the total price for the domain at its minimum renewal duration. Obtain this from \/domain\/checkDomain first."},"payWith":{"type":"string","enum":["usdc","card"],"description":"Optional. `\"usdc\"` pays for this purchase directly in USDC on Base over x402 instead of from account credit. The first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with a `PAYMENT-SIGNATURE` header, and the purchase completes in that call. If it then fails, the payment stays on the account as account credit; it is not sent back to the wallet. If your wallet pays URLs itself instead, pay the 402's `x402Url` (or have a person pay `payUrl`) and repeat the request with `usdcCheckoutId`. See the guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402. `\"card\"` pays by card through a Stripe Link agent wallet over MPP instead, with a real hold: the 402 carries a single-use `payUrl` and a `cardPaymentId`; pay the link (it authorizes, not charges), then repeat with `cardPaymentId`; the hold is captured if the purchase succeeds and released if it fails. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp"},"usdcCheckoutId":{"type":"string","description":"With `payWith: \"usdc\"`: the `checkoutId` from an earlier `PAYMENT_REQUIRED`, once its `x402Url` or `payUrl` has been paid. Send it with otherwise the same request; the purchase completes if the checkout is paid and was opened for this purchase."},"cardPaymentId":{"type":"string","description":"With `payWith: \"card\"`: the `cardPaymentId` from an earlier `PAYMENT_REQUIRED`, once its `payUrl` has been paid (the card is then held). Send it with otherwise the same request; the hold is captured if the purchase succeeds and released if not."},"dryRun":{"type":"boolean","description":"Optional. When true, runs all pre-flight validation and returns a preview with `dryRun: true` and `wouldSucceed` WITHOUT renewing or charging. Nothing changes and the rate-limit budget is not consumed."}}},"RenewDomainResponse":{"type":"object","example":{"status":"SUCCESS","domain":"example.com","expirationDate":"2027-04-21","cost":1099,"orderId":12345679,"limits":{"attempts":{"TTL":1,"limit":1,"used":1,"naturalLanguage":"1 out of 1 renewal attempts within 1 seconds used."},"success":{"TTL":86400,"limit":50,"used":1,"naturalLanguage":"1 out of 1000 successful renewals within 86400 seconds used."}},"balance":3928,"ttlRemaining":86400,"requestId":"019e04fa-3c11-7a02-9bd2-1f7c0e4a8b55"},"required":["status","domain","orderId"],"properties":{"status":{"type":"string","example":"SUCCESS"},"domain":{"type":"string","description":"The renewed domain name","example":"example.com"},"expirationDate":{"type":"string","description":"The new expiration date returned by the registry","example":"2026-04-21"},"cost":{"type":"integer","description":"The total amount charged in pennies"},"orderId":{"type":"integer","description":"Internal Porkbun order ID"},"limits":{"type":"object","description":"Current rate limit state for both attempt and success limits","properties":{"attempts":{"type":"object","properties":{"TTL":{"type":"integer","description":"Window in seconds"},"limit":{"type":"integer","description":"Max attempts allowed"},"used":{"type":"integer","description":"Attempts used"},"naturalLanguage":{"type":"string"}}},"success":{"type":"object","properties":{"TTL":{"type":"integer","description":"Window in seconds"},"limit":{"type":"integer","description":"Max successes allowed"},"used":{"type":"integer","description":"Successes used"},"naturalLanguage":{"type":"string"}}}}},"balance":{"type":"integer","description":"Remaining account credit balance in pennies after the charge"},"ttlRemaining":{"type":"integer","description":"Seconds until the success rate limit window resets"},"requestId":{"type":"string","description":"Per-request UUID (also in the X-Request-Id header). Present on every API response."}}},"TransferDomainRequest":{"type":"object","required":["secretapikey","apikey","authCode","cost"],"properties":{"secretapikey":{"type":"string","description":"Your API secret key."},"apikey":{"type":"string","description":"Your API key."},"authCode":{"type":"string","description":"The EPP auth code for the domain."},"cost":{"type":"integer","description":"The transfer cost in cents as returned by the pricing API. Must match exactly."},"holdForDnsSetup":{"type":"boolean","description":"Charge the transfer but hold it at `PENDINGDNS` instead of releasing it to the registry, so the DNS zone can be built before the domain moves. This is the no-downtime path: hold, then `POST \/domain\/prepareTransfer\/{domain}`, then load records with `\/dns\/import\/{domain}`, then `POST \/domain\/startTransfer\/{domain}`. Nothing releases a held transfer on a timer. Not available for .uk or Handshake TLDs, which return `TRANSFER_HOLD_NOT_AVAILABLE` and are not charged.","example":true},"payWith":{"type":"string","enum":["usdc","card"],"description":"Optional. `\"usdc\"` pays for this purchase directly in USDC on Base over x402 instead of from account credit. The first call answers HTTP 402 (`PAYMENT_REQUIRED`) with Coinbase's terms in the `PAYMENT-REQUIRED` header and charges nothing; an x402 client pays and repeats the same request with a `PAYMENT-SIGNATURE` header, and the purchase completes in that call. If it then fails, the payment stays on the account as account credit; it is not sent back to the wallet. If your wallet pays URLs itself instead, pay the 402's `x402Url` (or have a person pay `payUrl`) and repeat the request with `usdcCheckoutId`. See the guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-usdc-x402. `\"card\"` pays by card through a Stripe Link agent wallet over MPP instead, with a real hold: the 402 carries a single-use `payUrl` and a `cardPaymentId`; pay the link (it authorizes, not charges), then repeat with `cardPaymentId`; the hold is captured if the purchase succeeds and released if it fails. Guide: https:\/\/porkbun.com\/llms\/guides\/pay-with-card-mpp"},"usdcCheckoutId":{"type":"string","description":"With `payWith: \"usdc\"`: the `checkoutId` from an earlier `PAYMENT_REQUIRED`, once its `x402Url` or `payUrl` has been paid. Send it with otherwise the same request; the purchase completes if the checkout is paid and was opened for this purchase."},"cardPaymentId":{"type":"string","description":"With `payWith: \"card\"`: the `cardPaymentId` from an earlier `PAYMENT_REQUIRED`, once its `payUrl` has been paid (the card is then held). Send it with otherwise the same request; the hold is captured if the purchase succeeds and released if not."},"dryRun":{"type":"boolean","description":"Optional. When true, runs all pre-flight validation and returns a preview with `dryRun: true` and `wouldSucceed` WITHOUT initiating the transfer or charging. Nothing changes and the rate-limit budget is not consumed."}}},"TransferDomainResponse":{"type":"object","example":{"status":"SUCCESS","domain":"example.com","orderId":12345680,"transferId":98765,"message":"Transfer initiated. The domain transfer process has begun and typically takes 5-7 days to complete.","balance":2829,"ttlRemaining":86400,"limits":{"attempts":{"TTL":1,"limit":1,"used":1,"naturalLanguage":"1 out of 1 transfer attempts within 1 seconds used."},"success":{"TTL":86400,"limit":50,"used":1,"naturalLanguage":"1 out of 1000 successful transfers within 86400 seconds used."}},"requestId":"019e04fa-5f22-7c93-8a41-2e9d0b3f6c77"},"properties":{"status":{"type":"string","enum":["SUCCESS","ERROR"]},"domain":{"type":"string"},"orderId":{"type":"integer"},"transferId":{"type":"integer"},"message":{"type":"string"},"balance":{"type":"integer","description":"Remaining account credit balance in cents."},"ttlRemaining":{"type":"integer"},"limits":{"type":"object","properties":{"attempts":{"type":"object"},"success":{"type":"object"}}},"requestId":{"type":"string","description":"Per-request UUID (also in the X-Request-Id header). Present on every API response."}}},"GetTransferResponse":{"type":"object","properties":{"status":{"type":"string","enum":["SUCCESS","ERROR"]},"transfer":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["NEW","PENDINGAUTH","PENDINGSUBMIT","PENDINGTRANSFER","DONE","CANCELED","INIT"]},"statusDescription":{"type":"string"},"transferDate":{"type":"string","format":"date-time"},"orderId":{"type":"integer"}}}}},"ListTransfersResponse":{"type":"object","properties":{"status":{"type":"string","enum":["SUCCESS","ERROR"]},"transfers":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["NEW","PENDINGAUTH","PENDINGSUBMIT","PENDINGTRANSFER","DONE","CANCELED","INIT"]},"statusDescription":{"type":"string"},"transferDate":{"type":"string","format":"date-time"},"orderId":{"type":"integer"}}}}}},"BalanceResponse":{"type":"object","properties":{"status":{"type":"string","enum":["SUCCESS","ERROR"]},"balance":{"type":"integer","description":"Available account credit balance in cents."},"display":{"type":"string","description":"Human-readable balance string (e.g. `$12.34`)."}}},"ApiSettingsResponse":{"type":"object","properties":{"status":{"type":"string","enum":["SUCCESS","ERROR"]},"settings":{"type":"object","properties":{"monthlySpendLimit":{"type":"integer","nullable":true,"description":"The monthly spend limit the account holder set, in cents. `null` means none is set; `spendLimit` shows what is actually enforced."},"lowBalanceAlert":{"type":"integer","nullable":true,"description":"Send an alert email when balance drops below this amount (cents). `null` = disabled."},"autoTopup":{"type":"boolean","description":"Whether automatic balance top-up is enabled."},"topupThreshold":{"type":"integer","nullable":true,"description":"Trigger auto top-up when balance falls below this amount (cents). `null` = disabled."},"topupAmount":{"type":"integer","nullable":true,"description":"Amount to add during an auto top-up (cents)."}}},"monthlySpend":{"type":"integer","description":"Total API spend in the current calendar month, in cents."},"spendLimit":{"type":"object","description":"The monthly cap on API purchases actually enforced (registrations, renewals, transfers, closeouts, paid hosting).","properties":{"limit":{"type":"integer","nullable":true,"description":"Cents. `null` means no cap."},"source":{"type":"string","enum":["account","default","none"],"description":"`account`: the account holder's own limit. `default`: none set, so the $100 default applies. `none`: none set and the account bought over the API before the default existed (2026-10-03), so purchases are not capped."},"spent":{"type":"integer","description":"API spending this calendar month, in cents."},"remaining":{"type":"integer","nullable":true,"description":"Cents left this month, or `null` with no cap."}}},"topupLimit":{"type":"object","description":"The monthly cap on credit added over the API: card top-ups, auto top-ups set off by an API purchase, MPP and USDC together.","properties":{"limit":{"type":"integer","description":"Cents: the monthly spend limit, or $100 when none is set."},"source":{"type":"string","enum":["account","default"]},"added":{"type":"integer","nullable":true,"description":"Credit added over the API this calendar month, in cents."}}}}},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"integer","description":"Numeric endpoint id.","example":42},"url":{"type":"string","description":"Destination HTTPS URL.","example":"https:\/\/example.com\/porkbun\/webhook"},"secret":{"type":"string","description":"Signing secret used to verify the X-Porkbun-Signature header (HMAC-SHA256 key). Returned on create and rotateSecret, and on list\/get only to keys with full access or the account scope (omitted for read-only keys, since it can sign events to your endpoint).","example":"9f86d081..."},"events":{"type":"array","items":{"type":"string"},"description":"Subscribed event types, or [\"*\"] for all.","example":["domain.registered","dns.*"]},"status":{"type":"string","description":"ACTIVE or DISABLED.","example":"ACTIVE","enum":["ACTIVE","DISABLED"]},"consecutiveFailures":{"type":"integer","description":"Consecutive failed deliveries; reset to 0 on the next success or on re-enable. The endpoint auto-disables at 20.","example":0},"lastSuccessDate":{"type":"string","description":"Timestamp of the last successful delivery, or null.","example":"2026-06-17 18:30:00","nullable":true},"lastFailureDate":{"type":"string","description":"Timestamp of the last failed delivery, or null.","nullable":true},"lastError":{"type":"string","description":"Last delivery error, or null.","nullable":true},"createDate":{"type":"string","description":"When the endpoint was created.","example":"2026-06-17 12:00:00"}}},"WebhookEndpointResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"endpoint":{"$ref":"#\/components\/schemas\/WebhookEndpoint"}}},"WebhookListResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"endpoints":{"type":"array","items":{"$ref":"#\/components\/schemas\/WebhookEndpoint"}}}},"WebhookEventTypesResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"eventTypes":{"type":"array","items":{"type":"string"},"example":["domain.registered","domain.renewed","domain.transfer.completed","domain.expiring","dns.record.created","dns.record.updated","dns.record.deleted","cloudflare.connect.completed","cloudflare.connect.failed","account.verification.code_sent","account.verification.completed"]}}},"CloseoutItem":{"type":"object","properties":{"closeoutId":{"type":"integer","description":"Provider id for this listing."},"domain":{"type":"string"},"tld":{"type":"string"},"price":{"type":"integer","description":"Current closeout price, integer US cents. Descends on a schedule."},"providerRenewalPrice":{"type":"integer","nullable":true,"description":"The provider's own renewal figure. Indicative only \u2014 the amount actually charged is `renewalPrice` from \/closeout\/get\/{domain}, which uses Porkbun pricing."},"renewalPrice":{"type":"integer","nullable":true,"description":"Porkbun's renewal or transfer price for the year included. Only returned by \/closeout\/get\/{domain}."},"totalPrice":{"type":"integer","nullable":true,"description":"price + renewalPrice. What \/closeout\/buy charges. Only returned by \/closeout\/get\/{domain}."},"localDomain":{"type":"boolean","nullable":true,"description":"True when the name is already at Porkbun (renewed rather than transferred in). Only returned by \/closeout\/get\/{domain}."},"age":{"type":"integer","nullable":true,"description":"Domain age in years."},"registrationDate":{"type":"string","nullable":true,"description":"Original registration date, ISO 8601 UTC."},"endTime":{"type":"string","nullable":true,"description":"When the listing ends, ISO 8601 UTC."},"nameLength":{"type":"integer","nullable":true},"isIdn":{"type":"boolean"},"estibotAppraisal":{"type":"integer","nullable":true,"description":"Third-party appraisal in US cents. Provider-supplied; treat as a weak signal."},"monthlyVisitors":{"type":"integer","nullable":true},"inboundLinks":{"type":"integer","nullable":true},"expiredRevenue":{"type":"integer","nullable":true},"otherTldCount":{"type":"integer","nullable":true,"description":"How many other TLDs the same SLD is registered in."}}},"DnsRecordInput":{"type":"object","properties":{"name":{"type":"string","description":"Subdomain prefix only, or empty string for the apex. `@` and a fully-qualified name are both accepted and normalised.","example":"www"},"type":{"type":"string","description":"Record type. NS and SOA are ignored on import.","example":"A"},"content":{"type":"string","example":"203.0.113.10"},"ttl":{"type":"integer","description":"Raised to the API minimum if lower.","example":600},"prio":{"type":"integer","nullable":true,"description":"Priority, for MX and SRV.","example":10}}},"CreateWebhookRequest":{"type":"object","required":["apikey","secretapikey","url"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"url":{"type":"string","description":"HTTPS endpoint that receives the signed POST. Port 443 only, and the hostname must resolve to a public internet address \u2014 private\/loopback\/reserved targets are refused.","example":"https:\/\/example.com\/porkbun\/webhook"},"events":{"type":"array","items":{"type":"string"},"description":"Event types to subscribe to. Omit, or pass [\"*\"], for all events. Prefix wildcards like \"dns.*\" are allowed.","example":["domain.registered","dns.*"]}}},"UpdateWebhookRequest":{"type":"object","required":["apikey","secretapikey","id"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"id":{"type":"integer","description":"Endpoint id to update.","example":42},"url":{"type":"string","description":"HTTPS endpoint that receives the signed POST. Port 443 only, and the hostname must resolve to a public internet address \u2014 private\/loopback\/reserved targets are refused.","example":"https:\/\/example.com\/hook"},"events":{"type":"array","items":{"type":"string"},"description":"Replacement event subscription list (optional)."},"status":{"type":"string","description":"Set ACTIVE to resume (also clears the failure counter) or DISABLED to pause (optional).","example":"ACTIVE","enum":["ACTIVE","DISABLED"]}}},"WebhookIdRequest":{"type":"object","required":["apikey","secretapikey","id"],"properties":{"apikey":{"type":"string"},"secretapikey":{"type":"string"},"id":{"type":"integer","description":"Endpoint id.","example":42}}},"WebhookTestResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"eventId":{"type":"string","description":"UUID of the queued webhook.test event.","example":"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a90"},"message":{"type":"string","example":"Test event queued. It will be delivered to your endpoint within about a minute."}}},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"integer","description":"Delivery id.","example":9001},"endpointId":{"type":"integer","description":"Endpoint this delivery targets.","example":42},"eventType":{"type":"string","example":"domain.renewed"},"eventId":{"type":"string","description":"UUIDv7 of the event; sent as X-Porkbun-Webhook-Id. Stable across resends.","example":"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a90"},"status":{"type":"string","description":"PENDING (queued\/awaiting retry), PROCESSING (in flight), DELIVERED (2xx), or FAILED (gave up after max attempts).","example":"DELIVERED","enum":["PENDING","PROCESSING","DELIVERED","FAILED"]},"attempts":{"type":"integer","description":"Delivery attempts made so far.","example":1},"maxAttempts":{"type":"integer","description":"Attempt cap before the delivery is marked FAILED.","example":6},"httpStatus":{"type":"integer","description":"HTTP status from the last attempt, or null if not yet attempted \/ connection error.","example":200,"nullable":true},"lastError":{"type":"string","description":"Last delivery error, or null.","nullable":true},"nextAttemptAt":{"type":"string","description":"When the next attempt is scheduled (for PENDING rows).","example":"2026-06-17 18:31:00"},"createDate":{"type":"string","description":"When the delivery was queued.","example":"2026-06-17 18:30:00"},"deliveredDate":{"type":"string","description":"When it was successfully delivered, or null.","nullable":true},"payload":{"type":"object","description":"The full event envelope that was sent. Present only on GET \/webhook\/delivery\/{id}."}}},"WebhookDeliveryListResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"deliveries":{"type":"array","items":{"$ref":"#\/components\/schemas\/WebhookDelivery"},"description":"Newest first. The bulky `payload` field is omitted here."},"total":{"type":"integer","description":"Total matching deliveries (for pagination).","example":128},"start":{"type":"integer","description":"Offset of this page.","example":0},"limit":{"type":"integer","description":"Page size used.","example":50},"message":{"type":"string","example":"Delivery history is retained for about 30 days."}}},"WebhookDeliveryResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"delivery":{"$ref":"#\/components\/schemas\/WebhookDelivery"}}},"WebhookResendResponse":{"type":"object","properties":{"status":{"type":"string","example":"SUCCESS"},"delivery":{"$ref":"#\/components\/schemas\/WebhookDelivery"},"message":{"type":"string","example":"Delivery re-queued. It will be re-sent within about a minute, reusing the original event id."}}},"AftermarketListing":{"type":"object","properties":{"domain":{"type":"string","example":"fun.now"},"tld":{"type":"string","example":"now"},"price":{"type":"integer","description":"Integer US cents. What it means depends on saleType: see priceMeaning.","example":4988800},"priceDisplay":{"type":"string","example":"$49,888.00"},"priceMeaning":{"type":"string","enum":["buy_now_price","minimum_offer","current_bid"]},"saleType":{"type":"string","enum":["buy_now","make_offer","auction"]},"source":{"type":"string","description":"afternic, sedo, atom, namebright, dax or marketplace (Porkbun Marketplace).","example":"sedo"},"fastTransfer":{"type":"boolean","description":"The seller supports a fast transfer.","example":false},"buyUrl":{"type":"string","description":"Where the user buys (or bids on) it on porkbun.com.","example":"https:\/\/porkbun.com\/checkout\/search?q=fun.now"}}},"SavedCard":{"type":"object","description":"A payment method saved on the account. The first is the primary; the rest are backups, tried in this order when the primary is declined.","properties":{"role":{"type":"string","enum":["primary","backup"]},"type":{"type":"string","enum":["card","link"],"description":"`card`, or `link` for a Stripe Link wallet (no brand or digits)."},"brand":{"type":"string","example":"visa","description":"Card brand, or `link`."},"last4":{"type":"string","nullable":true,"example":"4242"},"expMonth":{"type":"integer","nullable":true,"example":1},"expYear":{"type":"integer","nullable":true,"example":2030},"nickname":{"type":"string","nullable":true,"description":"The name the account holder gave the card on the website, if any."},"status":{"type":"string","enum":["active","expired","declined"],"description":"`declined`: a hard decline (closed account, wrong number, lost card...). It is skipped until `retryAfter`, then tried after the other cards."},"lastDeclineCode":{"type":"string","nullable":true,"description":"The most recent decline code, kept even when a soft decline left the card `active`.","example":"insufficient_funds"},"retryAfter":{"type":"string","nullable":true,"description":"UTC date-time until which a `declined` card is skipped."},"usedForTopups":{"type":"boolean","description":"Top-ups charge cards only, so a Link wallet is `false` (renewals can still use it)."}}},"ChargedCard":{"type":"object","description":"The card a top-up charged.","properties":{"type":{"type":"string","enum":["card","link"],"description":"`card`, or `link` for a Stripe Link wallet (no brand or digits)."},"brand":{"type":"string","example":"visa","description":"Card brand, or `link`."},"last4":{"type":"string","nullable":true,"example":"4242"},"expMonth":{"type":"integer","nullable":true,"example":1},"expYear":{"type":"integer","nullable":true,"example":2030},"nickname":{"type":"string","nullable":true,"description":"The name the account holder gave the card on the website, if any."},"usedBackup":{"type":"boolean","description":"True when the primary card was declined and a backup paid."}}},"DeclinedCard":{"type":"object","properties":{"type":{"type":"string","enum":["card","link"],"description":"`card`, or `link` for a Stripe Link wallet (no brand or digits)."},"brand":{"type":"string","example":"visa","description":"Card brand, or `link`."},"last4":{"type":"string","nullable":true,"example":"4242"},"expMonth":{"type":"integer","nullable":true,"example":1},"expYear":{"type":"integer","nullable":true,"example":2030},"nickname":{"type":"string","nullable":true,"description":"The name the account holder gave the card on the website, if any."},"declineCode":{"type":"string","example":"insufficient_funds"}}},"LinkInBioLink":{"type":"object","description":"One entry on the page, in display order.","properties":{"id":{"type":"integer","description":"Pass to editLink \/ deleteLink \/ reorderLinks.","example":1292105},"type":{"type":"string","description":"`link`, `featured` (shown larger) or `category` (a heading with no URL).","enum":["link","featured","category"]},"title":{"type":"string","description":"Text shown on the button or heading.","example":"Shop my store"},"url":{"type":"string","nullable":true,"description":"Where it goes; null for a category.","example":"https:\/\/example.com\/shop"},"enabled":{"type":"boolean","description":"False hides it without deleting it."},"position":{"type":"integer","description":"1 = first on the page."},"startDate":{"type":"string","nullable":true,"description":"Shown only from this time (UTC, ISO 8601). Paid plans.","format":"date-time"},"endDate":{"type":"string","nullable":true,"description":"Hidden after this time (UTC, ISO 8601). Paid plans.","format":"date-time"},"animation":{"type":"string","nullable":true,"description":"`pulse`, `bounce` or `shake`. Paid plans.","enum":["pulse","bounce","shake"]},"protection":{"type":"object","nullable":true,"description":"What a visitor has to do before the link opens.","properties":{"type":{"type":"string","description":"`code`: enter a passcode (paid plans). `birthday`: confirm a minimum age. `sensitive`: a content warning.","enum":["code","birthday","sensitive"]},"value":{"type":"string","nullable":true,"description":"The passcode for `code`; the minimum age (18, 21, 25) for `birthday`."},"explanation":{"type":"string","nullable":true,"description":"Text shown to the visitor."}}},"imageUrl":{"type":"string","nullable":true,"description":"Thumbnail set in the editor, if any."}}},"LinkInBio":{"type":"object","properties":{"domain":{"type":"string","description":"","example":"example.com"},"url":{"type":"string","description":"Public address of the page: the domain itself, or `<domain-with-dashes>.l.ink` for the free forwarded plan.","example":"https:\/\/example.com"},"delivery":{"type":"string","description":"`domain`: served on the domain. `l.ink`: the domain forwards to the l.ink address (the free plan that comes with new .com registrations).","enum":["domain","l.ink"]},"plan":{"type":"object","properties":{"sku":{"type":"string","description":"PIXIELINKS2M1 \/ PIXIELINKS2M2 (free) or PIXIELINKSM1 \/ PIXIELINKSY1 (paid)."},"name":{"type":"string","description":""},"free":{"type":"boolean","description":""},"price":{"type":"integer","description":"Cents per interval."},"interval":{"type":"string","nullable":true,"description":"month or year; null when free."}}},"status":{"type":"string","description":"","example":"ACTIVE"},"isTrial":{"type":"boolean","description":""},"expireDate":{"type":"string","nullable":true,"description":"Paid plans: when the current term ends."},"features":{"type":"object","description":"What the plan allows. Only the free on-domain plan (PIXIELINKS2M1) turns any of these off.","properties":{"maxLinks":{"type":"integer","description":"50 on PIXIELINKS2M1, 1000 otherwise."},"scheduling":{"type":"boolean","description":""},"animation":{"type":"boolean","description":""},"passcodeProtection":{"type":"boolean","description":""},"analyticsIds":{"type":"boolean","description":""},"utmParameters":{"type":"boolean","description":""},"mailingList":{"type":"boolean","description":"Set up in the editor only."}}},"linkCount":{"type":"integer","description":""},"profile":{"type":"object","properties":{"title":{"type":"string","nullable":true,"description":""},"bio":{"type":"string","nullable":true,"description":""},"avatarUrl":{"type":"string","nullable":true,"description":""}}},"links":{"type":"array","items":{"$ref":"#\/components\/schemas\/LinkInBioLink"}},"socials":{"type":"object","description":"Social profile icons. Values are as entered: a handle or a full URL.","properties":{"location":{"type":"string","description":"","enum":["top","bottom"]},"email":{"type":"string","nullable":true,"description":""},"instagram":{"type":"string","nullable":true,"description":""},"facebook":{"type":"string","nullable":true,"description":""},"tiktok":{"type":"string","nullable":true,"description":""},"youtube":{"type":"string","nullable":true,"description":""},"twitter":{"type":"string","nullable":true,"description":""},"linkedin":{"type":"string","nullable":true,"description":""}}},"appearance":{"type":"object","description":"null means the editor default.","properties":{"preset":{"type":"string","nullable":true,"description":""},"fontFamily":{"type":"string","nullable":true,"description":""},"buttonStyle":{"type":"string","nullable":true,"description":""},"radius":{"type":"integer","nullable":true},"pageBackgroundMode":{"type":"string","nullable":true,"description":""},"textAlign":{"type":"string","nullable":true,"description":""},"pageBackgroundColor":{"type":"string","nullable":true,"description":"Hex colour."},"pageBackgroundColor2":{"type":"string","nullable":true,"description":"Hex colour."},"pageColor":{"type":"string","nullable":true,"description":"Hex colour."},"boxBackgroundColor":{"type":"string","nullable":true,"description":"Hex colour."},"borderColor":{"type":"string","nullable":true,"description":"Hex colour."}}},"settings":{"type":"object","properties":{"sensitiveContent":{"type":"string","nullable":true,"description":"`generic` warning or an age check (18, 21, 25) shown before the page."},"analytics":{"type":"object","properties":{"googleAnalyticsId":{"type":"string","nullable":true,"description":""},"facebookPixelId":{"type":"string","nullable":true,"description":""},"twitterPixelId":{"type":"string","nullable":true,"description":""},"tikTokPixelId":{"type":"string","nullable":true,"description":""},"utmSource":{"type":"string","nullable":true,"description":""},"utmMedium":{"type":"string","nullable":true,"description":""}}},"mailingList":{"type":"object","description":"Whether a signup form is set up (in the editor). Its settings are never returned.","properties":{"enabled":{"type":"boolean","description":""},"provider":{"type":"string","nullable":true,"description":""}}}}},"editorUrl":{"type":"string","description":"Where the account holder edits the page on porkbun.com."}}},"LinkInBioResponse":{"type":"object","properties":{"status":{"type":"string","description":"","example":"SUCCESS"},"linkInBio":{"$ref":"#\/components\/schemas\/LinkInBio"}}},"LinkInBioLinkResponse":{"type":"object","properties":{"status":{"type":"string","description":"","example":"SUCCESS"},"link":{"$ref":"#\/components\/schemas\/LinkInBioLink"},"linkInBio":{"$ref":"#\/components\/schemas\/LinkInBio"}}}},"parameters":{"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Optional. A unique string (e.g. UUID) to make this request safely retriable. Retries within 24 hours return the cached response. See the Idempotency section in the introduction for full semantics."},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"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.","example":"3f0c9a52-6d1e-4c8e-9b7a-2f5d1e0c4a11"}},"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your API key. Use together with X-Secret-API-Key."},"SecretApiKeyHeader":{"type":"apiKey","in":"header","name":"X-Secret-API-Key","description":"Your secret API key. Use together with X-API-Key."}},"headers":{"RetryAfter":{"description":"On 429: whole seconds to wait before retrying.","schema":{"type":"integer"}},"XRateLimitLimit":{"description":"Requests allowed per window for this API key (the general per-key limiter).","schema":{"type":"integer"}},"XRateLimitRemaining":{"description":"Requests left in the current window for this API key.","schema":{"type":"integer"}},"XRateLimitReset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"XRateLimitWindow":{"description":"Window length in seconds.","schema":{"type":"integer"}},"XRateLimitMode":{"description":"`observe` while the general per-key limiter only measures and refuses nothing. Endpoint-specific limits (availability checks, registrations, renewals and others) are enforced regardless, with 429 and Retry-After.","schema":{"type":"string","enum":["observe"]}}},"responses":{"RateLimited":{"description":"Rate limit exceeded (code RATE_LIMIT_EXCEEDED). Wait Retry-After seconds, then retry the same request.","headers":{"Retry-After":{"$ref":"#\/components\/headers\/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#\/components\/headers\/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#\/components\/headers\/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#\/components\/headers\/XRateLimitReset"},"X-RateLimit-Window":{"$ref":"#\/components\/headers\/XRateLimitWindow"},"X-RateLimit-Mode":{"$ref":"#\/components\/headers\/XRateLimitMode"}},"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/RateLimitExceeded"}}}}}},"security":[{"ApiKeyHeader":[],"SecretApiKeyHeader":[]}],"tags":[{"name":"Account","description":"Account credit balance, API spend controls, and onboarding"},{"name":"Domain","description":"Domain availability (single or up to 25 at once), registration, nameservers, URL forwarding, glue records, and auto-renewal"},{"name":"Porkbun DNS","description":"Create, read, update and delete DNS records in **Porkbun's** nameservers, including DNSSEC.\n\nThese endpoints manage the zone Porkbun serves. If a domain has been moved to a customer's own Cloudflare account (see the **Cloudflare** section), Cloudflare's nameservers answer for it instead \u2014 writes here still succeed and are kept in step in case the move is rolled back, but they do not change what resolves, and every response says so in `warnings`. Manage those domains with `\/cloudflare\/getRecords`, `\/cloudflare\/createRecord`, `\/cloudflare\/editRecord` and `\/cloudflare\/deleteRecord`.\n\nThe same is true, more bluntly, of a domain delegated to any other DNS provider: we hold a zone for every domain in the account, so a write always succeeds locally. Whenever the nameservers answering for the domain are not ours, the write response carries a `warnings` string saying so and naming them. Check `warnings` on every write; `GET \/dns\/preflight\/{domain}` reports the same thing (`nameservers-ours`) before you start."},{"name":"SSL","description":"Retrieve free SSL certificate bundles for domains registered at Porkbun"},{"name":"Hosting","description":"Two hosting products share these endpoints \u2014 pick one with the `sku` you pass to `\/hosting\/create`:\n\n\u2022 **Secure Static Hosting** \u2014 you upload the files; Porkbun serves them over HTTPS. SKUs `PIXIESECURESTATIC\u2026`\n\u2022 **Cloud for WordPress** \u2014 a managed WordPress site; you manage it through WordPress itself. SKUs `CLOUDWORDPRESS\u2026`\n\nShared by both: `\/hosting\/plans`, `\/hosting\/create`, `\/hosting\/get`, `\/hosting\/updateAutoRenew`, `\/hosting\/delete`.\nStatic hosting only: `\/hosting\/deploy`, `\/hosting\/files`, `\/hosting\/makeDir`, `\/hosting\/deleteFile` (they return `NOT_SUPPORTED_FOR_PRODUCT` on a WordPress site).\nWordPress only: `\/hosting\/createWpCredentials`, `\/hosting\/getWpCredentials`, `\/hosting\/deleteWpCredentials`.\n\nBoth products: the domain's first provision is a 15-day free trial that auto-renews at the plan price; a re-provision after deprovision is charged (one free trial per domain).\n\n`\/hosting\/plans` and `\/hosting\/create` also sell paid **Link in Bio** (SKUs `PIXIELINKS\u2026`); edit those pages with the `\/linkinbio` endpoints."},{"name":"Link in Bio","description":"A Link in Bio page: a single page of links, served on the domain (or, for the free plan that comes with .com registrations, forwarded to `<domain>.l.ink`). Edits made here show up immediately in the page and in the editor on porkbun.com.\n\n\u2022 **Turn one on:** free with `\/linkinbio\/create` (50 links), or paid with `\/hosting\/create` and sku `PIXIELINKSM1` ($3\/month) or `PIXIELINKSY1` ($30\/year) for 1000 links plus scheduling, animation, passcode protection and analytics IDs. Buying a paid plan over a free page keeps its content. Cancelling a paid plan (`\/hosting\/delete`) moves the page back to the free plan; nothing is deleted.\n\u2022 **Edit it:** `get` returns everything with link ids; then `updateProfile`, `addLink`, `editLink`, `deleteLink`, `reorderLinks`, `updateSocials`, `updateAppearance`, `updateSettings`. Reading needs the `read` permission, editing `hosting`.\n\nNot available with sandbox keys."},{"name":"Cloudflare","description":"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.\n\nTwo stages, and the first one needs a human:\n\n1. **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`.\n2. **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.\n\nQueueing 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."},{"name":"Email","description":"Email forwarding (forward an address at the domain to another mailbox) and email hosting (mailboxes: buy, set up, passwords)"},{"name":"Closeouts","description":"Expired domains that did not sell at auction, offered at a fixed price with no bidding \u2014 first buyer at the current price takes the name.\n\nThe price descends on a schedule, so a listing is often gone within minutes; treat a search result as perishable and re-check the price before buying.\n\nSearch returns `age` and `registrationDate` on every row and both are sortable, which is the part the website cannot do \u2014 it paginates by price tier and does not show registration date at all."},{"name":"Marketplace","description":"Browse domains listed for sale on Porkbun Marketplace (names Porkbun customers list themselves). To search every aftermarket at once (Afternic, Sedo, Atom, NameBright, DAX and Porkbun Marketplace), use `GET \/aftermarket\/search`."},{"name":"Aftermarket","description":"Registered domains that are for sale: partner aftermarkets (Afternic, Sedo, Atom, NameBright, DAX), Porkbun Marketplace and DAX auctions, in one search. Read-only; buying happens on porkbun.com through each listing's buyUrl."},{"name":"Pricing","description":"Domain pricing across all supported TLDs \u2014 no authentication required"},{"name":"API Key Management","description":"Request and retrieve API keys programmatically"},{"name":"Utility","description":"IP detection and credential verification"},{"name":"Webhooks","description":"Subscribe HTTPS endpoints to account events and Porkbun will `POST` a signed JSON payload to them as those events happen \u2014 no polling required. Manage endpoints with the **Webhooks** endpoints in this reference, the MCP tools (`create_webhook`, `list_webhooks`, \u2026), or the web UI at `porkbun.com\/account\/api`.\n\n**Event types:** `domain.registered`, `domain.renewed`, `domain.transfer.completed`, `domain.expiring` (fires at 60\/30\/5 days before expiry), `dns.record.created`, `dns.record.updated`, `dns.record.deleted`, `account.verification.code_sent`, `account.verification.completed`, `cloudflare.connect.completed`, `cloudflare.connect.failed` (a Cloudflare move reached a terminal state \u2014 `status` in the payload distinguishes `done` from `skipped`\/`failed`, so you can stop polling `\/cloudflare\/getQueue`). Subscribe to specific types, to a prefix wildcard like `dns.*`, or to `*` for everything (recommended \u2014 you'll receive new event types automatically). Call `GET \/webhook\/eventTypes` for the live catalog.\n\n**Payload envelope** \u2014 every delivery has the same outer shape:\n\n```json\n{\n  \"event\": \"domain.registered\",\n  \"id\": \"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a90\",\n  \"createdAt\": \"2026-06-17T18:30:00Z\",\n  \"data\": { \"domain\": \"example.com\", \"tld\": \"com\", \"expireDate\": \"2027-06-17 18:30:00\" }\n}\n```\n\n`id` is a UUIDv7 (time-ordered) and is also sent as the `X-Porkbun-Webhook-Id` header; use it to dedupe, since an endpoint may occasionally receive the same event more than once. `createdAt` is UTC (ISO 8601). The `data` object is event-specific; its fields are listed below.\n\n**What each event sends in `data`.** Dates inside `data` are UTC, formatted `YYYY-MM-DD HH:MM:SS`. Fields may be added over time, so ignore ones you don't recognise. No event carries contact details, auth codes, keys or other secrets.\n\n| Event | When it fires | `data` fields |\n|---|---|---|\n| `domain.registered` | A registration completes | `domain`, `tld`, `expireDate` |\n| `domain.renewed` | A renewal completes (manual, API or auto-renew) | `domain`, `tld`, `expireDate` (the new expiry) |\n| `domain.transfer.completed` | An inbound transfer finishes and the domain is in the account | `domain`, `tld` |\n| `domain.expiring` | 60, 30 and 5 days before expiry | `domain`, `tld`, `daysToExpire` (number: 60, 30 or 5), `expireDate` |\n| `dns.record.created` | A record is added (website, API, MCP, or a connected app) | `domain`, `name`, `id`, `type`, `content`, `ttl`, `prio` |\n| `dns.record.updated` | A record is changed | `domain`, `name`, `id`, `type`, `content`, `ttl`, `prio` (the values after the change) |\n| `dns.record.deleted` | A record is removed | `domain`, `name`, `id`, `type`, `content` (the values it had) |\n| `cloudflare.connect.completed` | A move to Cloudflare finished | `domain`, `status` (`done`), `message`, `cfAccountId`, `zoneId`, `previousNameservers`, `assignedNameservers`, `completedAt` |\n| `cloudflare.connect.failed` | A move to Cloudflare stopped without finishing | Same fields as completed; `status` is `failed` or `skipped`, `message` says why, `zoneId` may be `null` and the nameserver lists empty |\n| `account.verification.code_sent` | A verification code went to the account's email address or phone (from the website or the API) | `channel` (`email` or `phone`), `method` (`email`, `sms` or `call`), `sentTo` (masked), `expiresInMinutes` |\n| `account.verification.completed` | The account's email address or phone was verified (by link, website or API) | `channel`, `emailVerified`, `phoneVerified` (the account's state after this) |\n| `webhook.test` | You call `POST \/webhook\/test` | `endpointId` (number), `message` |\n\nField notes:\n\n- `domain` is the registered domain (`example.com`); `tld` is its extension without the dot (`com`).\n- DNS events: `name` is the full hostname (`www.example.com`, or `example.com` for the apex). `id`, `ttl` and `prio` are strings, the same as in `\/dns\/retrieve`, and `prio` is `null` for types that have none. Record changes made by Porkbun itself for certificate issuance (`_acme-challenge` names) are not sent.\n- Verification events never contain the code: it only proves the address or phone is real because it arrives there. `code_sent` tells an agent to ask the person for it; `completed` tells it to retry what failed with `VERIFICATION_REQUIRED` once both are `true`. Login codes are not sent.\n- Cloudflare events: `previousNameservers` and `assignedNameservers` are arrays of hostnames; `cfAccountId` and `zoneId` are Cloudflare's IDs.\n\nExamples:\n\n```json\n{\n  \"event\": \"domain.expiring\",\n  \"id\": \"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a91\",\n  \"createdAt\": \"2026-06-17T09:00:00Z\",\n  \"data\": { \"domain\": \"example.com\", \"tld\": \"com\", \"daysToExpire\": 30, \"expireDate\": \"2026-07-17 18:30:00\" }\n}\n```\n\n```json\n{\n  \"event\": \"dns.record.updated\",\n  \"id\": \"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a92\",\n  \"createdAt\": \"2026-06-17T18:31:05Z\",\n  \"data\": { \"domain\": \"example.com\", \"name\": \"www.example.com\", \"id\": \"123456789\", \"type\": \"A\", \"content\": \"192.0.2.1\", \"ttl\": \"600\", \"prio\": null }\n}\n```\n\n```json\n{\n  \"event\": \"cloudflare.connect.completed\",\n  \"id\": \"018f9c2a-7b3e-7c41-9b8a-2f1e6d4c5a93\",\n  \"createdAt\": \"2026-06-17T19:02:11Z\",\n  \"data\": {\n    \"domain\": \"example.com\", \"status\": \"done\", \"message\": \"Connected to Cloudflare.\",\n    \"cfAccountId\": \"0123456789abcdef0123456789abcdef\", \"zoneId\": \"fedcba9876543210fedcba9876543210\",\n    \"previousNameservers\": [\"curitiba.ns.porkbun.com\", \"fortaleza.ns.porkbun.com\"],\n    \"assignedNameservers\": [\"anna.ns.cloudflare.com\", \"lee.ns.cloudflare.com\"],\n    \"completedAt\": \"2026-06-17 19:02:10\"\n  }\n}\n```\n\nTo see any event's exact payload against your own handler, use a sandbox key and `POST \/sandbox\/triggerWebhook` with `{\"eventType\": \"...\", \"domain\": \"example.com\"}`; `GET \/webhook\/delivery\/{id}` shows the full payload of any past delivery.\n\n**Delivery headers:**\n\n- `X-Porkbun-Event` \u2014 the event type (e.g. `domain.renewed`).\n- `X-Porkbun-Webhook-Id` \u2014 the event UUID (matches `id` in the body).\n- `X-Porkbun-Webhook-Timestamp` \u2014 Unix seconds when the request was signed.\n- `X-Porkbun-Signature` \u2014 `sha256=` + the signature (see below).\n\n**Verify the signature.** The signature is `HMAC-SHA256(secret, \"{timestamp}.{rawBody}\")` where `secret` is the endpoint's signing secret, `{timestamp}` is the `X-Porkbun-Webhook-Timestamp` header value, and `{rawBody}` is the exact bytes of the request body (verify before parsing). Compare using a constant-time equality check, and reject timestamps that are too old (e.g. >5 minutes) to blunt replay attacks.\n\n```php\n$timestamp = $_SERVER['HTTP_X_PORKBUN_WEBHOOK_TIMESTAMP'];\n$signature = $_SERVER['HTTP_X_PORKBUN_SIGNATURE']; \/\/ \"sha256=...\"\n$body      = file_get_contents('php:\/\/input');\n$expected  = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $endpointSecret);\nif (!hash_equals($expected, $signature) || abs(time() - (int)$timestamp) > 300) {\n    http_response_code(400); exit;\n}\n\/\/ signature OK \u2014 now json_decode($body) and process\n```\n\n```javascript\nimport crypto from 'node:crypto';\nconst ts  = req.header('X-Porkbun-Webhook-Timestamp');\nconst sig = req.header('X-Porkbun-Signature');\nconst expected = 'sha256=' + crypto.createHmac('sha256', endpointSecret)\n  .update(`${ts}.${rawBody}`).digest('hex');\nconst ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))\n  && Math.abs(Date.now()\/1000 - Number(ts)) < 300;\n```\n\n**Delivery log & manual resend.** Every attempt is recorded \u2014 list recent deliveries with `GET \/webhook\/deliveries` (filter by `endpointId` or `status`, newest first) and fetch one with its full payload via `GET \/webhook\/delivery\/{id}`. History is retained ~30 days. To replay a delivery (e.g. after fixing a bug on your side), call `POST \/webhook\/resend` with the delivery `id`; it re-queues a fresh attempt reusing the **original event id**, so a consumer that dedupes on `X-Porkbun-Webhook-Id` treats it as the same event. The target endpoint must still exist and be ACTIVE.\n\n**Responding & retries.** Return any `2xx` status to acknowledge. Non-2xx responses, timeouts, or connection errors are retried with exponential backoff (~1m, 5m, 30m, 2h, 6h) up to 6 attempts. An endpoint that racks up 20 consecutive failures is automatically disabled and the account owner emailed; re-enable it (which resets the failure counter) via `POST \/webhook\/update` with `status: \"ACTIVE\"`. Use `POST \/webhook\/test` to send a `webhook.test` event and confirm your verifier works end-to-end."},{"name":"Sandbox","description":"Isolated sandbox\/test environment (sandbox API keys, pk1_sb_\u2026)."},{"name":"Mock","description":"Credential-free mock server returning schema-accurate example responses for any endpoint."}]}