DNS Record Change Monitor
Pricing
from $1.00 / 1,000 domain checkeds
DNS Record Change Monitor
Resolve A, AAAA, MX, NS, TXT, CNAME, CAA, SPF and DMARC records for a list of domains and report what changed since the last run, with a severity for NS, MX, SPF and DMARC changes. Built for daily schedules.
Pricing
from $1.00 / 1,000 domain checkeds
Rating
0.0
(0)
Developer
Matias Isnard
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Give it a list of domains. On each run it resolves A, AAAA, MX, NS, TXT, CNAME and CAA records, plus the SPF record and the DMARC record, and compares them with the records it saw on the previous run. You get the current records, whether anything changed, and for each changed record type which values were added and which were removed.
Changes that matter for security get a higher severity: a changed nameserver or mail server, or an SPF or DMARC record that changed or disappeared. Run it on a daily schedule and get an email or a Slack message when changed is true.
Who it is for
- Agencies, MSPs and IT teams who manage domains for clients and want to know when someone touches the DNS.
- Security teams watching for nameserver hijacks, mail rerouting or a removed DMARC policy.
- Anyone who wants a record of when DNS changed, without running their own cron job.
What it checks
| Type | Queried name | Notes |
|---|---|---|
| A, AAAA | the domain | IPv4 and IPv6 addresses. See rotation below. |
| MX | the domain | Priority and mail server, for example 10 aspmx.l.google.com. |
| NS | the domain | Nameservers. |
| TXT | the domain | All TXT records, including verification tokens. |
| CNAME | the domain | Only a CNAME on that exact name. A domain apex normally has none. |
| CAA | the domain | Which certificate authorities may issue certificates. |
| SPF | the domain | The TXT records that start with v=spf1. |
| DMARC | _dmarc.<domain> | The TXT records that start with v=DMARC1. |
Queries go to the public resolvers 1.1.1.1 and 8.8.8.8, so the result is what the rest of the internet sees.
Records are normalized before they are compared: sorted, duplicates removed, trailing dots removed, and TTLs ignored. Hostnames are lowercased. TXT, SPF and DMARC content keeps its case, because TXT data is case-sensitive (a verification token with a changed letter is a different token).
Change detection
After each check the records are saved in a named key-value store called dns-change-monitor-state in your account, one entry per domain. On the next run they are compared and each changed type is listed:
{ "type": "NS", "added": ["ns1.other-dns.example"], "removed": ["ns-421.awsdns-52.com"], "severity": "high" }
| Severity | When |
|---|---|
critical | SPF or DMARC was removed (records only removed, none added). |
high | NS, MX, SPF or DMARC changed. |
medium | CAA changed. |
low | A, AAAA, TXT or CNAME changed. |
none | Nothing changed. |
The item's severity is the highest one among its changes.
firstRunis true when there was no snapshot for the domain yet. Nothing is reported as changed on the first run.- A record type you add to the input later has no snapshot yet, so it is not reported as changed on that run either.
- If a query fails (timeout or server failure), that type is not compared, the error is listed in
errors, and the old values are kept for the next run. A failed query is never reported as removed records. - A domain that does not exist (NXDOMAIN) is reported with
status: "nxdomain". Its last snapshot is kept.
Round-robin and geo DNS
Many large sites return a different subset of IP addresses on each query. Without care, that shows up as a change every day. With ignoreARecordRotation on (the default), when the A or AAAA set differs from the snapshot the Actor queries it a second time. If the second answer matches the snapshot, the difference is treated as rotation and not reported. If it still differs, the union of both answers is compared with the snapshot and stored. Turn it off if you want every difference reported.
Separate tasks
The store is shared by all runs of this Actor in your account. If two tasks watch the same domain (for example with different record types), give each one its own stateKey so they keep separate snapshots. To start over, delete the store in Storage > Key-value stores, or use a new stateKey.
Input
{"domains": ["github.com", "google.com", "mercadolibre.com.ar"],"recordTypes": ["A", "AAAA", "MX", "NS", "TXT", "CNAME", "CAA", "SPF", "DMARC"],"ignoreARecordRotation": true,"stateKey": "client-a"}
domains: domains or hostnames. URLs are reduced to the hostname. Subdomains such aswww.example.comare checked as given. Invalid lines are skipped with a warning and not charged. Duplicates are checked once.recordTypes(default: all nine).ignoreARecordRotation(default true).stateKey(optional).
Output
One dataset item per domain. Example (TXT records shortened):
{"input": "github.com","domain": "github.com","checkedAt": "2026-10-30T03:59:50Z","previousCheckedAt": "2026-10-29T03:59:49Z","firstRun": false,"status": "ok","records": {"A": ["4.228.31.150"],"AAAA": [],"MX": ["0 github-com.mail.protection.outlook.com"],"NS": ["dns1.p08.nsone.net", "dns2.p08.nsone.net", "ns-1283.awsdns-32.org", "ns-421.awsdns-52.com"],"TXT": ["MS=ms44452932", "v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com ~all"],"CNAME": [],"CAA": ["0 issue \"digicert.com\"", "0 issue \"letsencrypt.org\""],"SPF": ["v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com ~all"],"DMARC": ["v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:dmarc@github.com"]},"changed": true,"changes": [{ "type": "NS", "added": ["ns-520.awsdns-01.net"], "removed": [], "severity": "high" }],"severity": "high","errors": []}
status is ok when at least one record type was resolved, nxdomain when the domain does not exist, and error when every query failed.
Daily schedule and alerts
- Open the Actor, fill in your domains and click Save as a new task.
- Go to Schedules > Create new, select the task and set a cron expression, for example
0 7 * * *(every day at 07:00). Choose your time zone. - Alerts only when something changed. In the task, open Integrations and add a webhook (or Make, Zapier or n8n) on the event Run succeeded. In that flow, read the run's dataset and send a message only for items where
changedis true.
A small script that does the same with an email or Slack message, run after the schedule:
# Items of the last run of the task where changed is truecurl -s "https://api.apify.com/v2/actor-tasks/<username>~<task-name>/runs/last/dataset/items?token=<API_TOKEN>" \| jq '[.[] | select(.changed == true) | {domain, severity, changes}]' > changes.json# Slack: post to an incoming webhook if there is anythingif [ "$(jq length changes.json)" -gt 0 ]; thenjq -n --rawfile t changes.json '{text: ("DNS changes:\n" + $t)}' \| curl -s -X POST -H "Content-Type: application/json" -d @- "https://hooks.slack.com/services/XXX/YYY/ZZZ"fi# Email: same check, then for example# mail -s "DNS changes" you@example.com < changes.json
To alert only on security-relevant changes, use select(.severity == "high" or .severity == "critical").
Pricing
Pay per event:
domain-checked: once per domain that was resolved. Domains that do not exist (NXDOMAIN), invalid input and domains where every query failed are not charged.change-detected: once per domain with at least one change, no matter how many records changed. Never charged on the first run of a domain.
The prices are shown on the Actor page. You can cap the cost of a run with the "Maximum cost per run" setting; the Actor stops cleanly when the cap is reached.
Limitations
- Results are what the public resolvers return at the time of the run. A change made at your DNS provider can take up to the record's TTL to show up.
- Sites with large geo-distributed pools can still produce A/AAAA changes when the pool really moves. Rotation filtering reduces noise, it does not remove it fully.
- Only the apex TXT and
_dmarcTXT are read. DKIM selectors and other subdomains are not checked unless you add them as their own domains (TXT is then compared for that name). - DNSSEC is not validated.
Run locally
python -m venv .venv.venv/Scripts/pip install -r requirements.txt # on Linux/macOS: .venv/bin/pip# put input in storage/key_value_stores/default/INPUT.json.venv/Scripts/python -m my_actorpython -m unittest discover -s tests -t .
Author: Matias Isnard.