DNS
Your DNS records: the zones and records behind every name you point at Hanzo.
Also for this capability: API · CLI · SDKs
Your DNS records: the zones and records behind every name you point at Hanzo.
| Base URL | https://api.hanzo.ai |
| Operations | 12 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
| Also reached at | /v1/dns — a relay onto the DNS control plane that owns the zone store |
Specification
HIP-1321 · DNS — Zones and Records — Draft · read the specification →
/v1/dns is the zones and records behind every name an org points at Hanzo. It
is implemented in hanzoai/cloud at apps/dns (HIP-0106) as a head over the
Hanzo DNS control plane (dns/plugin/hanzodns), which owns the authoritative
zone and record store.
This HIP states the boundary between dns and domain (§1), the relay's
identity rule (§2), and why the surface is five untyped operations rather than a
typed CRUD (§3).
Motivation
dns and domain are one word apart in English and two different questions in
practice, and the estate has folded pairs like this before. The fold is wrong
here. Buying a name and operating the zone under it are separate acts with
separate lifetimes: an org can hold a name it serves nowhere, and can serve a
zone for a name bought somewhere else. Folding either into the other would put a
registrar's billing and a nameserver's records behind one address, and a caller
who wanted one would be handed both.
Both are also heads over the same upstream plane, which is what makes the temptation to merge them look reasonable from the router's side. Sharing an upstream is not sharing a store, and HIP-0139 §7 decides capability boundaries by the store, not by the hop behind them.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The boundary: the name, and the zone under it
- domain (HIP-1123) is the name as property: searching for one, buying it, renewing it, and who holds it.
- dns is the zone under a name: the records that decide what resolves, and what each answer is.
An org MAY hold a domain with no zone and MAY serve a zone for a name this platform did not sell it. Neither capability MUST assume the other has a row.
§2 It owns no store, and it substitutes no credential
Every row belongs to the DNS plane. This head holds no DNS state and caches nothing across requests.
The plane is OIDC-gated and keys every zone per-org. The head therefore relays
the caller's OWN validated bearer (cloud.CallerBearer) together with the
server-derived X-Org-Id, and MUST NOT substitute a standing service
credential — a shared credential collapses tenants, because the plane would then
see one caller for every org. A request with no validated principal is refused
403 before any byte leaves cloud.
The head MUST build a fresh upstream request and set only the headers it means
to send. No inbound header is relayed blindly: a stray cookie, a forged X-* or
a second Authorization never crosses the hop.
§3 Five operations, and why none is typed
The whole surface is one greedy-wildcard registration at /v1/dns/* carrying
every verb, so the document publishes five operations — one per method the
generator knows — and none of them can be a typed op. The wire facts that make
that so are re-verified against the pinned zip in apps/dns/typed_wire_test.go
rather than asserted here.
Prose is consequently the only thing this head can state about itself, and it
states it per method. A caller who needs the record-level shape reads it from
the DNS plane's own contract. When the module named at the registration in
Mount lands, the operations become typed and this section is what changes.
§4 The name
The capability is dns — the initialism everyone says, in the address, the
package, the tag and this HIP, per HIP-0139 §2. It is not spelled out, and it is
not pluralized.
Rationale
The alternative considered was folding /v1/dns under /v1/domain as a
sub-resource. It reads tidy and it is wrong twice: it asserts a registrar
relationship this platform does not require, and it puts two stores behind one
capability's name, which is the defect HIP-0106 exists to refuse.
Security Considerations
A zone is an attack surface, not a preference. Whoever can write a record can redirect a name, and whoever can read one learns an org's internal topology. Because the caller's own bearer is relayed unchanged, an org reaches only the zones the DNS plane already grants it, and this head cannot widen that — it has no credential with which to.
Fail-closed is the default at both hops: no validated principal is a 403 here, and an unauthorized bearer is the plane's own refusal, not this head's silence.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | dns at its own prefix | 12 operations |
| CLI | hanzo dns … | 12 of 12 |
| SDK | DnsApi in every published client | 12 methods |
| MCP | tool dns on https://api.hanzo.ai/v1/mcp | 5 operations, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/dns/zones, operation get_dns_zones:
hanzo dns zones listimport { Configuration, DnsApi } from 'hanzoai';
const api = new DnsApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getDnsZones();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import DnsApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = DnsApi(client).get_dns_zones()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.DnsAPI.GetDnsZones(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, dns_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = dns_api::get_dns_zones(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.DnsApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new DnsApi(client).getDnsZones();curl https://api.hanzo.ai/v1/dns/zones \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches dns through the dns tool, which names its 5 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_dns"
}
}
}'Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/dns/health | Check the DNS control plane |
POST /v1/dns/sync | Push a set of zones and records in one call |
GET /v1/dns/zones/{zone}/records/{record} | Read one DNS record |
PUT /v1/dns/zones/{zone}/records/{record} | Amend a DNS record |
PATCH /v1/dns/zones/{zone}/records/{record} | Amend a DNS record |
DELETE /v1/dns/zones/{zone}/records/{record} | Delete a DNS record |
GET /v1/dns/zones/{zone}/records | List a zone's DNS records |
POST /v1/dns/zones/{zone}/records | Create a DNS record |
GET /v1/dns/zones/{zone} | Read one DNS zone |
DELETE /v1/dns/zones/{zone} | Delete a DNS zone |
GET /v1/dns/zones | List your org's DNS zones |
POST /v1/dns/zones | Create a DNS zone |
How is this guide?