Getting Started with the DNS Check API

This page walks through a first session with the DNS Check API: creating a record group, adding a DNS record to it, reading its status and history, and cleaning up. Each step is shown in curl, Python, JavaScript, and Go: pick a language from any example's tabs, and the page shows that language throughout.

Before You Start

  1. Generate an API key with Read and write access.
  2. Export the key to an environment variable rather than saving it in your code:
    export DNSCHECK_API_KEY="dnscheck_..."
    

    In Windows PowerShell, use this instead:

    $env:DNSCHECK_API_KEY = "dnscheck_..."
    

Walkthrough

1. Set up a client

On Windows, run the commands in WSL or Git Bash. They're written for a Unix shell such as bash or zsh, so they won't work as written in PowerShell or Command Prompt.

The curl examples use jq to read IDs from responses, so the later steps can use them. Run jq --version to check that you have it. If you don't, install it with brew install jq on macOS, sudo apt install jq on Ubuntu, Debian, or WSL, or winget install jqlang.jq for Git Bash.

Using the requests library (pip install requests), start a file named getting-started.py with this helper, which sends the key and turns error responses into exceptions. Add each step's code after it, and run it with python getting-started.py:

import os
import time

import requests

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

def call(method, path, **kwargs):
    response = session.request(method, f"{API}{path}", timeout=30, **kwargs)
    if response.headers.get("Content-Type", "").startswith("application/problem+json"):
        problem = response.json()
        raise RuntimeError(f"{problem['code']}: {problem['detail']} {problem.get('errors', '')}")
    response.raise_for_status()
    return response

For Node.js 18 or later, which has fetch built in, start a file named getting-started.mjs (so top-level await works) with this helper, which sends the key and turns an error response into an exception. Add each step's code after it, and run it with node getting-started.mjs:

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

const API = "https://www.dnscheck.co/api/v2";

