Skip to content
All skills
COOK ops v1.0.0 · Apache-2.0

Hostinger Dns Api

Read and edit DNS records on Hostinger-registered domains via the Hostinger Developer API. Use when a domain's nameservers are ns1/ns2.dns-parking.com (Hostinger default), when registrar lookup via RDAP returns "HOSTINGER operations, UAB", or whenever you need to script DNS changes without logging into hpanel. Validated 2026-04-24 on agentpact.xyz (CNAME swap to update Railway custom-domain target).

Audited
Source
SHA-256
Last reviewed
How we audit →

Install in your agent

Tell your agent: "install the recipes skill, then add hostinger-dns-api"
Or via curl: curl -sL https://recipes.wisechef.ai/skill -o ~/.claude/skills/recipes/SKILL.md

Full skill source · SKILL.md

hostinger-dns-api

Hostinger's Developer API (developers.hostinger.com) exposes domain portfolio + DNS zone management with a Bearer token. The shape of the API has two non-obvious gotchas — the GET response shape does NOT match the PUT request shape, and CNAME values must include a trailing dot — that cost a full diagnostic loop the first time you hit them.

When to use

  • You need to add/change/remove a DNS record on a Hostinger-registered domain
  • The user has provided a Hostinger API token, OR you've extracted one from Bitwarden
  • A domain's RDAP lookup shows registrar = "HOSTINGER operations, UAB" (NOT Namecheap, NOT Cloudflare)
  • Custom-domain provisioning on Railway/Vercel/Netlify needs a CNAME update at the registrar
  • You want to avoid the hpanel.hostinger.com web UI for any reason (automation, agent loop, scripting)

Sanity check first — confirm registrar

Don't assume. If dig +short NS domain.tld returns ns1.dns-parking.com / ns2.dns-parking.com, the domain is parked on Hostinger DNS but the registrar still might not be Hostinger. Verify via RDAP:

curl -sS -H "Accept: application/json" "https://rdap.centralnic.com/xyz/domain/example.xyz" \
  | jq -r '.entities[] | select(.roles[]=="registrar") | .vcardArray[1][] | select(.[0]=="fn") | .[3]'

For .com / .net / .org / etc., swap the rdap host. The IANA bootstrap registry at https://data.iana.org/rdap/dns.json lists the right RDAP endpoint per TLD. Only proceed with this skill if the registrar is Hostinger.

API authentication

Token comes from hpanel → Account → API or Bitwarden. Use Authorization: Bearer <token> for every request. The token gives access to ALL domains in the user's portfolio, not a single one — handle accordingly.

TOK='Hostinger-API-Token'

# List domains visible to this token
curl -sS -H "Authorization: Bearer $TOK" -H "Accept: application/json" \
  "https://developers.hostinger.com/api/domains/v1/portfolio" \
  | jq -r '.[] | "\(.domain)\t\(.status)\t\(.type)"'

Read DNS zone

curl -sS -H "Authorization: Bearer $TOK" -H "Accept: application/json" \
  "https://developers.hostinger.com/api/dns/v1/zones/agentpact.xyz" \
  | jq .

Returns a bare JSON array of records, each shaped:

{
  "name": "www",
  "type": "CNAME",
  "ttl": 300,
  "records": [
    { "content": "agentpactweb-production.up.railway.app.", "is_disabled": false }
  ]
}

Key fields:

  • name is the host label (@ for apex, www, mcp, etc.) — NOT a fully qualified name
  • type is the DNS record type (A, AAAA, CNAME, ALIAS, MX, TXT, NS, etc.) — Hostinger supports ALIAS at apex (CNAME-flattening)
  • records is a list of values; CNAME has one entry, MX/TXT can have several
  • content for CNAME/ALIAS MUST end with a literal dot (...up.railway.app.) — without it, the API may accept the write but DNS resolution will misbehave
  • is_disabled: false — set to true to soft-disable a record without deleting

Write DNS zone — the gotcha

PUT shape is NOT the same as GET shape. PUT requires the records array wrapped under a zone key:

# Pull current zone
curl -sS -H "Authorization: Bearer $TOK" -H "Accept: application/json" \
  "https://developers.hostinger.com/api/dns/v1/zones/agentpact.xyz" > /tmp/zone.json

