DNS Records API

A DNS record tells DNS Check what to monitor: a name, a record type, and the expected value. Each DNS record belongs to a record group.

Endpoints

Request Required access What it does
GET /api/v2/record-groups/{id}/dns-records Read Lists the record group's DNS records, oldest first. Query parameters: type (case is ignored), status (passing, failing, or unknown, ignoring case), name (part of the name, ignoring case and a trailing dot), and limit and cursor for paging (see Lists). For example: /api/v2/record-groups/{id}/dns-records?type=MX&status=failing.
GET /api/v2/dns-records Read Lists every DNS record in the account, across all its record groups, oldest first, with the same query parameters. It always needs an API key. For example, /api/v2/dns-records?status=failing finds every failing DNS record in one request. Each one's record_group_id says which record group it's in.
POST /api/v2/record-groups/{id}/dns-records Read and write Adds a DNS record, returning it with 201.
GET /api/v2/dns-records/{id} Read Gets one DNS record, including its latest check's status and, if it failed, why.
PATCH /api/v2/dns-records/{id} Read and write Changes the fields sent and leaves the rest as they are.
DELETE /api/v2/dns-records/{id} Read and write Deletes the DNS record and its events, returning 204.
POST /api/v2/dns-records/{id}/check Read and write Queues a check of the DNS record, returning 202. See Checking a DNS Record on Demand.
GET /api/v2/dns-records/{id}/events Read Lists the times the DNS record's status changed, newest first. See the Events API.

Reading a DNS Record

This reads a DNS record in the example record group. It works without the Authorization header too, but your own DNS records need it, with your API key in $DNSCHECK_API_KEY:

curl https://www.dnscheck.co/api/v2/dns-records/798282f4-3991-41a1-9dc9-0f0284de91f8 \
  -H "Authorization: Bearer $DNSCHECK_API_KEY"

The response is the DNS record, with a 200 status. status, error, and checked_at are from its latest check, and Fields describes the rest:

{
  "id": "798282f4-3991-41a1-9dc9-0f0284de91f8",
  "object": "dns_record",
  "record_group_id": "ea883d67-d9f6-45e3-b3a1-844dd1857824",
  "name": "dnscheck.co.",
  "type": "MX",
  "values": [
    "10 mail.protonmail.ch."
  ],
  "exclusive": true,
  "invert": false,
  "load_balancer": false,
  "status": "passing",
  "fail_count": 0,
  "error": null,
  "checked_at": "2026-11-15T00:05:35Z",
  "created_at": "2015-04-17T20:46:06Z",
  "updated_at": "2023-01-29T05:15:35Z",
  "web_url": "https://www.dnscheck.co/tests/ea883d67-d9f6-45e3-b3a1-844dd1857824/8",
  "api_url": "https://www.dnscheck.co/api/v2/dns-records/798282f4-3991-41a1-9dc9-0f0284de91f8"
}

Finding Failing DNS Records

To list every DNS record in your account that's failing, whichever record group it's in, filter the account-wide list by status. Each item is a DNS record, as Fields describes, and its record_group_id says which record group it's in. A list is paged, so keep requesting while has_more is true, passing next_cursor back as cursor (see Lists).

These print each failing DNS record's name, type, and web_url, the link to its page in DNS Check. limit=200 asks for the largest page:

This prints the first page. It uses jq, and the Python, JavaScript, and Go versions show how to continue to the next pages:

curl -s "https://www.dnscheck.co/api/v2/dns-records?status=failing&limit=200" \
  -H "Authorization: Bearer $DNSCHECK_API_KEY" \
  | jq -r '.data[] | "\(.name) \(.type) \(.web_url)"'

This uses the requests library (pip install requests):

import os

import requests

params = {"status": "failing", "limit": 200}
while True:
    response = requests.get(
        "https://www.dnscheck.co/api/v2/dns-records",
        headers={"Authorization": f"Bearer {os.environ['DNSCHECK_API_KEY']}"},
        params=params,
        timeout=30,
    )
    if response.headers.get("Content-Type", "").startswith("application/problem+json"):
        problem = response.json()
        print(f"{problem['code']}: {problem['detail']}")
        raise SystemExit(1)
    response.raise_for_status()
    page = response.json()
    for record in page["data"]:
        print(record["name"], record["type"], record["web_url"])
    if not page["has_more"]:
        break
    params["cursor"] = page["next_cursor"]

