# Integrating nobounce.dev

You are integrating nobounce.dev into a project. Follow this in order. Step 1
consumes an access code, which is single-use and costs money to obtain, so
handle it according to whatever spending authority you have.

If you have not yet decided whether this service fits, read https://nobounce.dev/evaluate.md
first. It needs no credentials.

## 1. Redeem the access code

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

Returns 201 with exactly this shape — do not write defensive code for
alternatives:

```json
{
  "account_id": "acct_...",
  "api_key": "nb_live_...",
  "tier": "pro",
  "live_dns": true,
  "included_checks": 100000,
  "period": "monthly",
  "note": "...",
  "try_it": "...",
  "docs": "https://nobounce.dev/llms.txt"
}
```

`tier` and `included_checks` reflect whichever tier the code grants. There is
no free tier and no self-serve key: without a valid code this returns 402
`access_code_required`. A 403 `access_code_not_redeemable` means the code is
unknown, expired, revoked or already used — the response does not distinguish
these. Codes come from support@nobounce.dev.

## 2. Store the key before doing anything else

`api_key` is shown exactly once and is not recoverable.

**Server-side only.** An environment variable or the project's existing secret
manager. Never a `NEXT_PUBLIC_*` or `VITE_*` variable, a client bundle, a
mobile app, or any file that is committed. Anyone who reads the key can spend
the quota. If the project has no server-side path, stop and resolve that first.

Verify the destination is not version controlled before writing it:

```bash
git -C <target-directory> rev-parse --is-inside-work-tree 2>/dev/null
```

If that prints `true`, choose somewhere else.

## 3. Call it on submit, from the server

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

Returns exactly this shape:

```json
{
  "verdict": "undeliverable",
  "reason": "typosquat_mx",
  "suggestion": "user@gmail.com",
  "confidence": 0.94,
  "checked": { "syntax": true, "mx": true, "typo": true, "disposable": true },
  "cached": false
}
```

`suggestion` is `null` when there is nothing to suggest. `verdict` is always
one of `deliverable`, `undeliverable`, `risky`, `unknown`. `reason` is one of 11
stable values; `GET https://nobounce.dev/v1/config` returns the full list.

Call it once on submit or on field blur — not on every keystroke — before
writing the address to your database or sending mail to it.

## 4. Map the verdict to behaviour

This determines whether the integration is worth having.

| verdict | suggestion | do this |
|---|---|---|
| `deliverable` | — | Accept. |
| `undeliverable` | present | Show "Did you mean **<suggestion>**?" with one-click accept. |
| `undeliverable` | `null` | Block, with a message derived from `reason`. |
| `risky` | — | Accept. Flag internally if you want. |
| `unknown` | — | Accept. |

**The suggestion path is the product.** An `undeliverable` carrying a
`suggestion` is a user one click from a successful signup. Offer the
correction; do not hard-reject, and do not silently auto-correct — show it and
let them confirm.

**`unknown` means accept.** It is returned when a DNS lookup did not complete
(`checked.mx: false`), so it describes the check, not the address. Treating it
as invalid rejects real customers during any DNS incident.

**`risky` means accept.** `disposable` and `role_account` are deliverable
addresses; whether to allow them is a product decision, not a validity one.

When blocking, use `reason` to say something specific — "that domain does not
exist" for `domain_not_found` — rather than a generic "invalid email".

## 5. Fail open

**Validation must never be a hard dependency of signup.** If the API is slow,
returns 5xx, rate-limits you, or the quota is exhausted, let the signup through.

```js
async function validateEmail(email) {
  try {
    const res = await fetch("https://nobounce.dev/v1/check", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.NOBOUNCE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) return { verdict: "unknown" };   // fail open
    return await res.json();
  } catch {
    return { verdict: "unknown" };                 // fail open
  }
}
```

Because `unknown` already means accept, the failure path falls out of the
mapping you wrote in step 4. No second branch is needed.

## Operational facts that will otherwise cost you time

- **The domain cache is shared across all customers.** A domain looked up by
  anyone is warm for everyone; the second check of a domain returns
  `cached: true` without re-resolving DNS. Common providers are effectively
  always warm, so your steady-state latency is better than a first-call
  measurement suggests.
- **A cache hit still counts against your quota.** `cached: true` means we
  skipped the DNS lookup, not that the check was free. `GET https://nobounce.dev/v1/me`
  reports `checks_used` and `cache_hits` separately.
- **Batch is one call, not N.** `POST /v1/check/batch` takes up to
  1,000 addresses per request; more returns 413 `batch_too_large` with
  `limit` and `received` in the body. Each result carries `email_sha256`
  instead of the address, so batch responses are safe to log as-is.
- **Retries are safe if you send `Idempotency-Key`.** Reuse the same key when
  retrying the same address and it is not billed twice.
- **You never have to send us a plaintext address for feedback.**
  `POST /v1/hash` returns `{"email_sha256","normalized","note"}`, computed in
  memory and not persisted. `POST /v1/feedback` accepts only the hash, so you
  can report delivery outcomes without handling personal data.
- **Every 4xx is RFC 9457 problem+json** with `type`, `title`, `status`,
  `error` and `fix`. `fix` is written to be actionable — surface it verbatim
  rather than paraphrasing it into something vaguer.

## Do not log the address

Do not write plaintext addresses to logs, analytics or a validation table.
nobounce does not store them either. Hash first with `/v1/hash` if you need to
correlate results with a user.

## Verification checklist

Prove the integration end to end before reporting success:

1. `GET https://nobounce.dev/v1/me` with the key returns your `tier` and a
   `checks_remaining` below `included_checks` after you have made a call.
2. Submitting `user@gmai.com` through the real signup form shows a "did you
   mean user@gmail.com?" prompt — not a rejection.
3. Accepting that prompt fills in `user@gmail.com` and the signup completes.
4. Submitting `user@gmail.com` completes with no interruption.
5. Submitting `user@mailinator.com` (`risky`) is accepted.
6. With the API key temporarily set to an invalid value, the signup still
   completes. This proves fail-open. Restore the key afterwards.
7. `grep` the codebase: the key appears only in server-side code, is absent
   from any client bundle, and no plaintext address is written to logs.

Step 6 matters most. An integration that breaks signup when validation is
unavailable is worse than no integration.

Full reference: https://nobounce.dev/llms.txt and https://nobounce.dev/openapi.json
