# Porkbun API v3 — Domain

> Domain availability (single or up to 25 at once), registration, nameservers, URL forwarding, glue records, and auto-renewal

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

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

---

# Endpoints

## POST /api/json/v3/domain/checkDomain

**Check several domains at once**

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.

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

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

**Partial results are normal and must be read.** Three lists come back and they mean different things:
- `domains` - answered, keyed by domain name, each value identical in shape to the single-check `response`.
- `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.
- `unresolved` - the registry did not answer in time. These are **neither available nor taken**; treating them as unavailable is wrong. Retry them.

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

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

**Key permission needed:** `read`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | yes | Domains to check, e.g. ["example.com", "example.net"]. Duplicates are removed. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/checkDomain \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","domains":["example.com","example.net"]}'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `checked` | integer | How many domains were answered |
| `domains` | object | Keyed by domain name; each value has the same shape as the single-check `response` object |
| `unresolved` | string[] | The registry did not answer for these. NOT a statement of availability - retry them. |
| `invalid` | object[] | Entries that could not be checked, each with a reason |
| `limits` | object | Bulk budget usage; `countedIn` is "domains" |

## POST /api/json/v3/domain/checkDomain/{domain}

**Check domain availability**

Check if a domain is available for registration and retrieve current pricing. Includes registration, renewal, and transfer prices.

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

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

**Key permission needed:** `read`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/checkDomain/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields (CheckDomainResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `response` | object |  |
| `limits` | object | Current rate limit usage for this account |
| `ttlRemaining` | integer | Seconds remaining until the rate limit window resets |

## POST /api/json/v3/domain/create/{domain}

**Register a domain**

Register a domain, paid from account credit, directly in USDC, or by card. Requirements:
- Account email and phone must be verified
- Enough **prepaid credit** to cover the price, or `"payWith": "usdc"` / `"card"` to pay directly (see "Paying for it" below)
- `agreeToTerms` must be `'yes'` or `'1'`
- `cost` must equal the current price for the domain's minimum registration duration (in pennies)
- Premium domains cannot be registered via API, and a single order cannot exceed $100 (`ORDER_TOO_LARGE`)

## Paying for it

Three ways: from prepaid account credit (the default), directly in USDC, or directly by card with a hold.

**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.
- Read the balance with `GET /account/balance` (cents, plus a display string).
- 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.
- 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.
- Rehearse with `dryRun: true`, which reports `cost`, `balance`, `sufficientFunds` and, when short, `shortfall`.

A purchase attempted without enough credit returns `INSUFFICIENT_FUNDS` carrying `cost`, `balance`, `shortfall`, `topUpAvailable` and `usdcAmountToCover` in cents, and a message naming them.

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

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

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

Registrations are always for the registry-minimum duration (usually 1 year).

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

**Rate limits (both apply):**
- Attempt limit (default: 1 attempt per second per account)
- Success limit (default: 1000 successful registrations per 86400 seconds per account)

Both limits are configurable per API key and their current values are returned in the `limits` field of the response.

## Dry run

Add `dryRun: true` to validate everything and preview the cost WITHOUT registering or charging — 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:

```json
{
  "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"
}
``` Every registration includes a free email hosting trial mailbox, waiting to be set up with /email/setupMailbox (see /email/getMailboxes).

**Key permission needed:** `purchase`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `cost` | integer | yes | 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` | string | yes | Must be 'yes' or '1' to confirm agreement to the Domain Name Registration Agreement, Product Terms of Service, Privacy Policy, and automatic renewal terms. |
| `idnLanguage` | string | no | 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. |
| `whoisPrivacy` | boolean | no | Optional. Override WHOIS privacy for this registration. When omitted, the account-level default is used (set under Account Security Settings on porkbun.com/account — 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` | string | no | 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` | string | no | 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` | string | no | 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` | boolean | no | 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. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/create/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","cost":0,"agreeToTerms":"yes"}'
```

Response fields (CreateDomainResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string | The registered domain name |
| `cost` | integer | The total amount charged in pennies |
| `orderId` | integer | Internal Porkbun order ID |
| `limits` | object | Current rate limit state for both attempt and success limits |
| `balance` | integer | Remaining account credit balance in pennies after the charge |
| `ttlRemaining` | integer | Seconds until the success rate limit window resets |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

Response fields (DryRunPreviewResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `dryRun` | boolean | Always true on a dry-run preview — distinguishes it from a real success response. |
| `wouldSucceed` | boolean | True if the operation would complete given current funds and spend-limit state. (Hard validation failures — unavailable, bad price, ineligible — return a normal error response instead of a preview.) |
| `operation` | string |  |
| `domain` | string |  |
| `tld` | string |  |
| `available` | string | Availability as reported by the registry check (e.g. `available` / `unavailable`). |
| `premium` | boolean | Whether the domain is a premium/aftermarket name. |
| `duration` | integer | Term in years that would be purchased. |
| `cost` | integer | Total cost in pennies that would be charged. |
| `costDisplay` | string | Human-readable cost. |
| `balance` | integer | Current account credit balance in pennies. |
| `sufficientFunds` | boolean | Whether the balance covers the cost. |
| `monthlySpendLimit` | integer | The account's monthly API spend cap in pennies. Present only if a cap is configured. |
| `monthlySpendSoFar` | integer | API spend so far this calendar month in pennies. Present only if a cap is configured. |
| `withinMonthlySpendLimit` | boolean | Whether this cost stays within the monthly cap. Present only if a cap is configured. |
| `message` | string | Human-readable summary of the preview outcome. |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

## POST /api/json/v3/domain/renew/{domain}

**Renew a domain**

Renew a domain, paid from account credit, directly in USDC, or by card. Requirements:
- Domain must be in your account and active
- Domain must be opted in to API access
- Account email and phone must be verified
- Sufficient account credit, or `"payWith": "usdc"` / `"card"` to pay directly
- `cost` must equal the current renewal price for the domain's minimum renewal duration (in pennies)
- 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)
- Premium renewals are not currently supported via API

Renewals are always for the registry-minimum duration (usually 1 year). Use `/domain/checkDomain/{domain}` with `priceType=renewal` to get the current price before renewing.

**Rate limits (both apply):**
- Attempt limit (default: 1 attempt per second per account)
- Success limit (default: 1000 successful renewals per 86400 seconds per account)

Both limits are configurable per API key and their current values are returned in the `limits` field of the response.

## Dry run

Add `dryRun: true` to validate everything and preview the cost WITHOUT renewing or charging — nothing is created. Example response:

```json
{
  "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"
}
```

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

**Key permission needed:** `purchase`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `cost` | integer | yes | 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` | string | no | 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` | string | no | 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` | string | no | 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` | boolean | no | 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. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/renew/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","cost":0}'
```

Response fields (RenewDomainResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string | The renewed domain name |
| `expirationDate` | string | The new expiration date returned by the registry |
| `cost` | integer | The total amount charged in pennies |
| `orderId` | integer | Internal Porkbun order ID |
| `limits` | object | Current rate limit state for both attempt and success limits |
| `balance` | integer | Remaining account credit balance in pennies after the charge |
| `ttlRemaining` | integer | Seconds until the success rate limit window resets |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

Response fields (DryRunPreviewResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `dryRun` | boolean | Always true on a dry-run preview — distinguishes it from a real success response. |
| `wouldSucceed` | boolean | True if the operation would complete given current funds and spend-limit state. (Hard validation failures — unavailable, bad price, ineligible — return a normal error response instead of a preview.) |
| `operation` | string |  |
| `domain` | string |  |
| `tld` | string |  |
| `available` | string | Availability as reported by the registry check (e.g. `available` / `unavailable`). |
| `premium` | boolean | Whether the domain is a premium/aftermarket name. |
| `duration` | integer | Term in years that would be purchased. |
| `cost` | integer | Total cost in pennies that would be charged. |
| `costDisplay` | string | Human-readable cost. |
| `balance` | integer | Current account credit balance in pennies. |
| `sufficientFunds` | boolean | Whether the balance covers the cost. |
| `monthlySpendLimit` | integer | The account's monthly API spend cap in pennies. Present only if a cap is configured. |
| `monthlySpendSoFar` | integer | API spend so far this calendar month in pennies. Present only if a cap is configured. |
| `withinMonthlySpendLimit` | boolean | Whether this cost stays within the monthly cap. Present only if a cap is configured. |
| `message` | string | Human-readable summary of the preview outcome. |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

## POST /api/json/v3/domain/transfer/{domain}

**Initiate a domain transfer**

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–7 days to complete.

**Requirements:**
- Account email and phone must be verified.
- Sufficient account credit to cover the transfer cost, or `"payWith": "usdc"` / `"card"` to pay directly.
- Domain must not already be in your account.
- No other active transfer for the same domain.
- `.uk` and manage-only TLDs are not supported via API.
- Premium domain transfers are not supported via API.
- 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.

## Dry run

Add `dryRun: true` to validate everything and preview the cost WITHOUT initiating the transfer or charging — nothing is created. Example response:

```json
{
  "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"
}
```

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

**Key permission needed:** `purchase`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `authCode` | string | yes | The EPP auth code for the domain. |
| `cost` | integer | yes | The transfer cost in cents as returned by the pricing API. Must match exactly. |
| `holdForDnsSetup` | boolean | no | 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. |
| `payWith` | string | no | 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` | string | no | 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` | string | no | 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` | boolean | no | 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. |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/transfer/example.com' \
  -H 'Content-Type: application/json' \
  -d '{
    "apikey": "pk1_...",
    "secretapikey": "sk1_...",
    "authCode": "abc123",
    "cost": 899
  }'
```

Response fields (TransferDomainResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `orderId` | integer |  |
| `transferId` | integer |  |
| `message` | string |  |
| `balance` | integer | Remaining account credit balance in cents. |
| `ttlRemaining` | integer |  |
| `limits` | object |  |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

Response fields (DryRunPreviewResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `dryRun` | boolean | Always true on a dry-run preview — distinguishes it from a real success response. |
| `wouldSucceed` | boolean | True if the operation would complete given current funds and spend-limit state. (Hard validation failures — unavailable, bad price, ineligible — return a normal error response instead of a preview.) |
| `operation` | string |  |
| `domain` | string |  |
| `tld` | string |  |
| `available` | string | Availability as reported by the registry check (e.g. `available` / `unavailable`). |
| `premium` | boolean | Whether the domain is a premium/aftermarket name. |
| `duration` | integer | Term in years that would be purchased. |
| `cost` | integer | Total cost in pennies that would be charged. |
| `costDisplay` | string | Human-readable cost. |
| `balance` | integer | Current account credit balance in pennies. |
| `sufficientFunds` | boolean | Whether the balance covers the cost. |
| `monthlySpendLimit` | integer | The account's monthly API spend cap in pennies. Present only if a cap is configured. |
| `monthlySpendSoFar` | integer | API spend so far this calendar month in pennies. Present only if a cap is configured. |
| `withinMonthlySpendLimit` | boolean | Whether this cost stays within the monthly cap. Present only if a cap is configured. |
| `message` | string | Human-readable summary of the preview outcome. |
| `requestId` | string | Per-request UUID (also in the X-Request-Id header). Present on every API response. |

## GET /api/json/v3/domain/getTransferSetup/{domain}

**State of an in-flight inbound transfer**

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.

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes | Domain name. |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `transfer` | object |  |
| `held` | integer |  |
| `zoneReady` | integer |  |
| `recordCount` | integer |  |
| `delegation` | object |  |
| `dnssec` | integer |  |
| `nextStep` | object |  |
| `porkbunNameservers` | string[] |  |

## POST /api/json/v3/domain/prepareTransfer/{domain}

**Create the DNS zone for a held transfer**

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

**Key permission needed:** `domains`

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

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `zoneReady` | boolean |  |
| `recordCount` | integer |  |
| `message` | string |  |
| `porkbunNameservers` | string[] |  |

## POST /api/json/v3/domain/startTransfer/{domain}

**Release a held transfer to the registry**

Release a transfer held at `PENDINGDNS` into the registry pipeline. Nothing releases a held transfer on a timer — this call is the only thing that does.

Refuses 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`.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `force` | boolean | no | Release even though the zone is empty. Use only when the domain needs no DNS here. |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `message` | string |  |

## POST /api/json/v3/domain/cancelTransfer/{domain}

**Cancel an inbound transfer and refund it**

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.

The response reports `withdrawnAtRegistry`, `registryResultCode`, `refunded` and `refundAmount` so you can see exactly how far it got. Supports `dryRun`.

**Key permission needed:** `domains`

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

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `cancelled` | boolean |  |
| `previousStatus` | string |  |
| `transferStatus` | string |  |
| `withdrawnAtRegistry` | boolean |  |
| `registryResultCode` | string |  |
| `registryCode` | string |  |
| `refunded` | boolean |  |
| `refundAmount` | integer |  |
| `orderId` | integer |  |
| `message` | string |  |

## POST /api/json/v3/domain/updateTransferAuthCode/{domain}

**Replace the auth code on a stuck transfer**

Replace the authorization code on an inbound transfer that stalled because the code was wrong, and re-queue it — instead of cancelling, refunding and re-submitting.

The 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`.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `authCode` | string | yes | The replacement authorization code, exactly as the losing registrar issued it. Never interpolate this into a shell command. |

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | string |  |
| `authCodeValid` | boolean |  |
| `previousStatus` | string |  |
| `transferStatus` | string |  |
| `message` | string |  |

## GET /api/json/v3/domain/getTransfer/{domain}

**Get transfer status**

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

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Authorization` | header | no | Bearer token: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |
| `domain` | path | yes | The domain name (e.g. `example.com`). |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/getTransfer/example.com' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields (GetTransferResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `transfer` | object |  |

## GET /api/json/v3/domain/listTransfers

**List active transfers**

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

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Authorization` | header | no | Bearer token: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/listTransfers' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields (ListTransfersResponse):

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

## POST /api/json/v3/domain/listAll

**List all domains**

Retrieve domains in the authenticated account. Results are returned in chunks of up to 1000 domains. Use `start` to paginate.

**Filtering:** all filter parameters are optional. Combine them freely.

- `domain` — exact match (returns 0 or 1)
- `nameContains` — substring search
- `tlds` — limit to these TLDs
- `expiringWithinDays` — only domains expiring within N days
- `autoRenew` — `yes` / `no`
- `apiAccess` — `yes` / `no` (filter to domains the API key can operate on)
- `sortName` — `domain` / `tld` / `create_date` / `expire_date`
- `sortDirection` — `asc` / `desc`

Supports 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`.

**Key permission needed:** `read`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | integer | no | Zero-based offset for pagination (default: 0). Returns up to 1000 domains per call. |
| `includeLabels` | string | no | Return label metadata for each domain. Defaults to no. |
| `domain` | string | no | Exact domain name match. Returns 0 or 1 result. Useful as an alternative to `/domain/get/{domain}`. |
| `nameContains` | string | no | Substring match against the full domain name. Case-insensitive. |
| `expiringWithinDays` | integer | no | Filter to domains expiring within this many days (relative to now). |
| `tlds` | string[] | no | Limit to these TLDs (without leading dot). |
| `autoRenew` | string | no | Filter to domains with auto-renew on or off. |
| `apiAccess` | string | no | Filter to domains opted in to API access (yes) or not (no). Useful for finding domains an API key can actually operate on. |
| `sortName` | string | no | Field to sort by. Default: `expire_date` ascending. |
| `sortDirection` | string | no | Sort direction. Default: asc. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/listAll \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","start":0,"includeLabels":"yes"}'
```

Response fields (DomainListAllResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `count` | integer | Number of domains returned in this page. |
| `domains` | object[] |  |

## GET /api/json/v3/domain/listAll

**List all domains**

Retrieve domains in the authenticated account. Results are returned in chunks of up to 1000 domains. Use `start` to paginate.

**Filtering:** all filter parameters are optional. Combine them freely.

- `domain` — exact match (returns 0 or 1)
- `nameContains` — substring search
- `tlds` — limit to these TLDs
- `expiringWithinDays` — only domains expiring within N days
- `autoRenew` — `yes` / `no`
- `apiAccess` — `yes` / `no` (filter to domains the API key can operate on)
- `sortName` — `domain` / `tld` / `create_date` / `expire_date`
- `sortDirection` — `asc` / `desc`

Supports 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`.

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `Authorization` | header | no | Bearer token: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |
| `start` | query | no | Zero-based offset for pagination. Returns up to 1000 domains per call. |
| `includeLabels` | query | no | Return label metadata for each domain. Defaults to no. |
| `domain` | query | no | Exact domain name match. Returns 0 or 1 result. |
| `nameContains` | query | no | Substring match against the full domain name. Case-insensitive. |
| `expiringWithinDays` | query | no | Filter to domains expiring within this many days. |
| `tlds` | query | no | Limit to these TLDs (no leading dot). Use bracket form: `?tlds[]=com&tlds[]=io`. |
| `autoRenew` | query | no | Filter to domains with auto-renew on or off. |
| `apiAccess` | query | no | Filter to domains opted in to API access. |
| `sortName` | query | no | Field to sort by. Default: `expire_date` ascending. |
| `sortDirection` | query | no | Sort direction. Default: asc. |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/listAll?tlds[]=com&expiringWithinDays=30&autoRenew=no' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields (DomainListAllResponse):

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `count` | integer | Number of domains returned in this page. |
| `domains` | object[] |  |

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

**Get a single domain**

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.

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes | Fully qualified domain name in the authenticated account. |
| `Authorization` | header | no | Bearer token: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |
| `includeLabels` | query | no | Return label metadata. Defaults to no. |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/get/example.com' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `domain` | object |  |

## POST /api/json/v3/domain/updateAutoRenew/{domain}

**Update auto-renew setting**

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.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `status` | string | yes | Auto-renew status to set |
| `domains` | string[] | no | Array of additional domain names to update. Combined with the domain in the URL path if provided. |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/updateAutoRenew/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","status":"on","domains":[]}'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `results` | object | Object keyed by domain name |

## POST /api/json/v3/domain/getNs/{domain}

**Get nameservers**

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

**Key permission needed:** `read`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/getNs/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `ns` | string[] |  |

## GET /api/json/v3/domain/getNs/{domain}

**Get nameservers**

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

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |
| `Authorization` | header | no | Bearer token auth: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/getNs/{domain}' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `ns` | string[] |  |

## POST /api/json/v3/domain/updateNs/{domain}

**Update nameservers**

Update the nameservers for the domain at the registry. The list you send is applied as a set — 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`.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `ns` | string[] | yes | Ordered array of nameserver hostnames |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/updateNs/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","ns":["ns1.example.com","ns2.example.com"]}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/domain/getGlue/{domain}

**Get glue records**

Retrieve all glue records (host objects) registered under the domain. Supports both GET (with header auth) and POST (with body or header auth).

**Key permission needed:** `read`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/getGlue/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `hosts` | array[] | 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": []}]` |

## GET /api/json/v3/domain/getGlue/{domain}

**Get glue records**

Retrieve all glue records (host objects) registered under the domain. Supports both GET (with header auth) and POST (with body or header auth).

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |
| `Authorization` | header | no | Bearer token auth: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/getGlue/{domain}' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `hosts` | array[] | 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": []}]` |

## POST /api/json/v3/domain/createGlue/{domain}/{subdomain}

**Create glue record**

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

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `ips` | string[] | yes | Array of IP addresses (IPv4 and/or IPv6) to associate with the host record |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/createGlue/{domain}/{subdomain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","ips":["1.2.3.4","2001:db8::1"]}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/domain/updateGlue/{domain}/{subdomain}

**Update glue record**

Update the IP addresses of a glue record. All existing IP addresses are replaced with the supplied list.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `ips` | string[] | yes | Array of IP addresses (IPv4 and/or IPv6) to associate with the host record |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/updateGlue/{domain}/{subdomain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","ips":["1.2.3.4","2001:db8::1"]}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/domain/deleteGlue/{domain}/{subdomain}

**Delete glue record**

Delete a glue record (host object) for a subdomain.

**Key permission needed:** `domains`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/deleteGlue/{domain}/{subdomain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/domain/getUrlForwarding/{domain}

**List URL forwards**

Retrieve all active URL forwards for a domain. Supports both GET (with header auth) and POST (with body or header auth).

**Key permission needed:** `read`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/getUrlForwarding/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields (GetUrlForwardingResponse):

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

## GET /api/json/v3/domain/getUrlForwarding/{domain}

**List URL forwards**

Retrieve all active URL forwards for a domain. Supports both GET (with header auth) and POST (with body or header auth).

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `domain` | path | yes |  |
| `Authorization` | header | no | Bearer token auth: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/getUrlForwarding/{domain}' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields (GetUrlForwardingResponse):

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

## POST /api/json/v3/domain/addUrlForward/{domain}

**Add URL forward**

Add a URL forward for a domain or subdomain.

**Key permission needed:** `dns`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `subdomain` | string | no | Subdomain to forward (optional, leave blank or omit for root domain). Alphanumeric and hyphens only. |
| `location` | string | yes | Destination URL to forward to |
| `type` | string | yes | Redirect kind. 'permanent' = HTTP 301; 'temporary' = HTTP 302 (default); 'masked' = loads the destination in a frame (URL masking). For a precise code — including a 307 temporary redirect — use `redirectType`. |
| `redirectType` | string | no | 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). |
| `includePath` | string | yes | Whether to append the request URI path to the forwarding destination |
| `wildcard` | string | yes | Whether to also forward all subdomains of the forwarded subdomain |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/addUrlForward/{domain} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","subdomain":"www","location":"https:\/\/destination.example.com","type":"temporary","includePath":"yes","wildcard":"yes"}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## POST /api/json/v3/domain/deleteUrlForward/{domain}/{id}

**Delete URL forward**

Delete a specific URL forward by ID.

**Key permission needed:** `dns`

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

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/deleteUrlForward/{domain}/{id} \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_..."}'
```

Response fields (BasicResponse):

| Field | Type | Description |
|---|---|---|
| `warnings` | string[] | 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. |
| `status` | string |  |
| `message` | string | Human-readable message. Present on ERROR, sometimes on SUCCESS. |
| `code` | string | Machine-readable error code. Present when status is ERROR. |

## GET /api/json/v3/domain/getContacts/{domain}

**Get domain contacts**

Return the domain's four contacts (registrant, admin, tech, billing) with their current field values. Also available via POST.

**Key permission needed:** `read`

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

```bash
curl https://api.porkbun.com/api/json/v3/domain/getContacts/example.com \
  -H 'X-API-Key: pk1_...' -H 'X-Secret-API-Key: sk1_...'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `contacts` | object |  |

## POST /api/json/v3/domain/updateContacts/{domain}

**Update domain contacts**

Edit the domain's contacts. Send a `contacts` object keyed by role (`registrant`, `admin`, `tech`, `billing`) containing ANY subset of roles — unspecified roles keep their current values — 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`.

**`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 — .mx refuses `NEW JERSEY` and requires `Ciudad de México` 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` — 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.

**Address-validated TLDs.** On a **registrant** change for a TLD that requires a validated address (`.de`, `.nrw`, `.uk`/`.co.uk`/…, `.us`, `.ca`, `.nyc`, `.au`, `.eu`, `.in`/`.co.in`/…, `.nz`/`.co.nz`/…), 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 — stays blocked if a real correction was offered). 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.

**Key permission needed:** `domains`

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

Request body fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `contacts` | object | no | Any subset of roles to change; unspecified roles are left as-is. |
| `contact` |  | no | A single contact applied to all four roles. Use this OR `contacts`, not both. |
| `dryRun` | boolean | no | Validate and report wouldSucceed without applying the change. |
| `addressValidationChoice` | string | no | 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 (rejected if a real correction was offered). |

```bash
curl -X POST https://api.porkbun.com/api/json/v3/domain/updateContacts/example.com \
  -H 'Content-Type: application/json' \
  -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"}}}'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `message` | string |  |
| `registrantChanged` | boolean |  |

## GET /api/json/v3/domain/getRegistrationRequirements/{tld}

**Get TLD registration requirements (JSON Schema)**

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 — for TLDs with registry eligibility rules (e.g. .us nexus, .ca legal type) — 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).

**Key permission needed:** `read`

| Parameter | In | Required | Description |
|---|---|---|---|
| `tld` | path | yes | TLD without a leading dot, e.g. `com`, `us`, `ca`. |
| `Authorization` | header | no | Bearer token: `Authorization: Bearer <token>` |
| `X-API-Key` | header | no | API key header auth (use with X-Secret-API-Key) |
| `X-Secret-API-Key` | header | no | Secret API key header auth (use with X-API-Key) |

```bash
curl 'https://api.porkbun.com/api/json/v3/domain/getRegistrationRequirements/us' \
  -H 'X-API-Key: pk1_...' \
  -H 'X-Secret-API-Key: sk1_...'
```

Response fields:

| Field | Type | Description |
|---|---|---|
| `status` | string |  |
| `tld` | string |  |
| `apiRegisterable` | boolean | 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). |
| `registrationDurationYears` | integer | Fixed registration term the API uses for this TLD. |
| `maxRegistrationYears` | integer | Maximum years the registry allows, or null if unspecified. |
| `whoisPrivacySupported` | boolean |  |
| `requiresValidatedAddress` | boolean |  |
| `registrantOnly` | boolean | TLD uses only the registrant contact (no admin/tech/billing). |
| `requestSchema` | object | JSON Schema (Draft 2020-12) for the /domain/create request body this TLD accepts (cost, agreeToTerms, whoisPrivacy, credentials), including the fixed registration term. |
| `registryRequirements` | object | 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. |
| `notApiRegisterableReason` | string | Present only when apiRegisterable is false. |

---

## More

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