Most APIs assume a human with a browser somewhere in the onboarding path: a signup form, an email confirmation, a dashboard where a key is copied. An agent hits all three as hard stops. This walks the full loop — access, validation, error recovery — with every step as one HTTP request.

Step 1: see the response shapes with no credentials

Start here, because it costs nothing and tells you whether the API is worth wiring up.

curl -X POST https://nobounce.dev/demo/check \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@gmai.com"}'
{
  "verdict": "undeliverable",
  "reason": "typosquat_mx",
  "suggestion": "user@gmail.com",
  "confidence": 0.94,
  "checked": { "syntax": true, "mx": true, "typo": true, "disposable": true },
  "cached": true
}

Understand what this endpoint is before you build on it. /demo/check resolves a frozen fixture corpus, not live DNS. Every verdict and every reason is reachable from it, so it is an excellent way to see real response shapes and write your handling code — but it cannot validate an arbitrary real address, and it is rate-limited by IP. Ask it about an address outside the corpus and it refuses with a structured error rather than guessing.

The full fixture list is in /v1/config, alongside the frozen verdict and reason enums.

Step 2: get a key

There is no free tier. Every live DNS check requires a paid key, entry $1/mo, and all routes end at the same entitlement.

If you were handed an access code, redemption is one call:

curl -X POST https://nobounce.dev/v1/keys \
  -H 'Content-Type: application/json' \
  -d '{"coupon":"NB-YOUR-CODE"}'

No browser, no captcha, no email confirmation, no human step. That is the property that makes handing an agent a code a viable onboarding path.

If you are paying, both rails are self-serve and both are a single call. Card:

curl -X POST https://nobounce.dev/v1/keys \
  -H 'Content-Type: application/json' \
  -d '{"rail":"stripe","tier":"pro","period":"monthly"}'

Returns a checkout URL. Or stablecoins, which needs no browser at all:

curl -X POST https://nobounce.dev/v1/keys \
  -H 'Content-Type: application/json' \
  -d '{"rail":"usevig","tier":"pro","period":"monthly"}'

Returns a payment address and the exact amount for each accepted chain and token. The key is issued when the payment reaches confirmed finality and is collected once from the returned poll URL.

Store the key in a secret manager. Never put it in a URL, a query parameter, or a committed file — it goes in the Authorization header and nowhere else.

Step 3: check an address

curl -X POST https://nobounce.dev/v1/check \
  -H "Authorization: Bearer $NOBOUNCE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"email":"diego@gmai.com"}'

For lists, batch up to 1000 addresses per request against /v1/check/batch. Batching matters more than it looks: the domain-level verdict cache is shared across all customers, so a batch of a thousand addresses at common providers is mostly cache hits.

Step 4: handle all four verdicts

This is where integrations go wrong. There are four verdicts and only one of them stops anything.

import os, httpx

def check(email: str) -> dict | None:
    try:
        response = httpx.post(
            "https://nobounce.dev/v1/check",
            headers={"Authorization": f"Bearer {os.environ['NOBOUNCE_KEY']}"},
            json={"email": email},
            timeout=2.0,
        )
        if response.status_code != 200:
            # RFC 9457 problem+json. The `fix` field says what to do.
            print(response.json().get("fix"))
            return None
        return response.json()
    except httpx.RequestError:
        return None   # no opinion, not a rejection

result = check("diego@gmai.com")

if result is None:
    proceed(email_verdict="unchecked", mx_checked=False)
elif result["verdict"] == "undeliverable" and result["suggestion"]:
    offer_correction(result["suggestion"])          # the valuable case
elif result["verdict"] == "undeliverable":
    reject(result["reason"])
else:
    # deliverable, risky and unknown all proceed.
    proceed(email_verdict=result["verdict"], mx_checked=result["checked"]["mx"])

Two rules carry most of the value.

A suggestion is not an error. undeliverable with a suggestion means the domain the user typed is not the domain they meant, and you know which one they meant. Offer it with one-click accept and a real "keep what I typed" option, since confidence is a probability rather than a certainty.

unknown proceeds. It means the DNS check did not complete; paired with checked.mx: false it is the contractual fail-open signal. Record it, do not reject on it. Validation must never be a hard dependency of signup — an agent that treats a validator outage as a bad address will confidently discard good data.

risky also proceeds. It covers role accounts (admin@, contato@) and disposable domains. Those are real addresses; whether to allow them is your product decision, made explicitly by branching on reason.

Step 5: recover from errors without a human

Every error is RFC 9457 problem+json with a fix field written in plain language, on the principle that a client receiving a 4xx should recover from the response body alone.

{
  "type": "about:blank",
  "title": "missing_or_invalid_api_key",
  "status": 401,
  "fix": "Send \"Authorization: Bearer <api_key>\"."
}

Read fix on any non-2xx and act on it, rather than retrying the identical request. That field is the highest-leverage affordance in the API for a non-human caller.

Step 6: report outcomes back, as hashes

If you learn later that an address bounced or delivered, /v1/feedback accepts that signal — but only as a SHA-256 hash. Send a raw address and it returns a 400 rather than hashing it for you. /v1/hash computes the hash if you need it done server-side.

No plaintext email address is stored anywhere in the service. Anything persisted is hashed; domains are kept in clear because a domain is not personal data, which is exactly what lets the shared cache work.

Using MCP instead of raw HTTP

The same surface is exposed over Model Context Protocol at POST /mcp, as streamable HTTP JSON-RPC 2.0, if your agent framework speaks MCP natively.

The document to actually give your agent

Everything above is condensed, ordered and made executable at /integrate.md, which is written for an agent to follow rather than for a person to read:

Wire email validation into my signup flow. Fetch and follow:
https://nobounce.dev/integrate.md

The prompt a human pastes is one line plus a URL. The instructions live in the hosted document, so they improve without anyone re-pasting anything. If you have not decided on the service yet, /evaluate.md is the counterpart, and it is written to be able to recommend against nobounce.