# Errors

> isBusinessEmail errors are RFC 9457 problem+json with a stable code. Every code, its HTTP status, what causes it, and how to handle it.

Source: https://isbusinessemail.com/docs/errors

Errors use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json`. Branch on `code`: it's stable. `title` and `detail` are for humans and may change.

```json
{
  "type": "https://isbusinessemail.com/docs/errors#invalid_input",
  "title": "Bad Request",
  "status": 400,
  "code": "invalid_input",
  "detail": "Send exactly one of \"email\" or \"domain\".",
  "docs_url": "https://isbusinessemail.com/docs/errors#invalid_input"
}
```

| Field | Meaning |
|---|---|
| `type` | URI of the error type: this page, with the code as anchor |
| `title` | Short HTTP status text |
| `status` | HTTP status code |
| `code` | Stable machine-readable code (below) |
| `detail` | Human-readable explanation of this occurrence |
| `docs_url` | Link to the section of this page for `code` |

Some errors add extra fields; ignore the ones you don't use.

## Summary

| Code | Status | Retry? |
|---|---|---|
| `invalid_input` | 400 | No: fix the request |
| `unauthorized` | 401 | No: send a key |
| `invalid_key` | 401 | No: check the key |
| `key_revoked` | 401 | No: use another key |
| `forbidden_origin` | 403 | No: add the origin to the key |
| `terms_not_accepted` | 403 | No: accept the updated terms |
| `rate_limited` | 429 | Yes, after `Retry-After` |
| `quota_exceeded` | 429 | After 00:00 UTC |
| `batch_too_large` | 413 | No: split the batch |
| `payload_too_large` | 413 | No: send less |
| `not_found` | 404 | No |
| `method_not_allowed` | 405 | No: use the right method |
| `internal_error` | 500 | Yes, with backoff |
| `pass_required` | 403 | Website checker only |
| `banned` | 403 | Website checker only |
| `busy` | 503 | Website checker only |

## API errors

### `invalid_input`

**400.** The request isn't well-formed: missing input, both `email` and `domain`, a non-string value, an unknown `policy`, input over the length limits (email 254, local part 64, domain 253), control characters, or invalid JSON. Fix the request; retrying won't help.

A well-formed request for an address that simply isn't valid is **not** an error: it returns `200` with `category: "invalid"`.

### `unauthorized`

**401.** No API key was sent, and the input isn't a [test address](https://isbusinessemail.com/docs/test-addresses). Send `Authorization: Bearer ibe_live_…` or `X-API-Key`. See [Authentication](https://isbusinessemail.com/docs/authentication).

### `invalid_key`

**401.** The key is malformed, fails its checksum, or doesn't exist. Common causes: a truncated copy-paste, a stray space or newline, or using a key from a different environment.

### `key_revoked`

**401.** The key was revoked, by you, by an admin, or automatically because it was found in a public GitHub repository. Create a new key in the dashboard. Revocation applies everywhere within 60 seconds.

### `forbidden_origin`

**403.** A publishable key (`ibe_pub_…`) was used from an origin that isn't on its allowed list, or without an `Origin` header. Add the exact origin (scheme + host + port, e.g. `https://app.example.com`) to the key in the dashboard. For server-side calls, use a secret key.

### `terms_not_accepted`

**403.** The account hasn't accepted the current Terms of Service and Privacy Policy. After a material change you get 30 days' notice and a grace period (responses carry a `Warning` header); after that, keys return this error until someone on the account signs in and accepts.

### `rate_limited`

**429.** Too many requests per second, or too many in flight, for your tier. Wait for `Retry-After` seconds and retry with jitter. See [Rate limits](https://isbusinessemail.com/docs/rate-limits).

### `quota_exceeded`

**429.** Your daily quota is used up. It resets at 00:00 UTC. Signups should fail open; batch jobs should stop and resume tomorrow, or [ask for a higher tier](https://isbusinessemail.com/contact).

### `batch_too_large`

**413.** A [batch](https://isbusinessemail.com/docs/batch-endpoint) had more than 100 items. Split it into chunks of 100.

### `payload_too_large`

**413.** The request body is over the size limit (256 KB for batch). Send fewer or shorter items.

### `not_found`

**404.** The path doesn't exist. Check for typos and the `/v1` prefix.

### `method_not_allowed`

**405.** The path exists but not with this HTTP method, e.g. `GET /v1/check/batch`. The `Allow` header lists what's supported.

### `internal_error`

**500.** Something broke on our side. Retry with exponential backoff, and if it persists, send us the `request_id` from a previous response or the time of the failure. For signups, fail open.

## Website checker errors

These come from the free checker on isbusinessemail.com, not from the developer API. You'll only see them if you call the website's internal endpoints, which aren't a supported API. [Get a free key](https://isbusinessemail.com/signup) instead.

### `pass_required`

**403.** The browser needs to pass the human check (Turnstile) before checking more addresses.

### `banned`

**403.** The network was temporarily blocked after automated or abusive use of the website checker.

### `busy`

**503.** The website checker's shared daily budget is used up. API key holders aren't affected.

## `degraded` is not an error

`degraded: true` in a `200` response means DNS lookups failed or timed out and the verdict comes from our lists alone, with lower `confidence`. It's a flag on a successful response, not an error status. A sensible default: treat `review` as `allow` while degraded and re-check later.

## Handling errors in code

```javascript
const res = await fetch(url, options);
if (!res.ok) {
  const problem = await res.json().catch(() => ({}));
  switch (problem.code) {
    case 'rate_limited':
    case 'quota_exceeded':
    case 'internal_error':
      return failOpen();            // let the signup through, re-check later
    case 'invalid_key':
    case 'key_revoked':
    case 'unauthorized':
      alertOps(problem);            // configuration problem: page someone
      return failOpen();
    default:
      throw new Error(`${problem.code}: ${problem.detail}`);
  }
}
```
