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 |
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 |
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 |
Read and write | Adds a DNS record, returning it with 201. |
GET /api |
Read | Gets one DNS record, including its latest check's status and, if it failed, why. |
PATCH /api |
Read and write | Changes the fields sent and leaves the rest as they are. |
DELETE /api |
Read and write | Deletes the DNS record and its events, returning 204. |
POST /api |
Read and write | Queues a check of the DNS record, returning 202. See Checking a DNS Record on Demand. |
GET /api |
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.
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