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.
| Name | Type | Required | Limits | Description |
|---|---|---|---|---|
zone | string | Yes | 1–65536 characters | The zone file text in master file format, at most 65536 characters. Nothing is looked up. |
origin | string | Yes | 1–1024 characters | The 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
| Field | Meaning |
|---|---|
request_id | Unique id of this request. Quote it when you contact support. |
endpoint | The endpoint id, for example mail.spf. |
schema_version | Version of the response format, currently 1.0. |
checked_at | When the check ran (ISO 8601, UTC). |
status | ok, info, warning or error from the worst finding, or unknown when the check was incomplete. unknown is not an error in the domain. |
data | The check's own data. Its fields are listed below. |
findings | What was found: code, severity, message, explanation and evidence. |
meta | Duration, cache use, resolver and how long the result stays fresh. |
Fields in data
| Field | Type |
|---|---|
origin | string |
record_count | integer |
name_count | integer |
types | array of ZoneTypeCount |
default_ttl | integer or null |
soa | SoaValues or null |
problems | array of ZoneProblem |
notes | array of string |
Status codes
| Status | Code | Meaning | Do 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 |
| 400 | bad_request | The request is malformed. | No |
| 409 | payment_in_progress, payment_bound_to_other_request | This payment is already being processed, or it is bound to a different request. No new payment is taken. | No |
| 413 | payload_too_large | The body is larger than 64 KB. | No |
| 422 | invalid_input | The 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 |
| 429 | rate_limited | Too many requests from this IP address or payer. | No |
| 502 | check_failed | The check could not be completed. | No |
| 503 | busy, unavailable, payments_unavailable | The service is busy or temporarily unavailable. | No |
| 504 | deadline_exceeded | The check ran out of time. | No |
Errors use the common error body. See errors.
Findings this endpoint can return
Name servers (1)
- Warning
NS_SINGLEThe zone has only one name server.
SOA record (6)
- Warning
SOA_EXPIRE_BELOW_REFRESHThe SOA expire is not longer than refresh plus retry. - Info
SOA_EXPIRE_SHORTThe SOA expire is shorter than two weeks. - Info
SOA_NEGATIVE_TTL_LONGThe SOA minimum (negative caching TTL) is longer than three hours. - Info
SOA_REFRESH_OUT_OF_RANGEThe SOA refresh is outside 20 minutes to 12 hours. - Warning
SOA_RETRY_NOT_BELOW_REFRESHThe SOA retry is not shorter than the refresh. - Info
SOA_SERIAL_NOT_DATEThe SOA serial is not in the YYYYMMDDnn date form.
Zone files (17)
- Error
ZONE_CNAME_AT_APEXThe zone apex has a CNAME record. - Error
ZONE_CNAME_WITH_OTHER_DATAA name has a CNAME and other records at the same time. - Warning
ZONE_DIRECTIVE_IGNOREDThe zone file uses $INCLUDE or $GENERATE, which were not read. - Warning
ZONE_DUPLICATE_RECORDThe same record appears more than once in the zone. - Error
ZONE_MULTIPLE_CNAMEA name has more than one CNAME record. - Error
ZONE_MULTIPLE_SOAThe zone has more than one SOA record. - Warning
ZONE_NO_DEFAULT_TTLRecords without a TTL appear before any $TTL line. - Error
ZONE_NO_NSThe zone has no NS records at its apex. - Error
ZONE_NO_SOAThe zone has no SOA record. - Warning
ZONE_OUT_OF_ZONEA record's name lies outside the zone. - Error
ZONE_SOA_NOT_AT_APEXThe SOA record is not at the zone's name. - Error
ZONE_SYNTAX_ERRORA line of the zone file cannot be read. - Warning
ZONE_TARGET_IS_CNAMEAn NS, MX or SRV record points to a name that is a CNAME. - Warning
ZONE_TARGET_NO_ADDRESSAn NS, MX or SRV target inside the zone has no address record. - Warning
ZONE_TTL_MISMATCHRecords of one name and type have different TTLs. - Error
ZONE_TTL_OUT_OF_RANGEA TTL is larger than DNS allows. - Info
ZONE_TTL_UNUSUALA TTL is below one minute or above one week.