Blacklist Monitoring: Scripts, an API and Webhook Alerts
Blacklist monitoring means querying several DNS blocklists (DNSBLs) for your sending IPs and domains on a schedule, and raising an alert only when a result changes. Catching a listing when it happens, rather than when customers report bounced mail, gives you time to find the cause and request removal before the damage spreads.
There are two practical ways to do it: run a small dig and cron script yourself, or hand the checks to a service through an API and webhooks. Either way, two details decide whether the monitor tells the truth: queries sent through public resolvers are refused by lists such as Spamhaus and produce misleading answers, and error codes must never be counted as listings.
The list operators' rules quoted here are based on their official pages as of November 2026. Policies change, so confirm them before you rely on a setup. For background on how DNSBLs work, see what a DNSBL (RBL) is.
Decide what to watch and which lists to query
Start by writing down the objects you actually need to watch.
- Sending IP addresses. These are the addresses your mail server or email provider really sends from. Your website and your mail often leave from different machines, so confirm the address in the Received headers of a message you sent. If you send over IPv6, monitor those addresses too.
- Domains. The domain in your From address and any domains that appear in links inside your messages. Domains are checked against domain blocklists, which are separate from IP lists.
The query format is defined in RFC 5782. For IPv4 you reverse the octets and append the list's zone, so 192.0.2.99 on a list becomes 99.2.0.192 followed by the zone name. For IPv6 the address is written as 32 hex nibbles in reverse order. A domain is simply prefixed to the zone. A listed entry returns an A record, conventionally in 127.0.0.0/8; an unlisted one returns NXDOMAIN. The RFC also notes that lists should publish a TXT record explaining the reason for a listing.
Watch several lists rather than one, because receivers do not all use the same ones. What each return code means varies by operator; our guide to DNSBL return codes covers the common ones.
Why public resolvers give wrong answers
A common mistake in home-grown monitoring is sending queries through a public resolver such as Google Public DNS (8.8.8.8) or Cloudflare (1.1.1.1). Spamhaus's official DNSBL usage FAQ explains what happens:
- Free use of the Spamhaus DNSBLs requires that the use is non-commercial and that queries do not come from a public or open resolver, or from an IP with generic reverse DNS.
- Through free open resolvers such as Google Public DNS, queries will in most cases get a not-listed (NXDOMAIN) reply. In other words, a listed IP can look clean.
- Queries via a public resolver can also receive 127.255.255.254. That is an error code, not a listing. Likewise 127.255.255.252 means a typo in the DNSBL name, and 127.255.255.255 means an excessive number of queries. Spamhaus states these codes must not be taken to mean the queried object is listed.
- Resolvers that rewrite NXDOMAIN answers return an IP address instead, so you never see the correct NXDOMAIN.
The FAQ's top recommendation is to run your own DNS resolver. For users who must stay on a public resolver, Spamhaus points to its Data Query Service (DQS); check its terms for what is free. The FAQ suggests dig txt whoami.fastly.net to see which resolver you are actually using.
RFC 5782 adds a built-in sanity check: an IPv4 list must contain an entry for 127.0.0.2 and must not contain 127.0.0.1, and clients should periodically check these test entries to confirm the list is working. In Spamhaus's example, 2.0.0.127.zen.spamhaus.org returns 127.0.0.10, 127.0.0.4 and 127.0.0.2, while 1.0.0.127.zen.spamhaus.org returns NXDOMAIN. A monitor should test this first and discard the run if it fails.
Option 1: a dig and cron script
Here is a minimal script that checks one IPv4 address against Spamhaus ZEN and SpamCop (bl.spamcop.net, which SpamCop documents as answering 127.0.0.2 for a listed IP). Replace the address and zones with your own.
#!/bin/sh
# Query your own recursive resolver (e.g. unbound on the same host), never a public one
RESOLVER=127.0.0.1
IP=192.0.2.10
ZONES="zen.spamhaus.org bl.spamcop.net"
REV=$(echo "$IP" | awk -F. '{print $4"."$3"."$2"."$1}')
q() {
out=$(dig +time=3 +tries=1 @"$RESOLVER" "$1" A) || { echo ERR; return; }
case "$out" in
*"status: NOERROR"*) a=$(printf '%s\n' "$out" | awk '$4=="A"{print $5}'); echo "${a:-NX}" ;;
*"status: NXDOMAIN"*) echo NX ;;
*) echo ERR ;;
esac
}
for Z in $ZONES; do
# If the 127.0.0.2 test entry does not resolve to a listing code, do not trust this run
case "$(q "2.0.0.127.$Z")" in
127.255.255.*|NX|ERR) echo "$Z ERROR test-entry"; continue ;;
127.*) ;;
*) echo "$Z ERROR test-entry"; continue ;;
esac
A=$(q "$REV.$Z")
case "$A" in
NX) echo "$Z CLEAN" ;;
ERR) echo "$Z ERROR no-answer" ;;
127.255.255.*) echo "$Z ERROR $A" ;;
127.*) echo "$Z LISTED $A" ;;
*) echo "$Z ERROR unexpected $A" ;;
esac
doneWhat the script is doing:
- Only an NXDOMAIN status counts as clean, 127.255.255.x is an error, and any other 127.x is a listing. Anything outside 127.0.0.0/8 is treated as an anomaly such as a rewriting resolver.
- A timeout makes dig exit non-zero, while SERVFAIL and REFUSED exit 0. None of them is treated as clean; the script records an error.
- Return codes are list-specific. Some lists use 127.0.0.1 to mean that a query was refused, for example, so read the documentation for each list you add and extend the case statement where needed.
Then let cron compare each run with the previous one and send mail only when something changed:
# Example: run hourly, send mail only when the output differs from the last run
0 * * * * /usr/local/bin/dnsbl-watch.sh > /var/tmp/dnsbl.now 2>&1; cmp -s /var/tmp/dnsbl.now /var/tmp/dnsbl.prev || mail -s "DNSBL status changed" you@example.com < /var/tmp/dnsbl.now; mv /var/tmp/dnsbl.now /var/tmp/dnsbl.prevThe hourly interval is only an example. Spamhaus offers free access for low-volume, non-commercial use, and clients that exceed its query volume threshold need a paid option; check each list's terms as your number of monitored IPs grows. The FAQ also advises trying commands a few times to confirm the results stay the same.
Option 2: the DNSBL Checker batch API and webhooks
If you would rather not maintain a resolver and per-list parsing rules, you can let a service run the checks. Below is the v1 API of this site, limited to what the server implementation actually does. The reference lives at /en/api-docs and keys are issued on the members page at /account. Sign-up needs only an email address, and you log in with a link sent by email.
Authentication and limits
- Keys start with
dnsbl_and are shown once, right after issue; only a hash is stored. Re-issuing a key invalidates the previous one. - Send the key in the
X-API-Keyheader on every request. HTTPS only; plain http:// is redirected to https://, so call https:// from the start. - Limits are 1,000 checks per day (one target counts as one), 100 targets per batch, and 50 monitor registrations. Over the daily limit you get
429withdaily_limit_exceededand aresets_attimestamp.
Batch checks
Send a targets array of domain names or IP addresses to POST /api/v1/batch:
curl -X POST https://dnsblchecker.com/api/v1/batch \
-H "X-API-Key: $DNSBL_API_KEY" -H "Content-Type: application/json" \
-d '{"targets":["example.com"]}'If any target still needs checking, the response is 202 with a batch_id. Poll GET /api/v1/batch/{batch_id} every few seconds until status is done (abridged example; the numbers and list name are illustrative, not a real result):
{
"batch_id": 1, "status": "done", "created": "2026-11-03 09:10:41", "count": 1, "pending": 0,
"results": [
{
"target": "example.com", "type": "domain",
"status": "done", "verdict": "listed",
"checked_at": "2026-11-03 09:12",
"lists_checked": 80, "listed_count": 1, "error_count": 4,
"listed_on": [
{ "list": "ZapBL RHSBL", "zone": "rhsbl.zapbl.net", "response": "127.0.0.2" }
]
}
]
}verdictislistedif at least one list reports a listing, otherwiseclean. Lists that timed out, or answered with an error code such as 127.255.255.x, are counted inerror_countand never as listings.- A batch returns a stored result if one exists from the last 30 days. For scheduled monitoring, always look at
checked_at. - Malformed targets come back in
rejectedwith a reason and are not counted. If you pass a URL, only the host name is used. Documentation-only ranges such as 192.0.2.0/24 are rejected, so test with a real sending IP.
Webhook monitoring
Register a target and a callback_url with POST /api/v1/monitors. Each registered target is then checked once a day without using the cache (the first check runs within about a day of registering), and a JSON POST is sent to your URL only on the first check (monitor.initial) and when the verdict flips between listed and clean (monitor.status_changed).
curl -X POST https://dnsblchecker.com/api/v1/monitors \
-H "X-API-Key: $DNSBL_API_KEY" -H "Content-Type: application/json" \
-d '{"target":"example.com","callback_url":"https://your-app.example/hooks/dnsbl"}'- The
signing_secretin the response is shown only once. Each delivery carries anX-DNSBL-Signature: sha256=...header, the HMAC-SHA256 of the raw body keyed with that secret. Verify it on your side. callback_urlmust be https on port 443 and resolve to a public IP; redirects are not followed, and the timeout is 10 seconds, so queue heavy work and answer quickly.- If your endpoint returns anything other than
2xx, or the connection fails, delivery is retried at the next daily check. The reason appears aslast_delivery_errorinGET /api/v1/monitors, andDELETE /api/v1/monitors/{monitor_id}removes a monitor.
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers.get("X-DNSBL-Signature", ""))Designing alerts: frequency, multiple lists and false alarms
A monitor is only useful if people still read its alerts after a few months. One that fires constantly soon gets ignored.
Frequency
- Separate how often you query from how often you notify. Query on a schedule, but notify only on a change of state. A daily all-clear message buries the one alert that matters.
- RFC 5782 says list TTLs should reflect how fast the list changes, and that lists of spam sources may use TTLs of minutes. Choose your interval knowing that some listings appear and expire quickly.
- Heavy querying can breach a list's terms of use or earn you 127.255.255.255 (excessive queries) instead of real answers.
Several lists
- Include the name of the list in every alert. Lists differ in how many receivers use them, so you might alert immediately for the lists that matter most to your recipients and batch the rest into a digest.
- The webhook above fires only when the verdict flips between listed and clean. If a target is already listed and then appears on a second list, no event is sent. Watch
last_listed_countinGET /api/v1/monitors, or run a batch and readlisted_on, to catch that case.
False positives and transient answers
- Record errors separately from listings. Spamhaus warns operators not to reject or filter mail on its error codes; the same logic applies to monitoring. A timeout or a 127.255.255.x answer should never trigger a listing alert.
- To avoid reacting to a single odd result, alert only after two consecutive identical results, or re-query a few minutes later before sending anything.
- If errors persist, suspect a resolver change or a change on the list's side, and look at the test-entry result first.
- A residential IP on the Spamhaus PBL is a policy listing, not an accusation: it means the address should not send mail directly. Decide per list what counts as an alert (see Spamhaus removal).
What to do when a listing is detected
- Confirm it. Check the operator's own lookup page and note the list name, the time and any reason shown.
- Fix the cause first. Look for compromised accounts, abused web forms, infected machines, poor list hygiene and mismatched reverse DNS (PTR) or HELO names. Requesting removal while the cause remains usually leads to a new listing.
- Follow that list's removal process. The decision to delist belongs to each list operator, and filing a request is no promise that the listing will be lifted. If you do not control the IP, as with shared hosting, contact your hosting provider or ISP.
- Let the monitor confirm the result. Receivers also run their own filters, so keep an eye on delivery after the verdict returns to clean.
For a one-off look at an IP or domain, use the blacklist checker. If tracing the cause or handling the requests is more than you can take on, you can ask us for help. For the full troubleshooting path when mail is bouncing, see email blocked by blacklists.
We introduce providers who take on delisting requests, consulting and advice (from JPY 50,000). Delisting is decided by each list operator and cannot be promised.
Get helpThis site may receive a referral fee from listed providers. About
FAQ
I checked through 8.8.8.8 and got no listing. Am I clean?
Not necessarily. Spamhaus's FAQ says queries through free open resolvers such as Google Public DNS will in most cases get a not-listed (NXDOMAIN) reply, so a listed IP can look clean. Query from your own resolver, or use the operator's lookup page.
I got 127.255.255.254. Does that mean I am listed?
No. It is an error code returned for queries via a public or open resolver, and Spamhaus states it does not mean the object is listed. Log it as an error and do not send a listing alert.
Can I just call the batch API every day as my monitor?
A batch may return a stored result up to 30 days old, so it is not always today's state. Check checked_at, or use webhook monitoring (POST /api/v1/monitors), which runs a fresh check every day.
Should I request removal as soon as an alert fires?
Fix the cause first. A request made while the problem remains usually ends in a new listing. The decision always rests with the list operator.
Official pages used
The facts in this article were checked against the official pages below at the time of publication. Policies and behavior change without notice, so check the official pages before relying on anything important.
- RFC 5782: DNS Blacklists and Whitelists
- Spamhaus FAQ: DNSBL Usage
- Spamhaus: Using our public mirrors? Check your return codes now
- SpamCop FAQ: How do I configure my mailserver to reject mail based on the blocklist?
Related guides
How to read DNSBL return codes: what 127.0.0.2 means under RFC 5782, codes for Spamhaus, SpamCop and UCEPROTECT, error codes, and NXDOMAIN vs SERVFAIL.
Listed on Spamhaus? Find out which list you are on (ZEN, PBL, XBL, CSS, SBL, DBL), what the return codes mean, and the right delisting path for each.
Email blocked by a blacklist? Read the bounce code, check the causes in the right order, see which listings matter, and request removal the right way.