API reference (machine endpoints)

Auth is always an API token (nhtdns_ prefix), HTTP Basic password slot or a Bearer header. Machine endpoints never read browser sessions and need no CSRF. Tokens are created in the panel and come in three tiers by where they will live; each endpoint group accepts only its own tier — no crossover.

Picking a token: decide by where it will be stored.

DDNS token For routers / NAS. Bound to one hostname, only valid on /nic/update; a leak loses at most that one record
ACME token For cert-renewal servers. All domains, but only _acme-challenge TXT plus managed-cert download
Manage token Only for servers / scripts you control. Add/delete domains plus full record CRUD and import/export across all your domains; optional expiry

Device endpoints (DDNS / ACME tokens)

GET /nic/update DynDNS2 update. Responds with text/plain protocol literals: good (updated) / nochg (no change) / nohost (hostname not owned by token) / badauth (auth failed) / abuse (rate limited) / 911 (server error)
POST /api/dns/acme/present body {"fqdn":"_acme-challenge.example.com.","value":"..."} → {"code":200}; fqdn must fall inside the token's authorized zone and its first label must be _acme-challenge
POST /api/dns/acme/cleanup Same shape as present; removes the matching TXT value
GET /api/dns/certs/current Fetch the managed certificate (ACME token, query domain=<fqdn>). Pull daily from cron and reload when the content changes
Rate limits Per token 10/min and 300/h; per source IP 30/min and 600/h. Minute-level DDNS polling fits comfortably
Error shapes ACME endpoints: 400 = body missing fqdn/value; 401 = invalid token; 403 = out of scope (wrong scope / zone suspended). DDNS endpoint errors always use the protocol literals above with HTTP status fixed at 200

Management API (/api/dns/v1, manage token)

GET /api/dns/v1/zones Zone list (with ids and public_ns). Scripts use it to resolve a domain to its zone id first; each zone carries ns (the nameservers to set at the registrar for that domain) and brand_ns (whether brand NS is on) — public_ns is only the platform default
POST /api/dns/v1/zones body {"zone_name":"example.com"}. Add a domain through the same admission pipeline as the panel (normalization / hold checks / quotas); the response includes public_ns — point your registrar's NS at them next; the zone in the response also carries ns and brand_ns
DELETE /api/dns/v1/zones/:id Delete a domain (with all its records). If DNSSEC is enabled the body must carry {"confirm_dnssec":true} — guards against deleting while the DS is still at the registry, which would blackhole the domain; if the domain is a brand NS root that other domains still use, the body must carry {"confirm_brand_ns":true}, otherwise the API returns 409 with the number of affected domains (affected)
GET /api/dns/v1/zones/:id/records List records
POST /api/dns/v1/zones/:id/records body {"id":0,"name":"www","type":"A","value":"192.0.2.10","ttl":300,"line":"default"}; id absent or 0 = create, >0 = update that record (when updating, omitted ttl / line / enabled keep their current values). Validation shares the panel's pipeline (record types / CNAME exclusivity / quotas / lines). Success returns {"code":200,"data":{"record":{"id":…,"name":…,"type":…,"value":…,"ttl":…,"line":…,"enabled":…}}} — the record id is data.record.id
DELETE /api/dns/v1/zones/:id/records/:rid Delete a record
GET /api/dns/v1/zones/:id/export Export the BIND zone file (text/plain) — your data is always one request away; non-default routes and disabled records are kept as comment lines
POST /api/dns/v1/zones/:id/import body {"text":"..."}(BIND text, 1MB max). Each entry runs the validation pipeline; a per-entry report is returned and partial success is expected. Route/disabled comment lines from this site's own exports are restored as well (another user's custom route codes are rejected into the report)
GET /api/dns/v1/lines Available routes (platform presets + your custom routes). The line field on records takes a code from here; custom routes must use the full returned code, not just the name. Labels follow the Accept-Language header (English by default); scripts should rely on the immutable code
POST /api/dns/v1/lines body {"name":"office","cidrs":["203.0.113.0/24"]}. Create / update a custom route by name (queries whose source hits these IP ranges take this route); naming and IP-range limits match the panel, and the response carries a ready-to-use code
DELETE /api/dns/v1/lines/:name Delete a custom route (pass the bare name, no prefix). Refused while records still use it — move or delete those records first
Updating records When updating (id>0), omitted ttl / line / enabled keep their current values; pass enabled explicitly to change the disabled state. To edit a record: GET records to find its id, then POST with that id. Re-submitting an identical record returns 400 record_duplicate — it already exists in the state you want, no retry needed
Rate limits Per token 60/min and 1000/h; export/import additionally 3/min and 30/h per token. Per source IP 120/min and 2400/h. Every 429 carries Retry-After (seconds) — back off by it. Use the import endpoint to move records in bulk (a whole zone file counts as one request) instead of POSTing one by one
Error shapes Every error is {"error":"<code>","message":"<reason>"} — branch on error, show message to humans. 401 unauthorized; 403 token_scope_mismatch (DDNS/ACME tokens cannot reach management endpoints); 404 zone_not_found / record_not_found; 400 invalid_body / invalid_id / record_duplicate / record_cname_conflict / record_quota_exceeded / zone_suspended / service_error (message has the specific reason); 429 ip_rate_limited / token_rate_limited / io_rate_limited (with Retry-After)
Minimal example (curl)
TOKEN=nhtdns_xxxxxxxxxxxxxxxxxxxxxx

# 1) 列出 zone,拿到 id
curl -s https://anyns.io/api/dns/v1/zones -H "Authorization: Bearer $TOKEN"

# 2) 加一条 A 记录;响应 {"code":200,"data":{"record":{"id":123,"name":"www","type":"A","value":"192.0.2.10","ttl":300,"line":"default","enabled":true}}}
curl -s -X POST https://anyns.io/api/dns/v1/zones/<id>/records \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"www","type":"A","value":"192.0.2.10","ttl":300}'

# 2b) 改记录:先 GET 拿 id(data.records[].id),再带 id 改;没传的 ttl/line/enabled 保持原值
curl -s https://anyns.io/api/dns/v1/zones/<id>/records -H "Authorization: Bearer $TOKEN"
curl -s -X POST https://anyns.io/api/dns/v1/zones/<id>/records \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"id":123,"name":"www","type":"A","value":"192.0.2.11"}'

# 2c) 删记录:rid 是上面的记录 id(不是响应里的 code/status 字段);成功 {"code":200,"data":{"message":"记录已删除"}}
curl -s -X DELETE https://anyns.io/api/dns/v1/zones/<id>/records/123 -H "Authorization: Bearer $TOKEN"

# 出错时统一是 {"error":"record_duplicate","message":"已存在一条完全相同的记录…"} 这种形状;429 看 Retry-After 头

# 3) 分线解析:同一个名字,大陆线路答另一个 IP(line 填线路 code,平台线见 GET /lines)
curl -s -X POST https://anyns.io/api/dns/v1/zones/<id>/records \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"www","type":"A","value":"198.51.100.7","ttl":300,"line":"cn"}'

# 4) 自定义线路:建线拿 code,再把记录挂上去
curl -s -X POST https://anyns.io/api/dns/v1/lines \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"office","cidrs":["203.0.113.0/24"]}'

# 5) 整域导出备份
curl -s https://anyns.io/api/dns/v1/zones/<id>/export -H "Authorization: Bearer $TOKEN"