Zone file checker API

Reads a zone file of up to 64 KiB in master file format (RFC 1035) with $ORIGIN and $TTL, and reports lines that cannot be read, a missing or misplaced SOA, missing NS, CNAME at the apex or beside other data, duplicates, mixed TTLs in a set, odd TTLs, out-of-zone names, targets without addresses and SOA values. Nothing is looked up.

POST https://api.lookproof.dev/v1/dns/zone-lint

Updated 2026-10-09

Examples use the Base (eip155:8453) network.

Input

A JSON body with content-type: application/json.

NameTypeRequiredLimitsDescription
zonestringYes1–65536 charactersThe zone file text in master file format, at most 65536 characters. Nothing is looked up.
originstringYes1–1024 charactersThe zone's name: relative names and @ in the zone file are read against it.

Example request

curl -i -X POST https://api.lookproof.dev/v1/dns/zone-lint \
  -H 'content-type: application/json' \
  -d '{"origin": "example.com", "zone": "$TTL 3600\n@       IN SOA  ns1.example.com. hostmaster.example.com. (\n                2026102701 ; serial\n                7200       ; refresh\n                3600       ; retry\n                1209600    ; expire\n                3600 )     ; minimum\n        IN NS   ns1\n        IN NS   ns2.example.net.\n        IN MX   10 mail\n        IN A    192.0.2.10\nns1     IN A    192.0.2.53\nmail    IN A    192.0.2.25\nwww     IN CNAME @\nwww     IN TXT  \"site-verification=abc123\"\n"}'
import asyncio
import os

from eth_account import Account
from x402 import x402Client
from x402.http.clients import x402HttpxClient
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import register_exact_evm_client


async def main() -> None:
    signer = EthAccountSigner(Account.from_key(os.environ["EVM_PRIVATE_KEY"]))
    client = register_exact_evm_client(x402Client(), signer, networks="eip155:8453")
    async with x402HttpxClient(client) as http:
        response = await http.post('https://api.lookproof.dev/v1/dns/zone-lint', json={'origin': 'example.com', 'zone': '$TTL 3600\n@       IN SOA  ns1.example.com. hostmaster.example.com. (\n                2026102701 ; serial\n                7200       ; refresh\n                3600       ; retry\n                1209600    ; expire\n                3600 )     ; minimum\n        IN NS   ns1\n        IN NS   ns2.example.net.\n        IN MX   10 mail\n        IN A    192.0.2.10\nns1     IN A    192.0.2.53\nmail    IN A    192.0.2.25\nwww     IN CNAME @\nwww     IN TXT  "site-verification=abc123"\n'})
        response.raise_for_status()
        print(response.json())


asyncio.run(main())
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
});

const response = await fetchWithPayment("https://api.lookproof.dev/v1/dns/zone-lint", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({"origin": "example.com", "zone": "$TTL 3600\n@       IN SOA  ns1.example.com. hostmaster.example.com. (\n                2026102701 ; serial\n                7200       ; refresh\n                3600       ; retry\n                1209600    ; expire\n                3600 )     ; minimum\n        IN NS   ns1\n        IN NS   ns2.example.net.\n        IN MX   10 mail\n        IN A    192.0.2.10\nns1     IN A    192.0.2.53\nmail    IN A    192.0.2.25\nwww     IN CNAME @\nwww     IN TXT  \"site-verification=abc123\"\n"}),
});
console.log(await response.json());

curl: unpaid request — returns 402 with the price (PAYMENT-REQUIRED). The Python and JavaScript clients pay it and send the request again. Only ordinary wallets (EOA) can pay; smart-contract wallets get a 402 without charge. See payments.

Example response

{
  "checked_at": "2026-10-24T12:00:00Z",
  "data": {
    "default_ttl": 3600,
    "name_count": 4,
    "notes": [
      "The zone is read as text only: nothing is looked up, and DNSSEC signatures are not verified."
    ],
    "origin": "example.com",
    "problems": [],
    "record_count": 9,
    "soa": {
      "expire": 1209600,
      "minimum": 3600,
      "mname": "ns1.example.com",
      "refresh": 7200,
      "retry": 3600,
      "rname": "hostmaster.example.com",
      "serial": 2026102701,
      "serial_format": "date"
    },
    "types": [
      {
        "count": 3,
        "type": "A"
      },
      {
        "count": 2,
        "type": "NS"
      },
      {
        "count": 1,
        "type": "CNAME"
      },
      {
        "count": 1,
        "type": "MX"
      },
      {
        "count": 1,
        "type": "SOA"
      },
      {
        "count": 1,
        "type": "TXT"
      }
    ]
  },
  "endpoint": "dns.zone_lint",
  "findings": [
    {
      "code": "ZONE_CNAME_WITH_OTHER_DATA",
      "evidence": {
        "count": 1,
        "items": [
          {
            "owner": "www.example.com",
            "types": "TXT"
          }
        ]
      },
      "explanation": "A name with a CNAME may hold no other data except DNSSEC records (RFC 1034 section 3.6.2, RFC 2181 section 10.1). Resolvers then answer inconsistently and many loaders refuse the zone. The evidence names the other types.",
      "message": "A name has a CNAME and other records at the same time.",
      "recommendation": "Keep either the CNAME or the other records for that name.",
      "severity": "error"
    }
  ],
  "meta": {
    "cache_hit": false,
    "duration_ms": 42,
    "fresh_until": "2026-10-24T12:05:00Z",
    "observed_at": "2026-10-24T12:00:00Z",
    "resolver": null,
    "source": "offline"
  },
  "request_id": "req_aaaaaaaaaaaaaaaaaaaaaaaaaa",
  "schema_version": "1.0",
  "status": "error"
}

Response fields

FieldMeaning
request_idUnique id of this request. Quote it when you contact support.
endpointThe endpoint id, for example mail.spf.
schema_versionVersion of the response format, currently 1.0.
checked_atWhen the check ran (ISO 8601, UTC).
statusok, info, warning or error from the worst finding, or unknown when the check was incomplete. unknown is not an error in the domain.
dataThe check's own data. Its fields are listed below.
findingsWhat was found: code, severity, message, explanation and evidence.
metaDuration, cache use, resolver and how long the result stays fresh.

Fields in data

FieldType
originstring
record_countinteger
name_countinteger
typesarray of ZoneTypeCount
default_ttlinteger or null
soaSoaValues or null
problemsarray of ZoneProblem
notesarray of string

Status codes

StatusCodeMeaningDo you pay?
200—The result is delivered, also when it has negative findings.Yes: charged once per payment; redelivery of the same payment is free
402—Payment is required, or the attached payment was refused. The body states the price.No
400bad_requestThe request is malformed.No
409payment_in_progress, payment_bound_to_other_requestThis payment is already being processed, or it is bound to a different request. No new payment is taken.No
413payload_too_largeThe body is larger than 64 KB.No
422invalid_inputThe input failed validation on a request that carries a payment; nothing is charged. Without a payment, invalid input gets a 402 whose error field names the problem.No
429rate_limitedToo many requests from this IP address or payer.No
502check_failedThe check could not be completed.No
503busy, unavailable, payments_unavailableThe service is busy or temporarily unavailable.No
504deadline_exceededThe check ran out of time.No

Errors use the common error body. See errors.

Findings this endpoint can return

Name servers (1)
SOA record (6)
Zone files (17)