Each time a check changes a DNS record's status, DNS Check records an event. The API lists them in two ways: the account's whole feed, oldest first, which a script can poll to follow every change, and one DNS record's history, newest first. Each event is the same object a webhook delivers as a version 2 envelope, so the feed also lets a webhook endpoint catch up on deliveries it missed.
If you want to know about changes as fast as possible, webhooks (Enterprise) push each event to you, usually within 5 seconds of it happening, which is faster on average than polling the feed.
Endpoints
| Request | Required access | What it does |
|---|---|---|
GET /api |
Read | The account's events, oldest first, from a cursor, or from a time given as since |
GET /api |
Read | One DNS record's events, newest first |
Both reach back as far as the account's plan keeps notification history: 7 days on Basic, 90 days on Professional, and 365 days on Enterprise.
Reading Events
This reads the account's oldest event, one per page (limit=1). It needs your API key in $DNSCHECK_API_KEY (see Getting Started):
curl "https://www.dnscheck.co/api/v2/events?limit=1" \
-H "Authorization: Bearer $DNSCHECK_API_KEY"
The response is a page holding that one event, with a 200 status. has_more is true because more events follow: pass next_cursor back as cursor to read the next one, as Following the Feed describes:
{
"data": [
{
"id": "evt_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f",
"object": "event",
"type": "dns_record.failing",
"timestamp": "2026-10-23T14:05:00Z",
"notified": true,
"text": "The example.com MX record in the \"Mail\" record group is failing: https://www.dnscheck.co/tests/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a/12345",
"data": {
"dns_record": {
"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."],
"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"
},
"record_group": {
"id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
"object": "record_group",
"name": "Mail",
"web_url": "https://www.dnscheck.co/tests/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
"api_url": "https://www.dnscheck.co/api/v2/record-groups/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a"
},
"status": "failing",
"error": "Expected:\nexample.com. MX 10 mail.example.com.\n\nFound:\nexample.com. MX 20 old-mail.example.net.",
"responding_name_server": "192.0.2.53",
"history_url": "https://www.dnscheck.co/tests/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a/12345/history"
}
}
],
"next_cursor": "q-0VxqrpuI3nmZBxnUYuuA",
"has_more": true
}
Fields
A check creates an event when it changes a DNS record's status to passing or failing, including the first result for a new record. Checks that leave the status as it was create none, with the one exception described under type.
| Field | Type | Description |
|---|---|---|
id |
string | evt_ followed by 32 hexadecimal characters, the same as the webhook-id of the webhook that delivered it. |
object |
string | Always "event" |
type |
string | dns_record.failing or dns_record.passing. They usually alternate. The exception is a ServFail failure when your ServFail error notification threshold is above 1 (the default is 2): notifications are held back, and if the record is still failing when the hold ends, a second dns_record.failing event follows the first, and it's the second event that sends notifications. |
timestamp |
string | When the check that changed the status ran, in UTC |
notified |
boolean or null | Whether the event sent notifications (see Events and Notifications). null on events from before DNS Check recorded it. |
text |
string | A sentence describing the event, ending with a link to the DNS record, as every webhook payload format carries it. It describes the record as it is now, as data.dns_record does. |
data.dns_record |
object | The DNS record's id, object, record_group_id, name, type, values, web_url, and api_url, as in the DNS Records API. They describe the record as it is now, not when the event happened, and leave out its current status: follow api_url for that. |
data.record_group |
object | The record group's id, object, name, web_url, and api_url, as in the Record Groups API, also as it is now |
data.status |
string | failing or passing, matching type |
data.error |
string or null | Why the check failed, in the words the record's History page uses. It's plain text, sometimes over several lines, so display it rather than parsing it. On a passing event, it's usually null, but it can carry a note, such as that the record is flapping. |
data.responding_name_server |
string or null | The name server whose answer the check evaluated: its IP address, followed by its hostname in parentheses when it has one |
data.history_url |
string | The record's History page in DNS Check |
Following the Feed
Start without a cursor to read from the oldest event the plan keeps. Then keep passing next_cursor back as cursor.
To start from a point in time instead, such as when your endpoint or script went down, pass it as since, an RFC 3339 date and time with a time zone, such as 2026-10-23T14:05:00Z. The first page holds the events from that time on, and you keep following next_cursor from there. Pick a time a little before the trouble started, and skip the event IDs you've already handled, so you don't miss anything or process anything twice. A since earlier than the plan keeps starts from the oldest event it keeps, and one in the future is refused.
Unlike other lists, the feed's next_cursor is never null, since new events keep arriving: when has_more is false, you're caught up, so keep the cursor and ask again with it next time.
This script catches up on the feed, then exits. It handles each event (here, by printing it), saves the cursor to events-cursor.txt after each page so the next run picks up where this one left off, and waits out a 429's Retry-After. With your API key in $DNSCHECK_API_KEY (see Getting Started), replace handle with your own processing:
This uses the requests library (pip install requests).
import os
import time
from pathlib import Path
import requests
API = "https://www.dnscheck.co/api/v2"
CURSOR_FILE = Path("events-cursor.txt")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DNSCHECK_API_KEY']}"
def handle(event):
record = event["data"]["dns_record"]
print(event["timestamp"], event["type"], record["name"], record["type"])
cursor = CURSOR_FILE.read_text().strip() if CURSOR_FILE.exists() else None
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
response = session.get(f"{API}/events", params=params, timeout=30)
if response.status_code == 429:
time.sleep(int(response.headers["Retry-After"]))
continue
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 event in page["data"]:
handle(event)
cursor = page["next_cursor"]
CURSOR_FILE.write_text(cursor)
if not page["has_more"]:
break
This needs Node.js 18 or later. Save it as read-events.mjs (so top-level await works) and run it with node read-events.mjs.
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
const API = "https://www.dnscheck.co/api/v2";
const CURSOR_FILE = "events-cursor.txt";
function handle(event) {
const record = event.data.dns_record;
console.log(event.timestamp, event.type, record.name, record.type);
}
let cursor = existsSync(CURSOR_FILE) ? readFileSync(CURSOR_FILE, "utf8").trim() : null;
while (true) {
const params = new URLSearchParams({ limit: "200" });
if (cursor) params.set("cursor", cursor);
const response = await fetch(`${API}/events?${params}`, {
headers: { Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}` },
signal: AbortSignal.timeout(30000),
});
if (response.status === 429) {
await sleep(Number(response.headers.get("Retry-After")) * 1000);
continue;
}
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 event of page.data) handle(event);
cursor = page.next_cursor;
writeFileSync(CURSOR_FILE, cursor);
if (!page.has_more) break;
}
Using only the standard library. Save it as read-events.go and run it with go run read-events.go.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const api = "https://www.dnscheck.co/api/v2"
const cursorFile = "events-cursor.txt"
type event struct {
Timestamp, Type string
Data struct {
DNSRecord struct{ Name, Type string } `json:"dns_record"`
}
}
func handle(e event) {
fmt.Println(e.Timestamp, e.Type, e.Data.DNSRecord.Name, e.Data.DNSRecord.Type)
}
func main() {
client := &http.Client{Timeout: 30 * time.Second}
saved, _ := os.ReadFile(cursorFile)
cursor := strings.TrimSpace(string(saved))
for {
params := url.Values{"limit": {"200"}}
if cursor != "" {
params.Set("cursor", cursor)
}
req, err := http.NewRequest("GET", api+"/events?"+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)
}
body, err := io.ReadAll(response.Body)
response.Body.Close()
if err != nil {
log.Fatal(err)
}
if response.StatusCode == http.StatusTooManyRequests {
seconds, _ := strconv.Atoi(response.Header.Get("Retry-After"))
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
if response.StatusCode >= 400 {
if !strings.HasPrefix(response.Header.Get("Content-Type"), "application/problem+json") {
log.Fatalf("HTTP %s", response.Status)
}
var problem struct{ Code, Detail string }
if err := json.Unmarshal(body, &problem); err != nil {
log.Fatal(err)
}
log.Fatalf("%s: %s", problem.Code, problem.Detail)
}
var page struct {
Data []event
HasMore bool `json:"has_more"`
NextCursor string `json:"next_cursor"`
}
if err := json.Unmarshal(body, &page); err != nil {
log.Fatal(err)
}
for _, e := range page.Data {
handle(e)
}
cursor = page.NextCursor
if err := os.WriteFile(cursorFile, []byte(cursor), 0o644); err != nil {
log.Fatal(err)
}
if !page.HasMore {
break
}
}
}
Run it on a schedule, such as hourly with cron, or have webhooks (Enterprise) push each change to you instead.
If the script stops partway through a page, that page's events are handled again on the next run, so make handling safe to repeat, such as by skipping event IDs you've already handled.
A DNS record that changes state very often keeps about its newest 1,000 events. Deleting a DNS record deletes its events.
Events and Notifications
Not every event sends notifications: none go out when the record group has notifications turned off, for a new record's first pass, or while a ServFail failure is held back. The notified field indicates whether one did, and webhooks deliver only events where it's true. The webhooks page explains how events and notifications relate in full.
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