# NameGender API: File jobs (CSV and XLSX)

These are implementation instructions for AI coding assistants such as Claude Code, Cursor and GitHub Copilot. They are generated from the OpenAPI 3.1 contract at https://namegender.com/openapi.json, so field names, limits and error codes match the live API. The human-readable reference is https://namegender.com/docs#batches.

## Your task

- Integrate the file-job endpoints under `https://namegender.com/api/v1/batches` into this codebase: upload a file, wait for the job, download the result.
- Use only the endpoints, parameters and response fields documented here. If the task needs something that is not documented, stop and ask instead of guessing.
- Put the calls in one small server-side module. Read the API key from an environment variable named `NAMEGENDER_API_KEY`.
- If an official client library exists for this language (https://namegender.com/client-libraries) and it has `batches`, prefer it: it already handles the Idempotency-Key, retries and polling.
- Apply every rule under "Notes for file jobs" and "Implementation rules", then work through "Verify the integration".

## How the API behaves

Every lookup endpoint accepts both GET and POST so it can be called from a browser, a shell or a server. All paths live under `/api/v1`; a future breaking change will get a new version prefix instead of altering these.

**Success and failure.** The HTTP status code is the only success signal: a 200 carries the result, anything else carries the error body described below. There is no separate success flag in the JSON.

**Credits.** One credit per resolved name, not per request: a bulk call for 100 names costs 100 credits. Credits are checked before any work starts, so a request is never half-processed. Free accounts get 100 credits per day.

**Rate limit.** 1200 requests per minute per API key by default, counted per key rather than per IP so that several customers behind one address do not share a bucket. Every response carries `X-RateLimit-Remaining`; a 429 carries `Retry-After`.

**Errors.** Every failure, at every status code, returns the same body shape. Branch on the machine-readable `error` field, never on `message`: messages are translated into 22 languages and their wording can change.

**Support.** Each response carries a `request_id`, also sent as the `X-Request-Id` header. Quote it in any support request and we can find the exact call in our logs. You may send your own `X-Request-Id` to correlate with your tracing system.

**Data version.** Responses include `data_version`, the name-dataset snapshot that produced the answer. If a result changes between two calls, comparing this field is the first thing to check.

## Authentication

Every request needs an API key, sent in a request header. Accepted forms, in order of preference:

- Recommended: `Authorization: Bearer <key>`.
- `X-Api-Key: <key>`: Alternative header form, for clients that cannot set `Authorization`. Keys are never read from the query string or the request body.

Keys start with `ng_live_`. Create one at https://namegender.com/register; every account gets 100 free credits per day.

## Endpoints

- `POST https://namegender.com/api/v1/batches`: Upload a file and start processing it
- `GET https://namegender.com/api/v1/batches`: List file jobs
- `GET https://namegender.com/api/v1/batches/{batch}`: Status of a file job
- `DELETE https://namegender.com/api/v1/batches/{batch}`: Cancel or delete a file job
- `GET https://namegender.com/api/v1/batches/{batch}/result`: Download the result file
- `POST https://namegender.com/api/v1/batches/{batch}/start`: Start a job uploaded with start=false

Uploads a CSV or XLSX file (up to 100 MB, 1,000,000 rows) and, by default, queues it. One credit per row, reserved when processing begins; a job that fails is not charged.

`name_column` is required unless `start=false`: a guessed column that turns out to be wrong would spend credits on the wrong data. Send `start=false` to get the columns, a preview and the credit cost first, then call `/start`.

If starting fails (no credits, unknown column, AI consent), nothing is left behind: no job, no stored file.

Send an `Idempotency-Key` header to retry safely: a repeat with the same key returns the first job with `200` and `Idempotent-Replayed: true` instead of opening a second one.

## Upload request

Send `multipart/form-data`, not JSON. Settings go in form fields next to the file.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` (header) | string | no | Up to 80 printable ASCII characters, unique per upload. SDKs generate one for you. |
| `file` | file | yes | CSV, TSV or XLSX with a header row. |
| `start` | boolean | no | false: upload and inspect only. Default `true`. |
| `name_column` | string | no | Header of the column holding the names, exactly as written in the file. Required unless `start=false`. |
| `country_column` | string | no | Header of a column holding a country per row: ISO 3166-1 alpha-2, alpha-3 or the country name (`FR`, `FRA`, `France`, any case). Unrecognised values fall back to `country`. |
| `country` | string | no | Default country (ISO 3166-1 alpha-2, or alpha-3) for rows without one. At most 3 characters. |
| `ai_fallback` | boolean | no | Ask an AI model for names not in the dataset. Requires AI consent on the account, exactly as on /gender/bulk. Default `false`. |
| `best_guess` | boolean | no | Return the more likely gender even when the evidence is weak. Default `false`. |
| `delete_after_download` | boolean | no | The result can be downloaded once, then it is deleted. Default `false`. |

## Start request

For a job uploaded with `start=false`. JSON body with `Content-Type: application/json`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name_column` | string | yes | Header of the column holding the names, exactly as written in the file. |
| `country_column` | string | no | Header of a column holding a country per row: ISO 3166-1 alpha-2, alpha-3 or the country name (`FR`, `FRA`, `France`, any case). Unrecognised values fall back to `country`. |
| `country` | string | no | Default country (ISO 3166-1 alpha-2, or alpha-3) for rows without one. At most 3 characters. |
| `ai_fallback` | boolean | no | Ask an AI model for names not in the dataset. Requires AI consent on the account, exactly as on /gender/bulk. Default `false`. |
| `best_guess` | boolean | no | Return the more likely gender even when the evidence is weak. Default `false`. |
| `delete_after_download` | boolean | no | The result can be downloaded once, then it is deleted. Default `false`. |

## Example requests

```bash
# Upload and start
curl -X POST "https://namegender.com/api/v1/batches" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@customers.csv" \
  -F "name_column=first_name" \
  -F "country_column=country"

# Poll
curl "https://namegender.com/api/v1/batches/B-7K2M9QX4TA" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY"

# Download
curl "https://namegender.com/api/v1/batches/B-7K2M9QX4TA/result" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY" \
  -o customers-gender.csv
```

## Job object

Every call except the download returns this shape (the list wraps it in `data`).

| Field | Type | Description |
| --- | --- | --- |
| `id` | string |  |
| `status` | string | One of `uploaded`, `queued`, `processing`, `completed`, `failed`, `cancelled`. |
| `source` | string | One of `api`, `panel`. |
| `file` | object |  |
| `file.name` | string |  |
| `file.format` | string | One of `csv`, `xlsx`. |
| `columns` | object |  |
| `columns.name` | string \| null |  |
| `columns.country` | string \| null |  |
| `options` | object |  |
| `options.country` | string \| null |  |
| `options.ai_fallback` | boolean |  |
| `options.best_guess` | boolean |  |
| `options.delete_after_download` | boolean |  |
| `rows` | object |  |
| `rows.total` | integer |  |
| `rows.processed` | integer |  |
| `rows.identified` | integer \| null | Rows given a gender. Set once completed. |
| `progress` | integer | Range 0–100. |
| `credits` | object |  |
| `credits.reserved` | integer \| null | Held from the balance when processing began. |
| `credits.charged` | integer \| null | Final charge. Null until completed; a failed job is not charged. |
| `summary` | object \| null |  |
| `summary.male` | integer |  |
| `summary.female` | integer |  |
| `summary.unknown` | integer |  |
| `summary.from_llm` | integer |  |
| `data_version` | string \| null |  |
| `error` | object \| null | Set when `status` is `failed`. Branch on `code`. |
| `error.code` | string | One of `source_missing`, `no_columns`, `name_column_missing`, `empty_file`, `bad_format`, `unreadable`, `no_credits`, `processing_error`, `stalled`. |
| `error.message` | string |  |
| `result` | object \| null |  |
| `result.url` | string |  |
| `result.format` | string | One of `csv`, `xlsx`. |
| `result.expires_at` | string \| null |  |
| `inspection` | object \| null | Only while `status` is `uploaded`. |
| `inspection.columns` | string[] |  |
| `inspection.preview` | string[][] |  |
| `inspection.guessed_name_column` | string \| null |  |
| `inspection.guessed_country_column` | string \| null |  |
| `inspection.credits_needed` | integer |  |
| `inspection.credits_available` | integer |  |
| `poll_after_seconds` | integer \| null |  |
| `created_at` | string \| null |  |
| `started_at` | string \| null |  |
| `finished_at` | string \| null |  |
| `expires_at` | string \| null |  |

## Notes for file jobs

- **Always send `name_column`** with the exact header text. There is no automatic column choice on start: a wrong guess would spend credits on the wrong data. When the header is not known in advance, upload with `start=false`, pick from `inspection.columns`, then call `/start`.
- **Generate one `Idempotency-Key` per logical upload and keep it for every retry of that upload.** A new key on each retry defeats it: a dropped connection would then open a second job and reserve credit twice. A replay returns HTTP 200 with the header `Idempotent-Replayed: true`.
- **Poll with `GET /batches/{id}`** and wait `poll_after_seconds` between calls. Stop when `status` is `completed`, `failed` or `cancelled`. Do not poll faster; it counts toward the rate limit.
- **A failed job is a result, not an exception to retry blindly.** Branch on `error.code`. A failed job is never charged. Only `processing_error` and `stalled` are worth one new upload; the others need a different file or a top-up.
- **At most 3 jobs can be queued or running per account.** A further start returns 429 `too_many_batches` with `Retry-After`; wait for a job to finish instead of looping.
- **Do not retry 409 or 410.** `batch_not_finished` means poll first; `batch_not_startable`, `batch_in_progress` and `result_gone` describe the job's state, which a retry does not change.
- **Download soon.** `result.expires_at` says when the file is deleted. With `delete_after_download=true` the first download is the only one: write it to durable storage before acknowledging.
- **Read the result by position, not only by header.** The result is the uploaded file with `gender`, `probability`, `sample_size`, `country`, `source`, `matched_as`, `first_name`, `middle_name`, `last_name` and `name_type` appended as the last ten columns. If the upload already had a column with one of these names (for example `country`), the result has two columns with that header; the appended ones are always the last ten.
- **Strip the UTF-8 byte order mark** when parsing a CSV result (for example `utf-8-sig` in Python). It is there so that Excel opens the file correctly.
- **Jobs belong to the account, not to the key.** `GET /batches` also lists jobs started from the dashboard or with other keys; filter by `source` or by the IDs you stored.
- **For fewer than a few hundred values, use `/gender/bulk` instead**: it answers in one request without a job.

## Errors

Every failure, at every status code, returns the same JSON body. Fields: `error`, `message`, `request_id`, `docs`, `errors`, `retry_after`. The HTTP status code is the success signal; there is no success flag in the body. Branch on `error`, never on `message`.

```json
{
    "error": "no_credits",
    "message": "You are out of credits. Buy more or wait for your daily free quota to reset.",
    "request_id": "req_9c1f5b2a7e0d4a13",
    "docs": "https://namegender.com/docs#error-no_credits"
}
```

| `error` | HTTP | Meaning |
| --- | --- | --- |
| `missing_key` | 401 | No API key was sent. Add an Authorization: Bearer header or an X-Api-Key header to the request. |
| `invalid_key` | 401 | This API key is not valid. |
| `revoked_key` | 401 | This API key has been revoked. |
| `blocked` | 403 | This account has been suspended. Contact support. |
| `email_not_verified` | 403 | This account's email address has not been confirmed yet. Open the confirmation link we emailed you, or request a new one from your dashboard. |
| `ip_not_allowed` | 403 | Requests from this IP address are not allowed for this key. |
| `proxy_secret_required` | 403 | This key only works through its API marketplace, which signs every request. Use the key issued to you there. |
| `forbidden` | 403 | You are not allowed to perform this action. |
| `no_credits` | 402 | You are out of credits. Buy more or wait for your daily free quota to reset. |
| `missing_input` | 400 | The "<field>" parameter is required. |
| `invalid_input` | 422 | The "<field>" parameter is not valid. |
| `too_many_items` | 422 | A maximum of 100 items can be sent in one request. |
| `ai_consent_required` | 422 | ai_fallback sends the name to a third-party AI provider, which this account has not agreed to. See the "ai" field for who that provider is, then enable AI lookups in your dashboard settings. |
| `unknown_endpoint` | 404 | There is no endpoint at this path. Check the URL against the API documentation. |
| `method_not_allowed` | 405 | This endpoint does not accept <METHOD> requests. |
| `payload_too_large` | 413 | That request body is too large. |
| `rate_limited` | 429 | Too many requests. Slow down and try again shortly. |
| `batch_not_found` | 404 | There is no file job with this ID on this account. |
| `batch_not_startable` | 409 | This job has already been started or closed. |
| `batch_not_finished` | 409 | This job has not finished yet. Check its status and download the result once it is completed. |
| `batch_in_progress` | 409 | This job is being processed and cannot be cancelled now. Delete it once it has finished. |
| `result_gone` | 410 | This job has no result to download. It failed, was cancelled, expired, or its single-download result has already been fetched. |
| `too_many_batches` | 429 | You already have 100 file jobs running. Wait for one to finish before starting another. |
| `not_ready` | 503 | The name database is being rebuilt. Try again in a moment. |
| `server_error` | 500 | Something went wrong on our side. We have been notified. |

## Implementation rules

- **Keep the key on the server.** Read it from `NAMEGENDER_API_KEY`. Never put it in browser, mobile or desktop client code, in logs, or in URLs; send it in the `Authorization: Bearer` header. The API does not read keys from the query string or the request body.
- **Do not keep names you do not need.** If the names are personal data, turn on "Don't store the names this key sends" for the key in the dashboard: the input and resolved name are then never written to the request history. Send POST so the names also stay out of web server access logs.
- **Unknown is an answer, not an error.** HTTP 200 with `gender: null` means the evidence is insufficient. Store it as unknown. Do not replace it with a default gender, do not retry it, and do not raise an exception.
- **Branch on `error`, never on `message`.** Messages are translated and can change. Treat an `error` value you do not recognise as a generic failure instead of crashing.
- **Retry only what can succeed on retry.** On 429, wait the number of seconds in the `Retry-After` header (also `retry_after` in the body), then retry. On 503 and 500, retry at most 3 times with exponential backoff (for example 1 s, 2 s, 4 s). Never retry 400, 401, 402, 403, 404, 405, 413 or 422: fix the request, the key or the account instead.
- **Stop on 402 `no_credits`.** Surface it to whoever runs the job. The daily free quota resets at midnight UTC; purchased credits stay on the balance until they are spent.
- **Stay under the rate limit.** The default is 1200 requests per minute per API key. Watch `X-RateLimit-Remaining` and cap concurrency; do not fire unbounded parallel requests.
- **Batch instead of looping.** For more than one value, use `POST https://namegender.com/api/v1/gender/bulk` with up to 100 values per request instead of calling a single-value endpoint in a loop.
- **Set a client timeout** (for example 10 seconds). Lookups usually take a few milliseconds; a request that hangs should fail rather than block a worker.
- **Encode input as UTF-8** and let the HTTP client build the query string or JSON body. Do not concatenate raw names into URLs: non-Latin names such as `محمد` or `中村` must be percent-encoded.
- **Log `request_id`** (also returned as the `X-Request-Id` header) with every failure. You may send your own `X-Request-Id` to correlate calls with your tracing.
- **Store `data_version` next to results** you persist, so a changed answer between two runs can be traced to a dataset update.
- **Use results responsibly.** A name-based result is a statistical association, not a person's gender identity. Keep unknown results visible, let people correct their own data, and do not use inferred gender for employment, medical, financial, insurance, legal or eligibility decisions.

## Verify the integration

- Upload a three-row CSV with `start=false` and confirm your code reads `inspection.columns` and `inspection.credits_needed`.
- Start with a `name_column` that is not in the file and confirm your code surfaces the 422 and its `columns` list instead of retrying.
- Send the same upload twice with the same `Idempotency-Key` and confirm both calls return the same `id`.
- Poll a job to `completed` using `poll_after_seconds`, download the result, and confirm the appended columns are read by position, not only by header.
- Confirm a job with `status: failed` is reported with its `error.code` and is not re-uploaded automatically.
- Call with an invalid key and confirm your code reports 401 `invalid_key` without retrying.
- Search the client-side bundle and the logs for `ng_live_` and confirm the key does not appear.