# Edit /tmp/zone.json in Python — find the record, change record content, write the modified zone
python3 - <<'PY'
import json
zone = json.load(open('/tmp/zone.json'))
for entry in zone:
    if entry['name'] == 'www' and entry['type'] == 'CNAME':
        entry['records'] = [{'content': 'ypb6kcbs.up.railway.app.', 'is_disabled': False}]
        entry['ttl'] = 300
# PUT requires {"zone": [...]} wrapper, NOT a bare array
json.dump({"zone": zone}, open('/tmp/zone-payload.json', 'w'))
PY

# Send PUT
curl -sS -X PUT -H "Authorization: Bearer $TOK" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  --data @/tmp/zone-payload.json \
  "https://developers.hostinger.com/api/dns/v1/zones/agentpact.xyz"
# Expected: {"message":"Request accepted"}

If you forget the wrapper and PUT a bare array, the response is {"message":"The zone field is required.","errors":{"zone":["The zone field is required."]}} — that's the giveaway.

Verify the change took

# Re-pull and confirm
curl -sS -H "Authorization: Bearer $TOK" -H "Accept: application/json" \
  "https://developers.hostinger.com/api/dns/v1/zones/agentpact.xyz" \
  | jq -r '.[] | "\(.type)\t\(.name)\t\(.records[0].content)"'

Then verify external DNS propagation against a public resolver (TTL-respecting):

dig +short www.agentpact.xyz @1.1.1.1
dig +short www.agentpact.xyz @8.8.8.8

Hostinger publishes within seconds. Resolver propagation depends on TTL — keep TTL low (300s) when planning further iteration.

Pitfalls

  1. GET ≠ PUT shape. GET returns [<record>, ...]. PUT requires {"zone": [<record>, ...]}. Always wrap.

  2. Trailing dot required on CNAME/ALIAS values. 2x68toyo.up.railway.app (no dot) and 2x68toyo.up.railway.app. (with dot) are different to Hostinger's parser. The . form always works; the bare form sometimes doesn't, and the failure mode is silent — the record looks set in the API but DNS resolution acts as if it's missing.

  3. @ is the apex. Use name: "@" for the root domain, not agentpact.xyz and not empty string.

  4. PUT replaces the entire zone, not just one record. Always pull, mutate, push back. If you push only the changed record, every other record disappears. (Discovered the hard way is a permanent education — don't.)

  5. is_disabled is a soft-delete, not a hard remove. To remove a record permanently, drop the entry from the zone array entirely before PUT.

  6. ALIAS records survive at apex. Hostinger supports ALIAS (CNAME-flattening) on @, which most other registrars don't. If you have an ALIAS @ -> something.up.railway.app. already configured, leave it — CNAMEs at apex would violate RFC 1034 and Hostinger's API will reject them. Use ALIAS for apex, CNAME for sub-labels.

  7. Mass changes need rate-limit awareness. The API is generous but not unlimited. If iterating across many domains, sleep 1-2s between PUTs and respect any 429 responses.

Companion skills

  • railway-serverless-diagnose — when the reason for a Hostinger DNS edit is a Railway custom-domain CNAME target change. Read pitfall #4 there: delete+recreate on Railway issues a NEW per-domain CNAME each time, requiring a fresh DNS update.
  • cloudflare-tunnel-wiring — when the domain WAS at Cloudflare and you're moving it OFF, or when comparing CF DNS workflow to Hostinger.
  • bitwarden-key-extraction — when the Hostinger token is in BW under a confusingly-named item. As of 2026-04-24, no dedicated Hostinger BW item existed for Adam — the token was passed in-chat. Recommend creating one.

Verification — definition of done

  1. Verified registrar is actually Hostinger (RDAP, not just nameservers)
  2. Pulled current zone, confirmed expected records exist with expected values
  3. PUT-ed the modified zone with {"zone": [...]} wrapper and trailing-dot CNAME content
  4. Got {"message":"Request accepted"} response
  5. Re-pulled and saw the change
  6. Confirmed dig +short against a public resolver returns the new value (allow up to 5 min for propagation, but typically 30-60s)