# NameGender API: Check one name

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#name-check.

## Your task

- Integrate `GET|POST https://namegender.com/api/v1/name-check` into this codebase.
- 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 HTTP call in one small server-side function or class. Read the API key from an environment variable named `NAMEGENDER_API_KEY`.
- Follow the language, HTTP client, logging and error-handling conventions already used in this codebase. If an official client library exists for this language (https://namegender.com/client-libraries), prefer it over hand-written HTTP code.
- Apply every rule under "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.

## Endpoint

- `GET https://namegender.com/api/v1/name-check` with query-string parameters
- `POST https://namegender.com/api/v1/name-check` with a JSON body

Says whether a name typed into a sign-up or lead form looks like a real person's name, and why. It catches keyboard mashing ("asdf qwerty"), placeholders ("Test Test", "John Doe", "n/a"), fictional characters ("Mickey Mouse"), profanity, digits, symbols and email addresses in the name field. One credit per name.

**It never calls a name fake.** The answer is an `assessment` (`plausible`, `suspicious` or `implausible`), a `score` from 0 to 100 and the `signals` behind it. Use it to flag a record or ask for more information, not to reject people automatically.

Real names that trip simpler filters are left alone: surnames such as Null, Test or Cockburn, two-letter names such as Ng, single names, and every script we cannot read. A first name found in official birth records raises the score; a name we have never seen does not lower it on its own. Surnames are judged by their shape only, because there is no surname dataset behind this endpoint.

## Request

Send the same fields either as query-string parameters (GET) or as a JSON body (POST with `Content-Type: application/json`). Prefer POST from server code: the input then stays out of access logs.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | The name as typed into the form. Send this or `first_name`/`last_name`. At most 200 characters. |
| `first_name` | string | no | At most 100 characters. |
| `last_name` | string | no | At most 100 characters. |
| `country` | string | no | Country hint, as on the gender endpoints. Used for name parsing and for the record lookup. At most 60 characters. |
| `locale` | string | no | At most 35 characters. |
| `ip` | string | no | At most 45 characters. |

## Example requests

```bash
curl "https://namegender.com/api/v1/name-check" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY"

curl -X POST "https://namegender.com/api/v1/name-check" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[]'
```

## Response

HTTP 200. Checked.

| Field | Type | Description |
| --- | --- | --- |
| `credits_charged` | integer |  |
| `credits_remaining` | integer |  |
| `data_version` | string \| null |  |
| `request_id` | string |  |
| `country_source` | string \| null | One of `country`, `locale`, `ip`, `null`. |
| `query` | string |  |
| `assessment` | string | `implausible`: at least one high-severity signal. `suspicious`: a medium signal or two low ones. Otherwise `plausible`. One of `plausible`, `suspicious`, `implausible`. |
| `score` | integer | Higher is more like a real name. For your own threshold. Range 0–100. |
| `signals` | object[] |  |
| `signals[].code` | string | One of `keyboard_pattern`, `repeated_characters`, `placeholder`, `placeholder_pair`, `fictional_character`, `profanity`, `contains_url_or_email`, `contains_digits`, `contains_symbols`, `organization_name`, `too_long`, `no_vowels`, `same_first_and_last`, `initials_only`, `single_name`, `first_name_not_found`, `first_name_attested`, `script_not_covered`. |
| `signals[].severity` | string | One of `high`, `medium`, `low`, `info`, `positive`. |
| `signals[].part` | string \| null | One of `full`, `first_name`, `last_name`, `null`. |
| `signals[].value` | string \| null |  |
| `first_name` | string \| null |  |
| `last_name` | string \| null |  |
| `name_type` | string | One of `personal`, `organization`, `role`. |
| `evidence` | object |  |
| `evidence.first_name_status` | string \| null | `counted`: in official birth statistics. `attested`: recorded without counts. `not_found`: not in our data. One of `counted`, `attested`, `not_found`, `null`. |
| `evidence.first_name_counted_records` | integer |  |

## Notes for this endpoint

- Never reject a person automatically on this result. Use `implausible` to block obvious junk only after a human or a second step agrees, and `suspicious` to flag a record for review or to ask for more information.
- Show or log the `signals` with the decision, so a wrongly flagged real name can be found and corrected.
- A name missing from our data is not a reason to reject it: `first_name_not_found` is low severity on purpose, and names in scripts we do not cover come back `plausible` with `script_not_covered`.
- Surnames are judged by their shape only; there is no surname dataset behind this endpoint.
- One credit per name. Use `/api/v1/name-check/bulk` for up to 100 names.

## 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

- Call with an invalid key and confirm your code reports 401 `invalid_key` without retrying.
- Simulate a 429 response with `Retry-After: 2` and confirm your code waits before retrying.
- Search the client-side bundle and the logs for `ng_live_` and confirm the key does not appear.
