Skip to main content

Checking addresses

A check is what a sign-up or contact form asks: is this mail address, its domain or this client IP on a list? Everything except /health needs the X-Api-Key header.

The routes​

RouteWhat it answers
POST /api/v1/checkOne check: a mail address, a domain and a client IP, any of them, at least one
POST /api/v1/check/batchUp to 100 checks in one call, answered in their order

Both answer whether or not the service is licensed; see Without a license. They change nothing, so a read-only key may ask them, and every role of the console may too. The management routes are listed in the reference.

Asking​

curl -s -X POST http://127.0.0.1:5441/api/v1/check \
-H "X-Api-Key: $BLOCKLIST_API_KEY" -H "Content-Type: application/json" \
-d '{"email": "Someone@Discard.example", "ip": "203.0.113.7", "mailDomain": true}'
FieldWhat it is
emailThe mail address the form was given. Its domain is checked too, unless domain names another
domainA domain to check on its own, or instead of the address's. A leading @ is ignored
ipThe client's IP address, IPv4 or IPv6. An IPv4 address written as IPv6 (::ffff:203.0.113.7) is checked as IPv4
mailDomaintrue asks DNS whether the domain receives mail. Off by default; see The mail-domain check

Addresses and domains are trimmed and lower-cased before anything is compared, so case never decides a match.

A request the service cannot check is answered 400 with a sentence saying why, and nothing is counted:

SentenceCause
Name an address, a domain or an IP to check.All three are empty
'...' is not an e-mail address.The address is not one the service accepts: plain ASCII, one @, no quoted local part, no IP address for a domain, a real top-level domain, at most 254 characters
'...' is not a domain.The domain is not a host name with at least one dot
'...' is not an IP address.The IP cannot be read as an IPv4 or IPv6 address

The answer​

The request above, against a domain that two of the throwaway-domain lists hold:

{
"listed": true,
"reason": "domain on disposable-email-domains, FakeFilter",
"email": { "normalized": "someone@discard.example", "ownBlock": false },
"domain": {
"value": "discard.example",
"lists": ["disposable-email-domains", "FakeFilter"],
"ownBlock": false,
"mail": "accepts"
},
"ip": { "value": "203.0.113.7", "ownBlock": false, "lists": [] },
"unlicensed": false
}
FieldWhat it says
listedtrue when anything listed the request
reasonWhy, in one of the words below; null when nothing listed it
emailThe address as normalized, and ownBlock: whether your own entries hold the address or its domain. null when no address was asked about
domainThe domain checked; lists, the public lists that hold it, in the console's order; ownBlock, whether one of your own domain entries holds it or a parent of it; and mail, the mail-domain check's answer. null when there was no domain
ipThe IP checked; ownBlock, whether your own IP entries hold it; and lists, the public lists that hold it. null when no IP was asked about
unlicensedtrue while the service runs without a license, so that only your own entries and the built-in list of throwaway domains were consulted

A program that wants the whole picture reads the parts. A program that wants one thing to record or show reads reason.

The reason words​

reason is the first of these that applies, in this order:

OrderreasonWhen
1IP blocklistYour own IP entries hold the client IP
2IP on <lists>One or more public IP lists hold it, named and separated by commas, for example IP on Tor exit list
3the domain receives no mailThe mail-domain check was asked for and found that the domain cannot receive mail
4domain on <lists>One or more public domain lists hold the domain, for example domain on burner-email-providers
5blocklistYour own entries hold the address, or the domain or a parent of it

The words and their order are fixed, so a report can group refusals by them. The names inside them are the lists' names as their cards on the Blocklists page show them.

Your own address and domain entries come last: a domain that a public list also holds is reported by that list's name, and the ownBlock fields still say that your own entries hold it.

The mail-domain check​

With "mailDomain": true the service asks DNS whether the domain can receive mail at all:

mailWhat DNS said
acceptsThe domain has a mail server (an MX record), or, without one, an address of its own, which mail servers deliver to
noneThe domain does not exist, has no address at all, or publishes a "null MX" saying it accepts no mail
unknownDNS could not be asked: it did not answer in time, or answered with a failure. Neither yes nor no

Only none lists the request (the domain receives no mail). unknown lists nothing, so a DNS outage never refuses a visitor.

mail is null - the check is not made - when:

  • the request did not ask for it;
  • the client IP was already listed, so the answer is decided and no lookup is spent on it;
  • the license does not carry the mail-domain check. See Licensing.

The service asks through the resolvers the container is given, waits at most five seconds and tries once more.

Batches​

POST /api/v1/check/batch takes {"checks": [ ... ]}, one to 100 requests shaped like a single check, and answers {"results": [ ... ]} with one answer per check, in the same order.

The batch is checked before anything is answered: one request that cannot be checked refuses the whole batch with 400, naming it (Check 3: '...' is not an IP address.). A batch with no checks or more than 100 is refused with A batch carries 1 to 100 checks.

Without a license​

Every check is answered. Without a license the service consults:

  • your own entries: addresses, domains and client IPs, and the deployment's;
  • the built-in list of throwaway domains that ships with every release, with its exceptions, while it is switched on.

No public list is downloaded or consulted, and the mail-domain check is not made. Every answer says "unlicensed": true. Nothing on the check surface ever answers 403 for want of a license, so an integration written against this service keeps running when a license lapses. See Licensing.

Rate limits​

Off by default, and per API key rather than per address: the callers here are machines, several can sit behind one NAT, and the key is the only identity this service has.

A caller over the limit gets 429 with a Retry-After, so a client knows when to come back instead of retrying straight into the limit again.

It covers the checks - single checks and batches - and nothing else:

  • /health is never limited, so an orchestrator's health probe cannot get the container restarted.
  • The management surface is never limited, so a limit set too low can always be raised again, from the Options page or with PUT /options/ratelimit.

Before you switch it on, measure your callers' real traffic and set the limit above it: a limit set too low refuses real checks with 429, and the callers see only the refusal.

Who may ask​

The checks are their own door in the service's firewall, apart from the management API: you can admit only your web servers to the checks while the management API stays reachable from your office. Threat protection never refuses the checks, so a form that shares an address with somebody who mistyped a password keeps getting its answers. See Firewall and threat protection.