DoH JSON API

Query ResolveDB names over HTTPS using a Google-style JSON DNS response.

Endpoint

GET https://doh.resolvedb.io/resolve

The equivalent parameter-selected route is https://doh.resolvedb.io/dns-query?name=....

ResolveDB is authoritative-only. These endpoints resolve ResolveDB qnames, not arbitrary Internet domains.

Request

curl --get https://doh.resolvedb.io/resolve \
  --data-urlencode "name=get.newyork.weather.public.v1.resolvedb.net" \
  --data-urlencode "type=TXT"
ParameterRequiredDefaultBehavior
nameYes-ResolveDB qname, maximum 253 characters
typeNoADNS type name or number; use TXT for UQRP data
cdNofalseCopied to the DNS checking-disabled flag
edns_client_subnetNo-Echoed in the JSON response; not used for routing
doNo-Accepted for compatibility but currently ignored
random_paddingNo-Accepted and ignored

Response

{
  "Status": 0,
  "TC": false,
  "RD": true,
  "RA": false,
  "AD": false,
  "CD": false,
  "Question": [
    { "name": "get.newyork.weather.public.v1.resolvedb.net", "type": 16 }
  ],
  "Answer": [
    {
      "name": "get.newyork.weather.public.v1.resolvedb.net",
      "type": 16,
      "TTL": 300,
      "data": "\"v=rdb1;s=ok;t=data;...\""
    }
  ]
}

Status is the DNS RCODE. ResolveDB normally represents an unknown name as NOERROR with no answers (NODATA), not NXDOMAIN. RA is false because the service is authoritative, not recursive.

All JSON responses use Cache-Control: no-store. Use DNS wire transport when you need HTTP caching based on the DNS TTL.

Parsing TXT Data

Answer[].data uses DNS presentation format. A TXT record can contain multiple quoted character strings. Unquote and concatenate them before parsing the UQRP envelope. Then decode d according to e.

The ordinary d= field consumes the entire remainder, including semicolons and equals signs. e=b64 is standard padded Base64 and produces bytes; decode those bytes as UTF-8 only for text/JSON. Payload ttl is a hint, distinct from Answer[].TTL. Dataset envelopes have their own identifiers and fields.

This example reads the operator-managed, public-read Hooli demo record. Hooli namespaces are read-only examples and cannot be claimed by customers.

function decodeTxtPresentation(value) {
  // ResolveDB JSON escapes quotes/backslashes. For arbitrary DNS presentation
  // (including decimal byte escapes), use the preview SDK's parseCharStrings.
  const strings = value.match(/"(?:\\.|[^"\\])*"/g) ?? [];
  return strings.map((part) => JSON.parse(part)).join('');
}

function parseUqrpJson(txt) {
  const dataMarker = ';d=';
  const markerIndex = txt.indexOf(dataMarker);
  if (markerIndex === -1) throw new Error('Missing UQRP data field');

  const metadata = Object.fromEntries(
    txt.slice(0, markerIndex).split(';').map((field) => field.split('=', 2)),
  );
  let payload = txt.slice(markerIndex + dataMarker.length);
  if (metadata.e === 'b64') {
    const bytes = Uint8Array.from(atob(payload), (character) => character.charCodeAt(0));
    payload = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
  }
  return JSON.parse(payload);
}

const name = 'get.dark-mode.flags.hooli.v1.resolvedb.net';
const response = await fetch(
  `https://doh.resolvedb.io/resolve?name=${encodeURIComponent(name)}&type=TXT`,
);
const dns = await response.json();
if (dns.Status !== 0 || !dns.Answer?.length) {
  throw new Error(dns.Comment || 'No answer');
}

const txt = decodeTxtPresentation(dns.Answer[0].data);
console.log(parseUqrpJson(txt));
// { enabled: true, variant: "default" }

Private hosted records use the same response format but require an auth-rdbq... label. Treat the qname as a bearer credential and use HTTPS.

Status Codes

DNS statusMeaning
0NOERROR: answer or NODATA
1FORMERR: malformed query
2SERVFAIL: service or storage failure
5REFUSED: authorization or namespace denial

Validation errors return HTTP 400 with a static JSON error. Valid DNS queries, including DNS-level errors, return HTTP 200. The JSON endpoint does not emit plan-based X-RateLimit-* headers.

CORS

The endpoint permits browser requests from any origin.

Shared Answering Contract (Implemented)

The shared answering contract aligns product values, effective DNS TTLs, and error outcomes with native DNS. It adds requested DNSSEC material through do=true, supporting DNSKEY queries, and query statistics for DoH. Signing does not by itself assert the recursive validation meaning of the AD flag.

Shared schema/zone and units/moon/gated-BTC answering now provide the common outcomes and requested do=true material in the repository. Computed ordinary results retain flat fields after v=rdb1;s=ok;t=data. BTC remains mock-only and gated off. Hosted private/public-read/demo records now use that shared path too: private hits and negatives have TTL 0 (including SOA/proofs), public positive TTLs are capped at record expiry, and corrupt values fail with SERVFAIL. All-family evaluation statistics include DoH. Weather, forecast, sun, and GeoIP share the same ordinary fields and requested signatures; forecast uses daily provider output and TTL at most 1800. Dataset answering is also shared. Statistics identify the evaluated family, including records for hosted units and datasets for dataset reads. Verified private attribution survives miss/expiry; public/schema/dataset activity remains unattributed. JSON HTTP caching remains no-store. Repository conformance is complete; production rollout and package publication remain operator-controlled.

Names with a terminal root dot or different DNS letter case must select the same query. Encoded query parameters use case-stable b32-/hex-, while TXT e=b64 payloads use standard padded Base64. Ordinary results retain flat fields or a final d= payload; dataset envelopes remain distinct. JSON responses retain HTTP no-store. Encoding alignment is implemented across server, Rails accepted hosted keys, and preview Go/JS clients; MCP rejects reserved encoder prefixes.

Next Steps