This needs Node.js 18 or later. Save it in a .mjs file, so top-level await works:

const params = new URLSearchParams({ status: "failing", limit: "200" });
while (true) {
  const response = await fetch(`https://www.dnscheck.co/api/v2/dns-records?${params}`, {
    headers: { Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}` },
    signal: AbortSignal.timeout(30000),
  });
  if (response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
    const problem = await response.json();
    console.error(`${problem.code}: ${problem.detail}`);
    process.exit(1);
  }
  if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
  const page = await response.json();
  for (const record of page.data) console.log(record.name, record.type, record.web_url);
  if (!page.has_more) break;
  params.set("cursor", page.next_cursor);
}

Using only the standard library. Save it as failing-records.go and run it with go run failing-records.go:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

func main() {
	client := &http.Client{Timeout: 30 * time.Second}
	params := url.Values{"status": {"failing"}, "limit": {"200"}}
	for {
		req, err := http.NewRequest("GET", "https://www.dnscheck.co/api/v2/dns-records?"+params.Encode(), nil)
		if err != nil {
			log.Fatal(err)
		}
		req.Header.Set("Authorization", "Bearer "+os.Getenv("DNSCHECK_API_KEY"))
		response, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		}
		if response.StatusCode >= 400 && !strings.HasPrefix(response.Header.Get("Content-Type"), "application/problem+json") {
			log.Fatalf("HTTP %s", response.Status)
		}
		var page struct {
			Data []struct {
				Name, Type string
				WebURL     string `json:"web_url"`
			}
			HasMore      bool   `json:"has_more"`
			NextCursor   string `json:"next_cursor"`
			Code, Detail string
		}
		err = json.NewDecoder(response.Body).Decode(&page)
		response.Body.Close()
		if err != nil {
			log.Fatal(err)
		}
		if response.StatusCode >= 400 {
			log.Fatalf("%s: %s", page.Code, page.Detail)
		}
		for _, record := range page.Data {
			fmt.Println(record.Name, record.Type, record.WebURL)
		}
		if !page.HasMore {
			break
		}
		params.Set("cursor", page.NextCursor)
	}
}

Each prints a line per failing DNS record, such as the following. No output means no DNS record is failing:

example.com. MX https://www.dnscheck.co/tests/3f6b2c1e-8d4a-4b7e-9c2f-5a1d7e0b6c93/5529
www.example.com. A https://www.dnscheck.co/tests/3f6b2c1e-8d4a-4b7e-9c2f-5a1d7e0b6c93/5530
mail.example.org. A https://www.dnscheck.co/tests/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a/5531

A list like this suits a periodic report. To react when a DNS record starts failing, use webhooks or the event feed instead. Both tell you about changes without scanning every DNS record.

Adding a DNS Record

To add a DNS record to monitor, send its name, type, and values to the record group it belongs in.

The example below reads that record group's ID from $GROUP_ID. You can get it from the record group's id in the API, or from the end of the record group's page URL in DNS Check.

The example adds an MX record so DNS Check can confirm that example.com's mail goes to mail.example.com:

curl https://www.dnscheck.co/api/v2/record-groups/$GROUP_ID/dns-records \
  -H "Authorization: Bearer $DNSCHECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "example.com", "type": "MX", "values": ["10 mail.example.com."]}'

This uses the requests library (pip install requests):

import os

import requests

response = requests.post(
    f"https://www.dnscheck.co/api/v2/record-groups/{os.environ['GROUP_ID']}/dns-records",
    headers={
        "Authorization": f"Bearer {os.environ['DNSCHECK_API_KEY']}",
    },
    json={"name": "example.com", "type": "MX", "values": ["10 mail.example.com."]},
    timeout=30,
)
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()
record = response.json()
print(record["id"])

This needs Node.js 18 or later. Save it in a .mjs file, so top-level await works:

const response = await fetch(`https://www.dnscheck.co/api/v2/record-groups/${process.env.GROUP_ID}/dns-records`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "example.com", type: "MX", values: ["10 mail.example.com."] }),
  signal: AbortSignal.timeout(30000),
});
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 record = await response.json();
console.log(record.id);

Using only the standard library. Save it as add-record.go and run it with go run add-record.go:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

func main() {
	body, err := json.Marshal(map[string]any{
		"name": "example.com", "type": "MX", "values": []string{"10 mail.example.com."},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest("POST", "https://www.dnscheck.co/api/v2/record-groups/"+os.Getenv("GROUP_ID")+"/dns-records", 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: 30 * 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 record struct {
		ID, Code, Detail string
		Errors           []struct{ Message string }
	}
	if err := json.NewDecoder(response.Body).Decode(&record); err != nil {
		log.Fatal(err)
	}
	if response.StatusCode >= 400 {
		fmt.Fprintf(os.Stderr, "%s: %s\n", record.Code, record.Detail)
		for _, e := range record.Errors {
			fmt.Fprintln(os.Stderr, e.Message)
		}
		os.Exit(1)
	}
	fmt.Println(record.ID)
}

The value in values is written the way it would be in a zone file, as each type's value format shows. The response is the new DNS record, with a 201 status and its api_url in the Location header. Its status is unknown until its first check.

To make a retry safe, send an Idempotency-Key: see Idempotency Keys. Getting Started walks through adding one and then reading its status.

Updating a DNS Record

An update sends only the fields it changes. To keep it from overwriting someone else's edit, send the ETag header from reading the DNS record as If-Match: if the DNS record has been edited since, the update gets a 412 with the code precondition_failed. This changes the value of the DNS record in $RECORD_ID, the id the API returned when it was added, reading it first for its ETag:

The first command reads the DNS record and saves its ETag in $ETAG, and the second sends it as If-Match:

curl -s -o /dev/null --etag-save etag.txt https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID \
  -H "Authorization: Bearer $DNSCHECK_API_KEY"
ETAG=$(cat etag.txt)
rm etag.txt

curl -X PATCH https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID \
  -H "Authorization: Bearer $DNSCHECK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: $ETAG" \
  -d '{"values": ["20 mail.example.com."]}'

This uses the requests library (pip install requests):

import os

import requests

url = f"https://www.dnscheck.co/api/v2/dns-records/{os.environ['RECORD_ID']}"
headers = {"Authorization": f"Bearer {os.environ['DNSCHECK_API_KEY']}"}

current = requests.get(url, headers=headers, timeout=30)
current.raise_for_status()
response = requests.patch(
    url,
    headers={**headers, "If-Match": current.headers["ETag"]},
    json={"values": ["20 mail.example.com."]},
    timeout=30,
)
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()
record = response.json()
print(record["values"])

This needs Node.js 18 or later. Save it in a .mjs file, so top-level await works:

const url = `https://www.dnscheck.co/api/v2/dns-records/${process.env.RECORD_ID}`;
const auth = { Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}` };

const current = await fetch(url, { headers: auth, signal: AbortSignal.timeout(30000) });
if (!current.ok) throw new Error(`HTTP ${current.status} ${current.statusText}`);
const response = await fetch(url, {
  method: "PATCH",
  headers: { ...auth, "Content-Type": "application/json", "If-Match": current.headers.get("ETag") },
  body: JSON.stringify({ values: ["20 mail.example.com."] }),
  signal: AbortSignal.timeout(30000),
});
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 record = await response.json();
console.log(record.values);

Using only the standard library. Save it as update-record.go and run it with go run update-record.go:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

func main() {
	url := "https://www.dnscheck.co/api/v2/dns-records/" + os.Getenv("RECORD_ID")
	auth := "Bearer " + os.Getenv("DNSCHECK_API_KEY")
	client := &http.Client{Timeout: 30 * time.Second}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", auth)
	current, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	current.Body.Close()
	if current.StatusCode != http.StatusOK {
		log.Fatalf("HTTP %s", current.Status)
	}

	body, err := json.Marshal(map[string]any{"values": []string{"20 mail.example.com."}})
	if err != nil {
		log.Fatal(err)
	}
	req, err = http.NewRequest("PATCH", url, bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", auth)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("If-Match", current.Header.Get("ETag"))
	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 record struct {
		Values       []string
		Code, Detail string
		Errors       []struct{ Message string }
	}
	if err := json.NewDecoder(response.Body).Decode(&record); err != nil {
		log.Fatal(err)
	}
	if response.StatusCode >= 400 {
		fmt.Fprintf(os.Stderr, "%s: %s\n", record.Code, record.Detail)
		for _, e := range record.Errors {
			fmt.Fprintln(os.Stderr, e.Message)
		}
		os.Exit(1)
	}
	fmt.Println(record.Values)
}

The response includes the updated DNS record, a 200 status, and the new ETag. A change is checked as When Changes Are Checked describes.

Fields

Fields marked as settable can be sent when adding or updating a DNS record; the rest are set by DNS Check. To add a DNS record, you must send name, type, and values. The other settable fields are optional.

Field Type Description
id string The DNS record's UUID
object string Always "dns_record"
record_group_id string The ID of the record group it's in
name string Settable, required to add. The name to look up, returned fully qualified with a trailing dot. When you send one, the trailing dot is optional, and names are always absolute: www means www., and @ isn't accepted. Names are stored lowercase, internationalized names are converted to punycode, and for a PTR record, an IP address is converted to its reverse name.
type string Settable, required to add. One of "A", "AAAA", "ALIAS", "CAA", "CNAME", "HTTPS", "MX", "NS", "PTR", "SOA", "SPF", "SRV", "SVCB", and "TXT"
values array of strings Settable, required to add. The value expected, written as in a zone file: see Value Formats below. Exactly one, unless load_balancer is true, in which case it lists every acceptable value. A wildcard value such as * matches any value, and is returned as you sent it.
exclusive boolean Settable. Whether the records found for the name and type must match exactly the exclusive DNS records in their record group, so an extra or missing record fails the check. See Exclusive DNS Record Matching. It can't be combined with invert or load_balancer. Default: false.
invert boolean Settable, Professional and Enterprise. Whether the check passes when the value is not found. See Inverted DNS Checks. Default: false.
load_balancer boolean Settable, Professional and Enterprise. Whether values can list several alternatives, any of which may be returned, for A, AAAA, and CNAME records. See Load Balancer Records. Default: false.
status string passing, failing, or unknown, from its latest check. It's unknown until the first check. After a change, it's the old value's result until the DNS record is checked again (see When Changes Are Checked).
fail_count integer How many checks in a row have failed
error string or null Why its latest check failed, in the words the web interface uses, or null if it passed, if it hasn't been checked, or while check results are briefly unavailable
checked_at string or null When it was last checked, in UTC, or null before its first check, or while check results are briefly unavailable
created_at string When it was added, in UTC
updated_at string When it was last changed, in UTC. A check that changes its status or fail_count updates it too, so a later updated_at doesn't always mean an edit.
web_url string Its web page
api_url string Its URL in API v2
warnings array of strings Only in the response to adding or updating a DNS record: things worth knowing that didn't stop the change, such as an identical DNS record already being in the group, a single-label name like www, which is looked up as a top-level name, or the legacy SPF resource record type, which most domains don't publish

What you send may be adjusted the way the web interface adjusts it, such as an IPv6 address written in its canonical form, or a trailing dot added to a domain name. The response shows the stored values, which are exactly what's checked.

Value Formats

Each entry in values is the part of a zone file line after the name, class, TTL, and type:

Type Value Example
A An IPv4 address 192.0.2.1
AAAA An IPv6 address, in canonical form 2001:db8::1
ALIAS The target record type, then the target name A target.example.com.
CAA Flag, tag, then the quoted value 0 issue "letsencrypt.org"
CNAME The canonical name target.example.com.
HTTPS Priority, target, then any parameters 1 . alpn=h2,h3
MX Preference, then the mail exchange 10 mail.example.com.
NS The name server ns1.example.com.
PTR The name the address points to host.example.com.
SOA Primary name server, responsible mailbox, serial, refresh, retry, expire, and minimum TTL ns1.example.com. hostmaster.example.com. 2026101801 7200 3600 1209600 3600
SPF The policy text, in double quotes. This is the legacy SPF resource record type. An SPF policy published in a TXT record, as RFC 7208 requires, has type TXT "v=spf1 include:_spf.example.com ~all"
SRV Priority, weight, port, then the target 10 5 443 server.example.com.
SVCB Priority, target, then any parameters 1 . alpn=dot port=853
TXT The text, in double quotes "google-site-verification=abc123"

TXT and SPF values are returned in double quotes, with \" for any quote in the text. You can send them with or without the quotes. A long value, such as a DKIM key, can also be sent the way a zone file splits it, as several quoted strings, optionally in parentheses: "v=DKIM1; k=rsa; " "p=MIIB...". DNS joins the strings into one value with nothing between them, so it returns a single quoted string.

When Changes Are Checked

A DNS record added or changed through the API is checked right away, in the background. The response returns before the check runs, so in the response, a changed DNS record's status, error, and checked_at still reflect its last check, of the old value.

To wait for the result, either use webhooks or poll the DNS record until its checked_at is later than the one in the write's response. An on-demand check requested while the write's check is running, or within 3 seconds after it finishes, doesn't run a second check.

Checking a DNS Record on Demand

POST /api/v2/dns-records/{id}/check queues a check of the DNS record, which usually runs within a few seconds. It works like a scheduled check: its result becomes the DNS record's status, and notifications go out if the result calls for them. For an exclusive record, it also checks the other exclusive records sharing its name and type. It needs no body.

The response, with a 202 status, is the DNS record as it stood before the check. To get the result, poll the DNS record until its checked_at is later than the one in the response, then read its status and error. This checks the DNS record in $RECORD_ID and waits up to 30 seconds for the result:

STATUS=$(curl -s -o record.json -w '%{http_code}' -X POST \
  https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID/check \
  -H "Authorization: Bearer $DNSCHECK_API_KEY")
if [ "$STATUS" = 200 ] || [ "$STATUS" = 202 ]; then
  BEFORE=$(jq -r .checked_at record.json)
  CHECKED_AT=$BEFORE
  DEADLINE=$((SECONDS + 30))
  while [ "$STATUS" = 202 ]; do
    sleep 5
    POLL_STATUS=$(curl -s -o poll.json -w '%{http_code}' \
      https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID \
      -H "Authorization: Bearer $DNSCHECK_API_KEY")
    if [ "$POLL_STATUS" = 200 ]; then
      mv poll.json record.json
      CHECKED_AT=$(jq -r .checked_at record.json)
    fi
    [ "$CHECKED_AT" != "$BEFORE" ] || [ $SECONDS -ge $DEADLINE ] && break
  done

  if [ "$STATUS" = 202 ] && [ "$CHECKED_AT" = "$BEFORE" ]; then
    echo "The check hasn't run yet. Read the DNS record again in a minute."
  else
    jq '{status, error, checked_at}' record.json
  fi
else
  jq -r '"\(.code): \(.detail)"' record.json 2>/dev/null || echo "HTTP $STATUS"
fi
rm -f record.json poll.json

This uses the requests library (pip install requests):

import os
import time

import requests

url = f"https://www.dnscheck.co/api/v2/dns-records/{os.environ['RECORD_ID']}"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DNSCHECK_API_KEY']}"

response = session.post(f"{url}/check", timeout=30)
if response.headers.get("Content-Type", "").startswith("application/problem+json"):
    problem = response.json()
    print(f"{problem['code']}: {problem['detail']}")
    raise SystemExit(1)
response.raise_for_status()
record = response.json()
before = record["checked_at"]
if response.status_code == 202:
    deadline = time.monotonic() + 30
    while True:
        time.sleep(5)
        poll = session.get(url, timeout=30)
        poll.raise_for_status()
        record = poll.json()
        if record["checked_at"] != before or time.monotonic() >= deadline:
            break

if response.status_code == 202 and record["checked_at"] == before:
    print("The check hasn't run yet. Read the DNS record again in a minute.")
else:
    print(record["status"], record["error"], record["checked_at"])

This needs Node.js 18 or later. Save it in a .mjs file, so top-level await works:

import { setTimeout as sleep } from "node:timers/promises";

const url = `https://www.dnscheck.co/api/v2/dns-records/${process.env.RECORD_ID}`;
const headers = { Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}` };

const response = await fetch(`${url}/check`, {
  method: "POST",
  headers,
  signal: AbortSignal.timeout(30000),
});
if (response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
  const problem = await response.json();
  console.error(`${problem.code}: ${problem.detail}`);
  process.exit(1);
}
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
let record = await response.json();
const before = record.checked_at;
if (response.status === 202) {
  const deadline = Date.now() + 30000;
  while (true) {
    await sleep(5000);
    const poll = await fetch(url, { headers, signal: AbortSignal.timeout(30000) });
    if (!poll.ok) throw new Error(`HTTP ${poll.status} ${poll.statusText}`);
    record = await poll.json();
    if (record.checked_at !== before || Date.now() >= deadline) break;
  }
}

if (response.status === 202 && record.checked_at === before) {
  console.log("The check hasn't run yet. Read the DNS record again in a minute.");
} else {
  console.log(record.status, record.error, record.checked_at);
}

Using only the standard library. Save it as check-record.go and run it with go run check-record.go:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
)

type dnsRecord struct {
	Status    string
	Error     any
	CheckedAt any `json:"checked_at"`
}

var client = &http.Client{Timeout: 30 * time.Second}

// Sends a request with the key, and returns the DNS record and status code
func call(method, url string) (dnsRecord, int) {
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("DNSCHECK_API_KEY"))
	response, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()
	if response.StatusCode >= 400 {
		var problem struct{ Code, Detail string }
		if err := json.NewDecoder(response.Body).Decode(&problem); err != nil {
			log.Fatalf("HTTP %s", response.Status)
		}
		log.Fatalf("%s: %s", problem.Code, problem.Detail)
	}
	var record dnsRecord
	if err := json.NewDecoder(response.Body).Decode(&record); err != nil {
		log.Fatal(err)
	}
	return record, response.StatusCode
}

func main() {
	url := "https://www.dnscheck.co/api/v2/dns-records/" + os.Getenv("RECORD_ID")
	record, status := call("POST", url+"/check")
	before := record.CheckedAt
	deadline := time.Now().Add(30 * time.Second)
	for status == http.StatusAccepted {
		time.Sleep(5 * time.Second)
		record, _ = call("GET", url)
		if record.CheckedAt != before || time.Now().After(deadline) {
			break
		}
	}

	if status == http.StatusAccepted && record.CheckedAt == before {
		fmt.Println("The check hasn't run yet. Read the DNS record again in a minute.")
	} else {
		fmt.Println(record.Status, record.Error, record.CheckedAt)
	}
}

Each poll counts against the request budget, so wait a few seconds between them (see Rate Limits). Give up after a while, as the example does.

If another check of the DNS record is running when the request arrives, its result stands in for this one's. If one finished within the last 3 seconds, such as the check after a write, it is the result: nothing is queued, the check budget isn't charged, and the response is the DNS record with a 200 status, as the example handles.

With webhooks (Enterprise), you may not need to poll at all. A webhook is sent whenever a check changes a DNS record's status, from failing to passing or from passing to failing, usually within 5 seconds of the check. So an automation that has just updated a DNS record at its DNS host can request a check, then wait for the dns_record.passing webhook. If this check doesn't find the change yet, the record's scheduled checks keep running at its record group's check frequency (every 5 minutes by default), and the webhook arrives after the first check that does. Only a status change sends a webhook: a DNS record that was already passing, and still passes, sends nothing, so check the status in the response first. Webhooks are only sent for record groups with notifications turned on.

Plan Requirements

Setting invert or load_balancer to true on a Basic account gets a 403 with the code feature_not_in_plan. Adding a DNS record beyond the plan's limit gets a 403 with the code plan_limit_reached, and an expired account can still read and delete DNS records, but not add, change, or check them. See API Errors for every error.


DNS monitoring illustration

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