The API can import a zone file into a record group, adding a DNS record to monitor for each record in it, and export a record group's DNS records as a zone file. Both work the same way as in the web interface. The Zone Files guide applies here too: which records are supported, the reverse DNS shorthand, and how records that zone file syntax can't express are kept as comments, so an exported file imports back into the same DNS records, apart from which ones are exclusive.
Endpoints
| Request | Required access | What it does |
|---|---|---|
GET /api |
Read | Exports the record group's DNS records as JSON holding zone file text. See Exporting. |
POST /api |
Read and write | Imports a zone file into the record group. See Importing. |
Importing
The examples on this page read your API key from $DNSCHECK_API_KEY, set up as in Getting Started, and the record group's ID from $GROUP_ID.
A record group's ID is the UUID at the end of its page's URL in DNS Check (https://www.dnscheck.co/tests/id), and the id the API returns for it, such as when you list your record groups:
export GROUP_ID="b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a"
Each example below reads a zone file named example.com.zone from the current directory and imports it into the record group by sending the file's whole text as the request's zone_file field. Use your own file's name: a zone file exported from your DNS host, or one you've written.
The zone file's text has to go inside a JSON string, which can't hold its line breaks and quotes as they are. jq, a command-line JSON tool that most package managers can install, does the conversion. Without jq, use the Python, JavaScript, or Go example.
jq -Rs '{zone_file: .}' example.com.zone | \
curl https://www.dnscheck.co/api/v2/record-groups/$GROUP_ID/zone-file/import \
-H "Authorization: Bearer $DNSCHECK_API_KEY" \
-H "Content-Type: application/json" \
--data @-
This uses the requests library (pip install requests), whose json= sends the body as JSON. It prints how many records were added and skipped, and why each skipped record was skipped. If the import is refused, it prints the error and what's wrong instead:
import os
import requests
with open("example.com.zone") as f:
zone_file = f.read()
response = requests.post(
f"https://www.dnscheck.co/api/v2/record-groups/{os.environ['GROUP_ID']}/zone-file/import",
headers={
"Authorization": f"Bearer {os.environ['DNSCHECK_API_KEY']}",
},
json={"zone_file": zone_file},
timeout=60,
)
if response.headers.get("Content-Type", "").startswith("application/problem+json"):
problem = response.json()
print(f"{problem['code']}: {problem['detail']}")
for error in problem.get("errors", []):
print(error["message"])
raise SystemExit(1)
response.raise_for_status()
result = response.json()
print(f"Added {result['created']}, skipped {result['skipped']}")
for warning in result["warnings"]:
print(warning)
This needs Node.js 18 or later, which has fetch built in. Save it as import-zone-file.mjs (so top-level await works) and run it with node import-zone-file.mjs. It prints how many records were added and skipped, and why each skipped record was skipped. If the import is refused, it prints the error and what's wrong instead:
import { readFile } from "node:fs/promises";
const zoneFile = await readFile("example.com.zone", "utf8");
const response = await fetch(`https://www.dnscheck.co/api/v2/record-groups/${process.env.GROUP_ID}/zone-file/import`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ zone_file: zoneFile }),
signal: AbortSignal.timeout(60000),
});
if (response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
const problem = await response.json();
console.error(`${problem.code}: ${problem.detail}`);
for (const error of problem.errors ?? []) console.error(error.message);
process.exit(1);
}
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
const result = await response.json();
console.log(`Added ${result.created}, skipped ${result.skipped}`);
for (const warning of result.warnings) console.log(warning);
Using only the standard library. Save it as import-zone-file.go and run it with go run import-zone-file.go. It prints how many records were added and skipped, and why each skipped record was skipped. If the import is refused, it prints the error and what's wrong instead:
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
func main() {
zoneFile, err := os.ReadFile("example.com.zone")
if err != nil {
log.Fatal(err)
}
body, err := json.Marshal(map[string]string{"zone_file": string(zoneFile)})
if err != nil {
log.Fatal(err)
}
url := "https://www.dnscheck.co/api/v2/record-groups/" + os.Getenv("GROUP_ID") + "/zone-file/import"
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("DNSCHECK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 60 * time.Second}
response, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode >= 400 && !strings.HasPrefix(response.Header.Get("Content-Type"), "application/problem+json") {
log.Fatalf("HTTP %s", response.Status)
}
var result struct {
Created, Skipped int
Warnings []string
Code, Detail string
Errors []struct{ Message string }
}
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
log.Fatal(err)
}
if response.StatusCode >= 400 {
fmt.Fprintf(os.Stderr, "%s: %s\n", result.Code, result.Detail)
for _, e := range result.Errors {
fmt.Fprintln(os.Stderr, e.Message)
}
os.Exit(1)
}
fmt.Printf("Added %d, skipped %d\n", result.Created, result.Skipped)
for _, warning := range result.Warnings {
fmt.Println(warning)
}
}
To make a retry safe, send an Idempotency-Key: see Idempotency Keys.
The other fields are the import options from the web form, with the same defaults:
| Field | Type | Description |
|---|---|---|
zone_file |
string | Required. The zone file's text, up to 100KB. Without an $ORIGIN, names are relative to the DNS root, so they're read as already fully qualified. |
fix_errors |
boolean | Correct common syntax errors, such as a domain name missing its trailing dot, and detect a missing $ORIGIN, as some DNS hosts' exports need. Default: true. |
exclusive |
boolean | Make each imported DNS record exclusive. Inverted and load balancer records are never exclusive. Default: false. |
replace_existing |
boolean | Make the record group match the zone file, deleting its DNS records that aren't in the file. See Importing Again. Default: false. |
The response, with a 200 status, says what the import did:
{
"object": "zone_file_import",
"record_group_id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
"created": 1,
"skipped": 1,
"unchanged": 0,
"updated": 0,
"deleted": 0,
"limit_reached": false,
"warnings": [
"Skipped the old.example.com A record: IP address should be an IPv4 address, or * for any address"
],
"dns_records": [
{
"id": "6f1c9e2d-0b8a-4f7e-8d6c-5b4a3f2e1d0c",
"object": "dns_record",
"record_group_id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
"name": "example.com.",
"type": "MX",
"values": ["10 mail.example.com."],
"exclusive": false,
"invert": false,
"load_balancer": false,
"status": "unknown",
"fail_count": 0,
"error": null,
"checked_at": null,
"created_at": "2026-10-23T14:05:00Z",
"updated_at": "2026-10-23T14:05:00Z",
"web_url": "https://www.dnscheck.co/tests/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a/12345",
"api_url": "https://www.dnscheck.co/api/v2/dns-records/6f1c9e2d-0b8a-4f7e-8d6c-5b4a3f2e1d0c"
}
]
}
| Field | Type | Description |
|---|---|---|
created |
integer | How many DNS records were added |
skipped |
integer | How many records couldn't be imported, such as one with an invalid value, of a type DNS Check doesn't monitor, or a load balancer or inverted record on a Basic account |
unchanged |
integer | How many records weren't added because the record group already had them, or because the zone file repeats them |
updated |
integer | How many DNS records already in the record group replace_existing gave the import's exclusive setting |
deleted |
integer | How many DNS records replace_existing deleted because they aren't in the zone file |
limit_reached |
boolean | Whether the import stopped at the plan's DNS record limit, leaving the rest of the file unimported |
warnings |
array of strings | Why each skipped record was skipped, and why the import stopped early, if it did, in the words the web interface uses |
dns_records |
array | The DNS records added, not those already in the record group, as the DNS Records API returns them |
A record that can't be imported is skipped, and the rest of the file still imports. If too many are skipped, the import stops early, and warnings say so. A file that neither adds a DNS record nor matches one already in the record group gets a 422 with the code validation_failed and changes nothing, even with replace_existing. Its errors say why:
- The zone file is empty.
- It's larger than 100KB.
- It contains no DNS records we can read. We ignore lines we can't read, so a malformed line in an otherwise valid file isn't counted as
skipped. - Every record in it was skipped. Each skipped record gets its own error, with the same message a successful import gives it in
warnings.
An import adds DNS records until the account reaches its plan's limit, then stops, setting limit_reached. If the account is already at its limit, the import gets a 403 with the code plan_limit_reached.
Importing Again
An import adds only the DNS records the record group doesn't already have. A record in the file matches one already in the group when its name, type, and value match (ignoring case when DNS does, such as in names), as well as its invert and load_balancer settings. Matches count as unchanged instead of being added, so importing the same file again adds nothing. A record the file repeats is added once.
To make a record group match a zone file, import it with replace_existing. The group's DNS records that the file has keep their IDs, event history, and status, and take the import's exclusive setting, counted as updated when that changes it. The ones the file doesn't have are deleted, with their event history. So syncing a zone on a schedule changes only what changed in the zone, and a failing DNS record doesn't send new notifications each time.
Writes and Checks
What Counts as a Write gives what an import costs against the write budget.
Imported DNS records are checked the same way as DNS records added one at a time.
Exporting
This returns the record group's DNS records as zone file text, inside a JSON object:
curl https://www.dnscheck.co/api/v2/record-groups/$GROUP_ID/zone-file \
-H "Authorization: Bearer $DNSCHECK_API_KEY"
The response has a 200 status. forward and reverse each hold zone file text as one JSON string, so line breaks show as \n:
{
"object": "zone_file",
"record_group_id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
"forward": "mail.example.com. A 192.0.2.25\nexample.com. MX 10 mail.example.com.\n",
"reverse": null
}
As on the record group's Export page, the DNS records come in two parts, since they're usually managed in different places. forward holds every DNS record but PTR records, which are managed on your domain's authoritative name servers. reverse holds the PTR records, which your hosting provider or ISP usually manages. Either is null when the record group has no DNS records of its kind. To save the forward records to a file:
curl -s https://www.dnscheck.co/api/v2/record-groups/$GROUP_ID/zone-file \
-H "Authorization: Bearer $DNSCHECK_API_KEY" | jq -r .forward > example.com.zone
Exporting one record group and importing the result into another copies its DNS records, including load balancer, inverted, wildcard-value, and ALIAS records, as long as you import both forward and reverse (one import each).
Zone files don't say which DNS records are exclusive, though, and an import's exclusive option applies to every DNS record it adds. So for a record group that mixes exclusive and non-exclusive DNS records, import with exclusive off, then update the exclusive ones.
Protect your DNS infrastructure with automated monitoring
Get notified immediately when DNS records change. Start monitoring your critical DNS infrastructure for free in under 5 minutes.
No credit card required • Cancel anytime