DEV Community

xusteve
xusteve

Posted on Edited on

Building an anonymous email-domain scan API on Cloudflare Workers

Mailers.email exposes domain health checks as both a REST API and an MCP server. This post covers the API contract, the Cloudflare Worker request path, caching, rate limiting and the design decisions behind no-key access.

The short version: POST /v1/scan accepts a domain, runs a small set of DNS checks through Cloudflare DoH, returns a structured score plus fix-first findings, and requires no account or API key. The public API is intentionally cheaper than the full web scan: blacklist checks are excluded.

Why email-domain checking works well as an API

Email configuration is mostly public DNS. MX, SPF, DKIM, DMARC, reverse DNS and domain age can all be inspected without mailbox access.

That makes the core check a good fit for a small, read-only API:

  • one domain in
  • a handful of DNS lookups out
  • a deterministic result that another service or agent can explain

It is also safe to automate. There is no message content, no mailbox login and no sending capability.

Contract of POST /v1/scan

curl -X POST https://api.mailers.email/v1/scan \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}'
Enter fullscreen mode Exit fullscreen mode

A successful response contains the domain, score, grade, confidence, individual checks and a fixes array:

{
  "ok": true,
  "data": {
    "domain": "example.com",
    "score": 69,
    "maxScore": 80,
    "grade": "C",
    "confidence": 83,
    "checks": ["mx", "spf", "dkim", "dmarc", "age", "rdns"],
    "fixes": ["rdns", "mx"]
  },
  "meta": {
    "cached": false,
    "quota": {
      "limit": 60,
      "remaining": 59,
      "scope": "hour"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The real response includes the record values and reasons for each check; the example is abbreviated.

About maxScore

The public API returns maxScore: 80.

This is deliberate. The score is normalized against the checks that actually ran. DNSBL checks are excluded from the public API, so a perfect public scan should not be presented as a perfect full deliverability audit.

That distinction matters when another system summarizes the result. A score should come with the scope it was measured against.

Request flow: Cloudflare Worker + DoH

The API is served by a Cloudflare Worker.

At a high level:

POST /v1/scan
  -> validate domain
  -> check IP rate limit
  -> check KV cache by domain
  -> query Cloudflare DoH for DNS records
  -> build checks and score
  -> cache result for 24 hours
  -> return JSON
Enter fullscreen mode Exit fullscreen mode

The Worker uses Cloudflare's DNS-over-HTTPS resolver for live DNS. No separate DNS service sits in the middle.

The scan currently covers:

  • MX records
  • SPF record and lookup count
  • DKIM at common selectors
  • DMARC policy
  • reverse DNS / PTR
  • domain age via RDAP where available

The checks are deliberately bounded. This is not a full inbox-placement or reputation test.

24-hour cache for the same domain

DNS records do not change every minute. Caching the result by domain keeps repeated checks cheap and avoids hammering upstream resolvers.

The cache is stored in Cloudflare KV with a 24-hour TTL. Cached requests still count toward the rate limit, but they skip the DNS work. The response includes meta.cached so callers know whether they received a cached result.

The cache is per domain, not per user. There is no account or API key to associate with a query.

Rate limiting: atomic counters with Durable Objects

Anonymous APIs need a limit that works under concurrency.

An ordinary read-modify-write sequence in KV is not atomic: two simultaneous requests can read the same counter and both write the same increment. For a 60/hour limit, that is a quiet way to let bursts through.

The scan API uses a Durable Object as a per-IP serialization point for counter updates. The object handles the counter operation, so increments are atomic. The API uses the same mechanism for the shared scan budget.

The public scan limit is:

  • 60 requests/hour/IP
  • shared by the REST scan, MCP scan_domain and the interactive scan
  • no API key or account
  • HTTP 429 when exceeded

The separate free DNS tool pages have their own stricter limit of 30/minute and 300/hour. The scan API and the tool pages are related products, but they are not the same quota.

Why the API excludes DNSBL

Blacklist checks are 32+ additional DNS queries per domain. That is a different cost profile from the six cheap checks above.

Keeping DNSBL out of the public API has three benefits:

  1. A scan stays fast and inexpensive.
  2. A single scan_domain MCP call does not fan out into dozens of queries.
  3. The API's scope stays honest: it reports DNS configuration, not live reputation.

The full blacklist scan lives in the web app at mailers.email/scan. The API documentation also states that the public scan does not include blacklist checks.

MCP: scan_domain

The same cheap scan is available as a remote MCP server:

{
  "mcpServers": {
    "mailers": {
      "url": "https://mcp.mailers.email/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It exposes exactly one tool:

scan_domain
Enter fullscreen mode Exit fullscreen mode

The tool is read-only. It does not access a mailbox, send email or require credentials. This keeps the permission model narrow enough for an agent to use without a long explanation.

One implementation detail: MCP calls run a fresh scan rather than using the REST API's 24-hour cache. The REST endpoint is optimized for repeated checks; the MCP path is optimized for an agent asking once during a task.

I wrote a separate MCP-focused post with client configuration and agent examples:

Giving AI agents an email deliverability check with MCP

Error handling and API behavior

The API returns JSON for normal application errors rather than an HTML error page.

Expected behavior:

  • 400 for an invalid or missing domain
  • 405 for a non-POST request
  • 410 for retired API routes; only POST /v1/scan remains public
  • 429 when the IP budget is exhausted
  • 502 when an upstream scan cannot complete

The health endpoint is available at https://api.mailers.email/health.

Privacy and limits, stated plainly

This is a hosted service, not a self-hosted open-source scanner.

What is true:

  • no account and no API key
  • DNS-based checks only
  • per-IP rate limits
  • same-domain results cached for 24 hours
  • no mailbox access and no email sending

What is not claimed:

  • no inbox-placement guarantee
  • no permanent "no data stored" claim
  • no blacklist check in the public API
  • no 100-point full audit score

Precise limits are more useful than vague promises.

What I would improve next

The current design is deliberately small. The next useful steps would be:

  1. Publish an OpenAPI file and client examples.
  2. Add a small response-schema version field.
  3. Add more granular error details for DNS timeouts.
  4. Keep the public API cheap while leaving heavier reputation checks in the web scanner.

Try it

If you build something with the API or the MCP server, I would like to hear what worked and what was missing.

Top comments (0)