> ## Documentation Index
> Fetch the complete documentation index at: https://docs.finowl.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Enrich a contact list

> Add gender estimates to thousands of CRM contacts with the batch endpoint, safely and efficiently.

The batch endpoint processes up to 50 inputs per request and counts as a single request toward your rate limit, so you can enrich up to 3,000 contacts per minute.

## Plan your credits

Each item costs 1 credit. Enriching 20,000 contacts costs 20,000 credits. Check your balance in [Billing](https://app.finowl.co/billing) before you start, because a batch that cannot be fully covered is rejected with `quota_exceeded`.

## Choose the best input for each contact

Use the strongest signal you have, in this order:

1. **First name or full name** (`type: "name"`), the most accurate.
2. **Personal email** (`type: "email"`), when you have no name.
3. **Username** (`type: "username"`), when that is all you have.

Add `country` whenever you know it. It noticeably improves results for names whose gender varies by country.

## Example

```javascript theme={null}
import { setTimeout as sleep } from "node:timers/promises";

const BATCH_SIZE = 50;

function toItem(contact) {
  const base = { id: contact.id };
  if (contact.country) base.country = contact.country;
  if (contact.firstName) return { ...base, type: "name", value: contact.firstName };
  if (contact.email) return { ...base, type: "email", value: contact.email };
  return { ...base, type: "username", value: contact.username };
}

async function enrich(contacts) {
  const items = contacts.filter((c) => c.firstName || c.email || c.username).map(toItem);

  for (let start = 0; start < items.length; start += BATCH_SIZE) {
    const batch = items.slice(start, start + BATCH_SIZE);
    const response = await fetch("https://api.finowl.co/v1/gender/batch", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FINOWL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ items: batch }),
      signal: AbortSignal.timeout(40_000),
    });

    const body = await response.json();

    if (response.status === 429 && body.error?.code === "rate_limited") {
      await sleep(Number(response.headers.get("Retry-After") ?? 60) * 1000);
      start -= BATCH_SIZE; // Not charged, so resend the same batch.
      continue;
    }
    if (!Array.isArray(body.data)) {
      // quota_exceeded, invalid_input or a service error. Stop and investigate.
      throw new Error(body.error?.code ?? `http_${response.status}`);
    }

    for (const result of body.data) {
      if (result.error) {
        await markFailed(result.id, result.error.code);
      } else {
        await saveGender(result.id, result.data);
      }
    }

    await sleep(1000); // Leave room in the rate limit for live traffic.
  }
}
```

## Make the job resumable

Large imports get interrupted. Make yours safe to restart:

* Store a status per contact: `pending`, `done` or `failed`.
* Select only `pending` contacts when the job starts, so a restart never pays twice for the same contact.
* Keep `failed` contacts separate and review them before deciding to resend.

## Validate before you send

One invalid item rejects the entire batch, so filter locally first:

* Skip empty values and values over 1,024 characters.
* Send emails as plain addresses only, such as `alice@example.com`, not `"Alice" <alice@example.com>`.
* Use two-letter country codes, such as `US` or `DE`.
* Make each `id` unique within a batch and at most 100 characters.

## Store results thoughtfully

Save `result_status` as well as `gender`, so you can tell "predicted female" apart from "unknown". Many teams also store `confidence` and the date of the prediction. Read [Reading predictions](/guides/interpreting-predictions) before using the results in personalization.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.