// Returns the parsed body, or null for a 204
async function call(method, path, body) {
  const response = await fetch(`${API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.DNSCHECK_API_KEY}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(30000),
  });
  if (response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
    const problem = await response.json();
    throw new Error(`${problem.code}: ${problem.detail} ${problem.errors ? JSON.stringify(problem.errors) : ""}`);
  }
  if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
  return response.status === 204 ? null : await response.json();
}

Using only the standard library, start a file named getting-started.go with this helper, which sends the key and stops the program with the error if a call fails. Add each step's code inside a main function after it, and run it with go run getting-started.go:

package main

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

const api = "https://www.dnscheck.co/api/v2"

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

// Returns the parsed body, or nil for a 204
func call(method, path string, body any) map[string]any {
	var payload bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&payload).Encode(body); err != nil {
			log.Fatal(err)
		}
	}
	req, err := http.NewRequest(method, api+path, &payload)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("DNSCHECK_API_KEY"))
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	response, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()
	if strings.HasPrefix(response.Header.Get("Content-Type"), "application/problem+json") {
		var problem map[string]any
		if err := json.NewDecoder(response.Body).Decode(&problem); err != nil {
			log.Fatal(err)
		}
		if errors, ok := problem["errors"]; ok {
			log.Fatalf("%v: %v %v", problem["code"], problem["detail"], errors)
		}
		log.Fatalf("%v: %v", problem["code"], problem["detail"])
	}
	if response.StatusCode >= 400 {
		log.Fatalf("HTTP %s", response.Status)
	}
	if response.StatusCode == http.StatusNoContent {
		return nil
	}
	var data map[string]any
	if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
		log.Fatal(err)
	}
	return data
}

2. Create a record group

GROUP_ID=$(curl -s https://www.dnscheck.co/api/v2/record-groups \
  -H "Authorization: Bearer $DNSCHECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Mail", "description": "SPF policy"}' \
  | jq -r '.id // error(.detail)')
echo "Record group ID: $GROUP_ID"

This saves the new record group's id in $GROUP_ID for the later steps. If the request fails, jq prints the error instead of an ID.

group = call(
    "POST", "/record-groups", json={"name": "Mail", "description": "SPF policy"}
).json()
print(group["id"])
const group = await call("POST", "/record-groups", {
  name: "Mail",
  description: "SPF policy",
});
console.log(group.id);
group := call("POST", "/record-groups",
	map[string]any{"name": "Mail", "description": "SPF policy"})
fmt.Println(group["id"])

In production code, send an Idempotency-Key with each POST, so a retry can't create a second record group: see Idempotency Keys.

The response is the new record group, with a 201 status. Its id is how the later steps refer to it:

{
  "id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
  "object": "record_group",
  "name": "Mail",
  "description": "SPF policy",
  "name_servers": [],
  "check_frequency": 300,
  "public": true,
  "notifications": true,
  "status": "unknown",
  "records": { "total": 0, "passing": 0, "failing": 0, "unknown": 0 },
  "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",
  "api_url": "https://www.dnscheck.co/api/v2/record-groups/b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a"
}

A new record group is public by default, to make it easy to share and troubleshoot: anyone with its URL can view it and check its DNS records without logging in. Setting public to false makes it private.

3. Add a DNS record to monitor

RECORD_ID=$(curl -s 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": "TXT", "values": ["v=spf1 -all"]}' \
  | jq -r '.id // error(.detail)')
echo "DNS record ID: $RECORD_ID"

This saves the new DNS record's id in $RECORD_ID.

record = call(
    "POST",
    f"/record-groups/{group['id']}/dns-records",
    json={"name": "example.com", "type": "TXT", "values": ["v=spf1 -all"]},
).json()
print(record["id"], record["status"])
const record = await call("POST", `/record-groups/${group.id}/dns-records`, {
  name: "example.com",
  type: "TXT",
  values: ["v=spf1 -all"],
});
console.log(record.id, record.status);
record := call("POST", fmt.Sprintf("/record-groups/%v/dns-records", group["id"]),
	map[string]any{"name": "example.com", "type": "TXT", "values": []string{"v=spf1 -all"}})
fmt.Println(record["id"], record["status"])

This monitors example.com's SPF policy, which says no server may send mail for it. To monitor your own domain, use its name and the value it publishes. The value in values is written as it would appear in a zone file, and the DNS Records API page gives the format for each record type. The response is the new DNS record, with a 201 status, and its own id. It returns a TXT value in double quotes:

{
  "id": "6f1c9e2d-0b8a-4f7e-8d6c-5b4a3f2e1d0c",
  "object": "dns_record",
  "record_group_id": "b2e4c0a1-5d7f-4c3e-9a8b-1f2e3d4c5b6a",
  "name": "example.com.",
  "type": "TXT",
  "values": ["\"v=spf1 -all\""],
  "exclusive": false,
  "invert": false,
  "load_balancer": false,
  "status": "unknown",
  "fail_count": 0,
  "error": null,
  "checked_at": null,
  "created_at": "2026-10-23T14:05:01Z",
  "updated_at": "2026-10-23T14:05:01Z",
  "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",
  "warnings": []
}

4. Read its status and history

A new DNS record's first check runs in the background and usually finishes within a few seconds. Until then, its status is unknown, its checked_at is null, and its history is empty. This waits up to 30 seconds for that check, then reads the DNS record's status and history. If the check hasn't run by then, it says so instead:

DEADLINE=$((SECONDS + 30))
while true; do
  sleep 5
  RECORD=$(curl -s https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID \
    -H "Authorization: Bearer $DNSCHECK_API_KEY")
  CHECKED_AT=$(jq -r .checked_at <<< "$RECORD")
  [ "$CHECKED_AT" != null ] || [ $SECONDS -ge $DEADLINE ] && break
done

if [ "$CHECKED_AT" = null ]; then
  echo "The first check hasn't run yet. Try again in a minute."
else
  jq . <<< "$RECORD"

  curl -s https://www.dnscheck.co/api/v2/dns-records/$RECORD_ID/events \
    -H "Authorization: Bearer $DNSCHECK_API_KEY" | jq .
fi
deadline = time.monotonic() + 30
while True:
    time.sleep(5)
    current = call("GET", f"/dns-records/{record['id']}").json()
    if current["checked_at"] or time.monotonic() >= deadline:
        break

if current["checked_at"]:
    print(current["status"], current["error"])
    events = call("GET", f"/dns-records/{record['id']}/events").json()
    for event in events["data"]:
        print(event["timestamp"], event["type"])
else:
    print("The first check hasn't run yet. Try again in a minute.")
const deadline = Date.now() + 30000;
let current;
while (true) {
  await sleep(5000);
  current = await call("GET", `/dns-records/${record.id}`);
  if (current.checked_at || Date.now() >= deadline) break;
}

if (current.checked_at) {
  console.log(current.status, current.error);
  const events = await call("GET", `/dns-records/${record.id}/events`);
  for (const event of events.data) console.log(event.timestamp, event.type);
} else {
  console.log("The first check hasn't run yet. Try again in a minute.");
}
deadline := time.Now().Add(30 * time.Second)
var current map[string]any
for {
	time.Sleep(5 * time.Second)
	current = call("GET", fmt.Sprintf("/dns-records/%v", record["id"]), nil)
	if current["checked_at"] != nil || time.Now().After(deadline) {
		break
	}
}

if current["checked_at"] != nil {
	fmt.Println(current["status"], current["error"])
	events := call("GET", fmt.Sprintf("/dns-records/%v/events", record["id"]), nil)
	for _, item := range events["data"].([]any) {
		event := item.(map[string]any)
		fmt.Println(event["timestamp"], event["type"])
	}
} else {
	fmt.Println("The first check hasn't run yet. Try again in a minute.")
}

The DNS record is the same one as in step 3, now with its status and checked_at from the first check, which should pass. If a check fails, error says why. Its history lists the times its status changed, newest first, so here it holds one dns_record.passing event. The Events API page shows what an event looks like. To check a DNS record again later, see Checking a DNS Record on Demand. To follow every record on the account instead, read the event feed, or, to hear about each change as soon as possible, have webhooks push it to you (Enterprise).

5. Clean up

curl -X DELETE https://www.dnscheck.co/api/v2/record-groups/$GROUP_ID \
  -H "Authorization: Bearer $DNSCHECK_API_KEY"
call("DELETE", f"/record-groups/{group['id']}")
await call("DELETE", `/record-groups/${group.id}`);
call("DELETE", fmt.Sprintf("/record-groups/%v", group["id"]), nil)

Deleting a record group deletes the DNS records in it, and returns 204 with no body.

Next Steps


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