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
| Route | What it answers |
|---|---|
POST /api/v1/check | One check: a mail address, a domain and a client IP, any of them, at least one |
POST /api/v1/check/batch | Up 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}'
| Field | What it is |
|---|---|
email | The mail address the form was given. Its domain is checked too, unless domain names another |
domain | A domain to check on its own, or instead of the address's. A leading @ is ignored |
ip | The client's IP address, IPv4 or IPv6. An IPv4 address written as IPv6 (::ffff:203.0.113.7) is checked as IPv4 |
mailDomain | true 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:
| Sentence | Cause |
|---|---|
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
}
| Field | What it says |
|---|---|
listed | true when anything listed the request |
reason | Why, in one of the words below; null when nothing listed it |
email | The address as normalized, and ownBlock: whether your own entries hold the address or its domain. null when no address was asked about |
domain | The 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 |
ip | The 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 |
unlicensed | true 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:
| Order | reason | When |
|---|---|---|
| 1 | IP blocklist | Your own IP entries hold the client IP |
| 2 | IP on <lists> | One or more public IP lists hold it, named and separated by commas, for example IP on Tor exit list |
| 3 | the domain receives no mail | The mail-domain check was asked for and found that the domain cannot receive mail |
| 4 | domain on <lists> | One or more public domain lists hold the domain, for example domain on burner-email-providers |
| 5 | blocklist | Your 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:
mail | What DNS said |
|---|---|
accepts | The domain has a mail server (an MX record), or, without one, an address of its own, which mail servers deliver to |
none | The domain does not exist, has no address at all, or publishes a "null MX" saying it accepts no mail |
unknown | DNS 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:
/healthis 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.