Going to production
The four things that decide whether this holds up under real traffic: what the limits are, what happens when you hit them, what happens when providers fall over, and how long an answer stays true.
Rate limits
| Endpoint | Limit | On exceed |
|---|---|---|
POST /v1/verify | 600 / minute | 429 rate_limited |
POST /v1/outcome | 6,000 / minute | 429 rate_limited |
Outcome reporting gets a ceiling ten times higher than verification on purpose. Your bounce data is what recalibrates the model, so nothing about the design should discourage you from sending it — including a limit you might hit while backfilling.
Quota
The default allowance is 10,000 verifications a month, and it is metered on the organisation that owns the key, across every key it holds — including revoked ones and keys made by other members. Spend already made is still spend. Creating a new key does not reset the meter.
Exceeding it is 402 quota_exceeded, which is a distinct condition from 429: backing off will not clear it and retrying will not either.
The fail-open contract
/v1/verify does not fail closed. If every provider in the pool errors or times out, the call still returns 200, with verdict: "unknown", calibration: "prior", the domain prior as the probability, and degraded: true.
The reasoning is blunt: a verification API that 500s breaks the signup flow it was added to protect. A wide interval you can decide against is strictly better than an exception you have to catch.
So the branch that matters in production is not catch. It is:
const r = await verify(email)
if (r.degraded) {
// Nobody answered. This is the prior, not a measurement of this address.
// Let the signup through, flag it, decide later.
}
if (r.bounce_probability > 0.25 || r.confidence_interval[1] > 0.6) {
// Act on the number AND its interval. A tight 0.3 and a wild 0.3
// are not the same fact.
}503 all_providers_failed is reserved for the case where not even a prior can be produced. Since the prior is a constant table, seeing one means a bug — quote the trace_id.
Retries
- 429 — back off and retry. Exponential, with jitter, is enough.
- 402 — do not retry. The allowance is spent until the period rolls or the quota changes.
- 5xx — retry once, then treat it as an outage on our side and let your traffic through. You should almost never see one; the fail-open path exists so that provider trouble never reaches you as a 5xx.
- Verification is a read. Repeating one is safe, and a repeat inside the cache window costs nothing.
Cache semantics
Every response carries cache: { hit, ttl_s }. ttl_s is real remaining life, computed from when the entry was written — not the nominal window.
- An address is cached for 7 days, extended to 30 days where the evidence is stable.
- A terminal local-gate decision is not cached. It costs nothing to recompute, and caching it is the one way a domain that fixes its MX record would keep getting the old answer for a week.
- The address cache is deployment-global rather than per-tenant. It is keyed by a peppered hash of the address, and
identity_hashis deliberately excluded from it so that one customer’s salted identifier can never be served to another.
Prior versus fitted
A new account gets calibration: "prior". The prior is seeded from published benchmarks — Microsoft-hosted mail is far likelier to bounce than Google Workspace mail, catch-all domains are wide — and it is labelled honestly rather than dressed up as a measurement.
It becomes "fitted" per feature bucket once that bucket has enough of your observed outcomes behind it, via Beta-Binomial shrinkage toward the prior. Which is to say: it only ever improves if you post to /v1/outcome.