# Server-side integration (Node client)

> Use isBusinessEmail from a backend or a mail pipeline: the zero-dependency Node client with timeouts, retries, caching and fail-open checks.

Source: https://isbusinessemail.com/docs/server-side

Backends call the API with a **secret key** (`ibe_live_…`). The official client is one file with no dependencies. It runs on Node 18+, Cloudflare Workers, Deno and Bun:

```bash
npm install @isbusinessemail/client
```

```js
import { createClient, isPersonal } from '@isbusinessemail/client';

const ibe = createClient({ apiKey: process.env.ISBUSINESSEMAIL_API_KEY });

const r = await ibe.check('jane@acme.io');
r.category;                               // "business"
r.workspace.google_workspace.detected;    // true
isPersonal(r);                            // false
```

Secret keys are refused from browsers. For forms in the browser, use a [publishable key](https://isbusinessemail.com/docs/authentication).

## Built for hot paths

The client was made for places like mail ingest, where every message is checked and a slow dependency must never hold up the pipeline:

| Behaviour | Default | Option |
|---|---|---|
| Hard timeout per request | 2 s | `timeoutMs` |
| Retry on network errors and 5xx (never on 429) | 1 retry | `retries` |
| In-memory LRU of answers; degraded and deep answers are not cached | 10 minutes, 10,000 entries | `cacheTtlMs`, `cacheSize` |
| Base URL | `https://api.isbusinessemail.com` | `baseUrl` |

## Fail open with `tryCheck`

`check()` throws an `IbeError` with `status`, `code` (the problem+json [error code](https://isbusinessemail.com/docs/errors)), `retryAfter` and `requestId`. `tryCheck()` never throws. It returns `null` when there is no answer, so you fall back to your own data:

```js
async function senderIsPersonal(address, ownListLookup) {
  const r = await ibe.tryCheck(address);
  const fromApi = isPersonal(r);          // true | false | null (unknown / no answer)
  return fromApi ?? (await ownListLookup(address));
}
```

`isPersonal()` is `true` only for the `personal` category (shared-domain providers such as gmail.com). It returns `null` for `unknown` and `invalid` and when there is no answer, so your fallback decides those cases.

## Many addresses at once

`checkBatch()` keeps your input order and skips anything already in the cache. It sends the rest in chunks of 100 (the [batch limit](https://isbusinessemail.com/docs/batch-endpoint)):

```js
const results = await ibe.checkBatch(['bob@gmail.com', 'contoso.com', 'x@mailinator.com']);
results.map((r) => r.category);   // ["personal", "business", "disposable"]
```

Every item counts toward your daily quota. Use it for backfills, not for one message at a time.

## Blocklists and your own lists

`is_blocked` is `true` when the domain, or for secret keys the exact address, is on our blocklist. Keep any blocklists of your own in your system and check them first. The API adds what you don't have: shared-domain providers, disposable and relay services, Workspace and Microsoft 365 detection and lookalike domains.

## Limits for a service

New accounts start at 5 checks a second and 100 a day. A mail pipeline usually needs more. Request higher limits from your dashboard and say roughly how many messages a day you process. Use [`GET /v1/usage`](https://isbusinessemail.com/docs/usage-endpoint) (`ibe.usage()`) to watch your quota. Plan for `429 quota_exceeded`: the client doesn't retry it, and `tryCheck()` returns `null`, so the pipeline keeps going on your own data.

## Reporting a wrong verdict

```js
await ibe.feedback('jane@acme.io', 'business', 'Our customer, a real company');
```

Reports are reviewed by a person before any list changes. See [Feedback](https://isbusinessemail.com/docs/feedback-endpoint).
