← Back to dashboard
porkbundomainsfresh

Porkbun + Claude Code Integration Guide

Text
██████╗  ██████╗ ██████╗ ██╗  ██╗██████╗ ██╗   ██╗███╗   ██╗
██╔══██╗██╔═══██╗██╔══██╗██║ ██╔╝██╔══██╗██║   ██║████╗  ██║
██████╔╝██║   ██║██████╔╝█████╔╝ ██████╔╝██║   ██║██╔██╗ ██║
██╔═══╝ ██║   ██║██╔══██╗██╔═██╗ ██╔══██╗██║   ██║██║╚██╗██║
██║     ╚██████╔╝██║  ██║██║  ██╗██████╔╝╚██████╔╝██║ ╚████║
╚═╝      ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝  ╚═════╝ ╚═╝  ╚═══╝

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


API Setup

Get Your API Keys

  1. Log in at porkbun.com
  2. Go to AccountAPI Access
  3. Enable API access and generate your API key and Secret API key

Test Your Credentials

Bash
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:

JSON
{ "status": "SUCCESS", "yourIp": "1.2.3.4" }

API Integration

TypeScript Client Helper

TypeScript
// 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

Python
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

Bash
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/purchase endpoint. Unofficial OpenAPI mirrors sometimes list a domain/create path ("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):

Bash
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'
JSON
{ "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):

Bash
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:

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

Bash
# 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:

Bash
# 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 shape

DNSSEC records

DNSSEC DS records live under the /dns/ namespace, not /domain/:

Bash
# 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

Bash
# 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:

Text
.env
.env.local
*.env

Automation 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:

Markdown
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:

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/verify-dns.js"
          }
        ]
      }
    ]
  }
}

scripts/verify-dns.js:

JavaScript
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

YAML
# .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 CaseApproach
Point subdomain to new IPCreate/update A record via API
Domain verification (TXT)Create TXT record for email/GSC verification
SSL certificatesGET /ssl/retrieve/{domain} for cert bundle
URL forwarding/domain/addUrlForward/{domain}
Check availability/domain/checkDomain/{domain}
DKIM/SPF for emailCreate TXT records via API

Troubleshooting

IssueFix
API key invalidRegenerate keys; ensure no trailing spaces when copying
Domain not foundDomain must be in your Porkbun account
Record already existsDelete existing record before creating (Porkbun doesn't upsert)
SSL retrieve failsSSL must be active (domain must have HTTPS certificate provisioned)
API access deniedEnable API access in account settings at porkbun.com/account