Operations

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

EndpointLimitOn exceed
POST /v1/verify600 / minute429 rate_limited
POST /v1/outcome6,000 / minute429 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

Read this one before you write your error handling

/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_hash is 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.