Porkbun + Claude Code Integration Guide
██████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗
██╔══██╗██╔═══██╗██╔══██╗██║ ██╔╝██╔══██╗██║ ██║████╗ ██║
██████╔╝██║ ██║██████╔╝█████╔╝ ██████╔╝██║ ██║██╔██╗ ██║
██╔═══╝ ██║ ██║██╔══██╗██╔═██╗ ██╔══██╗██║ ██║██║╚██╗██║
██║ ╚██████╔╝██║ ██║██║ ██╗██████╔╝╚██████╔╝██║ ╚████║
╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝Porkbun + Claude Code Integration Guide
Focus: Automating domain registration, DNS management, and SSL certificate workflows from Claude Code using the Porkbun REST API.
Overview
Porkbun is a domain registrar known for competitive pricing and a clean REST API. While there is no official MCP server or CLI for Porkbun, Claude Code can fully automate domain management — DNS record creation, domain registration, SSL retrieval, URL forwarding — through direct REST API calls from Bash or Node.js scripts. This guide shows how to wire Porkbun into your Claude Code automation workflow.
Here is the big picture — once you see how the pieces connect, the rest of this guide is just filling in the details:
Official Documentation
| Resource | URL |
|---|---|
| Porkbun API Docs | https://porkbun.com/api/json/v3/documentation |
| API Overview | https://porkbun.com/api/json/v3 |
| Control Panel | https://porkbun.com/account/domainsSpeedy |
API Setup
Get Your API Keys
- Log in at porkbun.com
- Go to Account → API Access
- Enable API access and generate your API key and Secret API key
Test Your Credentials
curl -s https://api.porkbun.com/api/json/v3/ping \
-X POST \
-H "Content-Type: application/json" \
-d '{
"apikey": "'$PORKBUN_API_KEY'",
"secretapikey": "'$PORKBUN_SECRET_API_KEY'"
}' | jq .Expected response:
{ "status": "SUCCESS", "yourIp": "1.2.3.4" }API Integration
TypeScript Client Helper
// lib/porkbun.ts
const PORKBUN_BASE = "https://api.porkbun.com/api/json/v3";
const auth = {
apikey: process.env.PORKBUN_API_KEY!,
secretapikey: process.env.PORKBUN_SECRET_API_KEY!,
};
async function porkbun<T>(path: string, body: object = {}): Promise<T> {
const res = await fetch(`${PORKBUN_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...auth, ...body }),
});
const data = await res.json();
if (data.status !== "SUCCESS") throw new Error(`Porkbun API error: ${data.message}`);
return data;
}
// List all domains
export const listDomains = () => porkbun<{ domains: any[] }>("/domain/listAll");
// Get DNS records for a domain
export const getDnsRecords = (domain: string) =>
porkbun<{ records: any[] }>(`/dns/retrieve/${domain}`);
// Create a DNS record
export const createDnsRecord = (
domain: string,
type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS",
name: string,
content: string,
ttl = "600"
) =>
porkbun(`/dns/create/${domain}`, { type, name, content, ttl });
// Delete a DNS record
export const deleteDnsRecord = (domain: string, recordId: string) =>
porkbun(`/dns/delete/${domain}/${recordId}`);
// Edit a DNS record
export const editDnsRecord = (
domain: string,
recordId: string,
type: string,
name: string,
content: string
) => porkbun(`/dns/edit/${domain}/${recordId}`, { type, name, content });Python Client Helper
import os, requests
PORKBUN_BASE = "https://api.porkbun.com/api/json/v3"
AUTH = {
"apikey": os.environ["PORKBUN_API_KEY"],
"secretapikey": os.environ["PORKBUN_SECRET_API_KEY"],
}
def porkbun(path: str, **kwargs) -> dict:
res = requests.post(
f"{PORKBUN_BASE}{path}",
json={**AUTH, **kwargs},
)
data = res.json()
if data.get("status") != "SUCCESS":
raise Exception(f"Porkbun API error: {data.get('message')}")
return data
# List domains
domains = porkbun("/domain/listAll")["domains"]
# Get DNS records
records = porkbun(f"/dns/retrieve/example.com")["records"]
# Create a DNS record
porkbun(
"/dns/create/example.com",
type="A",
name="api",
content="1.2.3.4",
ttl="600",
)
# Create a TXT record (for domain verification)
porkbun(
"/dns/create/example.com",
type="TXT",
name="", # root domain
content="v=spf1 include:mailgun.org ~all",
ttl="300",
)Common API Endpoints
BASE="https://api.porkbun.com/api/json/v3"
AUTH='{"apikey":"'$PORKBUN_API_KEY'","secretapikey":"'$PORKBUN_SECRET_API_KEY'"}'
# Ping / test connection
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" "$BASE/ping" | jq .status
# List all domains
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" "$BASE/domain/listAll" | jq '.domains[].domain'
# Get DNS records for a domain
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" "$BASE/dns/retrieve/example.com" | jq '.records[] | {id,type,name,content}'
# Create DNS A record
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"type":"A","name":"subdomain","content":"1.2.3.4","ttl":"600"}' \
"$BASE/dns/create/example.com" | jq .
# Check domain availability
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"domain":"my-new-domain.com"}' \
"$BASE/domain/checkDomain/my-new-domain.com" | jq .
# Get SSL certificate bundle
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH" "$BASE/ssl/retrieve/example.com" | jq '{certificatechain,privatekey}'
# Set URL forwarding
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"subdomain":"www","location":"https://example.com","type":"temporary","includePath":"yes"}' \
"$BASE/domain/addUrlForward/example.com" | jq .Domain pricing & lifecycle via API
The Overview above mentions "domain registration" — but here's the important reality check, verified against the official docs (June 2026): the Porkbun API does not expose an endpoint to register or purchase a new domain. The official knowledge base describes the API's scope as DNS manipulation, SSL bundle download, nameserver control, and price lookup only — registration and checkout happen in the dashboard, not over the API.
Registration is dashboard-only. There is no official
domain/register/domain/purchaseendpoint. Unofficial OpenAPI mirrors sometimes list adomain/createpath ("register using account credit"), but it is not in Porkbun's documented, supported surface — don't build a purchase flow on it. The API-supported lifecycle is: check pricing/availability → register in the dashboard → then manage nameservers, glue, DNSSEC, and DNS programmatically.
What you can do via the API, with copy-paste examples:
Get the full price list
pricing/get returns registration, renewal, and transfer prices for every TLD (no auth required for the public list, but sending auth is harmless):
BASE="https://api.porkbun.com/api/json/v3"
# Pricing for ALL TLDs
curl -s -X POST -H "Content-Type: application/json" -d '{}' \
"$BASE/pricing/get" | jq '.pricing.com, .pricing.org, .pricing.dev'{ "registration": "11.06", "renewal": "11.06", "transfer": "11.06" }Check availability + price for one name
checkDomain/{domain} returns whether a specific name is available and its price (note: this endpoint is rate-limited — see the gotcha):
AUTH='{"apikey":"'$PORKBUN_API_KEY'","secretapikey":"'$PORKBUN_SECRET_API_KEY'"}'
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" \
"$BASE/domain/checkDomain/codeamanilabs.io" | jq '.response'List your registered domains (with filters)
domain/listAll paginates 1000 at a time via the start offset:
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"start":"0","includeLabels":"yes"}' \
"$BASE/domain/listAll" | jq '.domains[] | {domain, status, expireDate, autoRenew}'Get & update nameservers
This is the lever you use after registering a domain in the dashboard but wanting to point it at an external DNS provider (Cloudflare, Vercel, etc.):
# Get current authoritative nameservers
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" \
"$BASE/domain/getNs/codeamanilabs.org" | jq '.ns'
# Replace nameservers (the ns array fully overwrites — list ALL of them)
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"ns":["maceio.ns.porkbun.com","fortaleza.ns.porkbun.com"]}' \
"$BASE/domain/updateNs/codeamanilabs.org" | jq '.status'Glue records (vanity / child nameservers)
Needed only if you run your own nameservers on a subdomain of the registered domain:
# List existing glue records
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" \
"$BASE/domain/getGlue/example.com" | jq .
# Create glue: ns1.example.com -> IPs (v4 and/or v6)
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"ips":["1.2.3.4","2606:4700::1"]}' \
"$BASE/domain/createGlue/example.com/ns1" | jq '.status'
# updateGlue/{domain}/{subdomain} and deleteGlue/{domain}/{subdomain} mirror this shapeDNSSEC records
DNSSEC DS records live under the /dns/ namespace, not /domain/:
# Get DNSSEC records
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" \
"$BASE/dns/getDnssecRecords/example.com" | jq .
# Create a DS record
curl -s -X POST -H "Content-Type: application/json" \
-d "$AUTH"' + {"keyTag":"64087","alg":"13","digestType":"2","digest":"<hash>"}' \
"$BASE/dns/createDnssecRecord/example.com" | jq '.status'
# Delete by key tag
curl -s -X POST -H "Content-Type: application/json" -d "$AUTH" \
"$BASE/dns/deleteDnssecRecord/example.com/64087" | jq '.status'Gotcha — checkDomain is aggressively rate-limited. Unlike DNS endpoints, domain/checkDomain hits Porkbun's upstream registry and is throttled hard (roughly one call every few seconds; bursts return a rate-limit error, not availability data). Don't loop it over a wordlist to brainstorm names — you'll get throttled and your script will misread the error as "unavailable." For bulk price comparisons use pricing/get once (it's the whole TLD table in a single call) and only call checkDomain for the handful of finalists.
Environment Variables
# Required
PORKBUN_API_KEY=pk1_... # From porkbun.com/account (API key)
PORKBUN_SECRET_API_KEY=sk1_... # From porkbun.com/account (Secret API key)Store in .env and never commit to version control. Add to .gitignore:
.env
.env.local
*.envAutomation Workflows
You've got the API down — now let automation handle the repetitive parts. Because Porkbun doesn't upsert, an update is always a retrieve-then-replace dance, and this is exactly the flow the /dns slash command and the GitHub Action below follow:
Claude Code Slash Command: Update DNS
.claude/commands/dns.md:
Update the DNS record for $ARGUMENTS.
Parse $ARGUMENTS as: "subdomain.domain.com TYPE value" (e.g., "api.example.com A 1.2.3.4")
Use Bash to call the Porkbun API:
1. First retrieve existing records to check if the record exists
2. If it exists, delete the old record, then create a new one
3. If it doesn't exist, create it directly
4. Verify the update by retrieving records again and confirming the new value
Report: what was changed, the record ID, and the new value.Usage: /project:dns api.example.com A 1.2.3.4
Hook: Verify Domain After Deploy
.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node scripts/verify-dns.js"
}
]
}
]
}
}scripts/verify-dns.js:
const domain = process.env.DOMAIN_NAME;
if (!domain) process.exit(0);
const response = await fetch(`https://api.porkbun.com/api/json/v3/dns/retrieve/${domain}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apikey: process.env.PORKBUN_API_KEY,
secretapikey: process.env.PORKBUN_SECRET_API_KEY,
}),
});
const data = await response.json();
if (data.status === "SUCCESS") {
console.log(`DNS verified — ${data.records.length} records for ${domain}`);
} else {
console.error("DNS verification failed:", data.message);
}GitHub Actions: Auto-update DNS on Vercel Deploy
# .github/workflows/update-dns.yml
name: Update Porkbun DNS
on:
workflow_dispatch:
inputs:
subdomain:
description: Subdomain to update
required: true
ip:
description: New IP address
required: true
jobs:
update-dns:
runs-on: ubuntu-latest
steps:
- name: Update DNS A record
env:
PORKBUN_API_KEY: ${{ secrets.PORKBUN_API_KEY }}
PORKBUN_SECRET_API_KEY: ${{ secrets.PORKBUN_SECRET_API_KEY }}
run: |
# Get existing record ID
records=$(curl -s -X POST https://api.porkbun.com/api/json/v3/dns/retrieve/${{ secrets.DOMAIN_NAME }} \
-H "Content-Type: application/json" \
-d "{\"apikey\":\"$PORKBUN_API_KEY\",\"secretapikey\":\"$PORKBUN_SECRET_API_KEY\"}")
record_id=$(echo $records | jq -r ".records[] | select(.name==\"${{ inputs.subdomain }}\") | .id")
if [ -n "$record_id" ]; then
# Delete existing
curl -s -X POST "https://api.porkbun.com/api/json/v3/dns/delete/${{ secrets.DOMAIN_NAME }}/$record_id" \
-H "Content-Type: application/json" \
-d "{\"apikey\":\"$PORKBUN_API_KEY\",\"secretapikey\":\"$PORKBUN_SECRET_API_KEY\"}"
fi
# Create new
curl -s -X POST "https://api.porkbun.com/api/json/v3/dns/create/${{ secrets.DOMAIN_NAME }}" \
-H "Content-Type: application/json" \
-d "{\"apikey\":\"$PORKBUN_API_KEY\",\"secretapikey\":\"$PORKBUN_SECRET_API_KEY\",\"type\":\"A\",\"name\":\"${{ inputs.subdomain }}\",\"content\":\"${{ inputs.ip }}\",\"ttl\":\"300\"}"Common Use Cases
| Use Case | Approach |
|---|---|
| Point subdomain to new IP | Create/update A record via API |
| Domain verification (TXT) | Create TXT record for email/GSC verification |
| SSL certificates | GET /ssl/retrieve/{domain} for cert bundle |
| URL forwarding | /domain/addUrlForward/{domain} |
| Check availability | /domain/checkDomain/{domain} |
| DKIM/SPF for email | Create TXT records via API |
Troubleshooting
| Issue | Fix |
|---|---|
API key invalid | Regenerate keys; ensure no trailing spaces when copying |
Domain not found | Domain must be in your Porkbun account |
Record already exists | Delete existing record before creating (Porkbun doesn't upsert) |
| SSL retrieve fails | SSL must be active (domain must have HTTPS certificate provisioned) |
| API access denied | Enable API access in account settings at porkbun.com/account |