# NameGender API: Gender from a username

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

## Your task

- Integrate `GET|POST https://namegender.com/api/v1/gender/username` 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/gender/username` with query-string parameters
- `POST https://namegender.com/api/v1/gender/username` with a JSON body

Digits, separators and common suffixes are stripped before lookup.

## 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 |
| --- | --- | --- | --- |
| `username` | string | yes | The value to resolve. At most 200 characters. |
| `country` | string | no | ISO-3166 alpha-2 code. Narrows the lookup to one country, which matters for names that flip gender across borders: Andrea is male in Italy and female in Germany. Omit it and the answer is the worldwide aggregate. Exactly 2 characters. |
| `ai_fallback` | boolean | no | Ask a language model when the dataset has no answer. Slower and less certain; the response reports `source: "llm"` so you can tell these apart. Requires AI lookups to be enabled in account settings; otherwise the call fails with `ai_consent_required`. Default `false`. |
| `best_guess` | boolean | no | Applied to the first name extracted from the username: return its most likely gender even below the confidence threshold, instead of null. Check `probability` before trusting the result. Default `false`. |

## Example requests

```bash
curl "https://namegender.com/api/v1/gender/username?username=jane_doe_92" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY"

curl "https://namegender.com/api/v1/gender/username?username=jane_doe_92&country=TR" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY"

curl -X POST "https://namegender.com/api/v1/gender/username" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username":"jane_doe_92","country":"TR"}'
```

## Response

HTTP 200. Resolved. `gender` is null when the name is not recognised — that is a successful answer of "we do not know", not an error, and it still costs one credit.

| Field | Type | Description |
| --- | --- | --- |
| `credits_charged` | integer | How many credits this call took from the balance: one per value resolved. |
| `credits_remaining` | integer | Balance once this call is paid for: purchased credits plus whatever is left of today's free quota. |
| `data_version` | string \| null | Name-dataset snapshot that produced the answer. |
| `request_id` | string \| null | Identifier of this call, also sent as the `X-Request-Id` header. Quote it in support requests. |
| `query` | string | The input exactly as you sent it. |
| `name` | string \| null | The first name actually looked up, after stripping titles and surnames. |
| `first_name` | string \| null | Given name parsed from the input. Present even when `gender` is null, and not charged separately. Titles and initials are never returned. For Chinese, Korean and Japanese script the surname comes first in the input and is returned in `last_name`. |
| `middle_name` | string \| null | Any further given names between the first name and the surname, for example "Fitzgerald" in "John Fitzgerald Kennedy". Initials are dropped. |
| `last_name` | string \| null | Surname parsed from the input, including particles such as "van" or "de la". "Smith, John" is read as surname first. With a Spanish-speaking `country` the last two words are both surnames (Pérez García). Always null for usernames, where a surname cannot be told apart from any other word. |
| `name_type` | string \| null | `organization` when the input carries a company marker (Acme LLC, Müller GmbH, Stanford University); `role` for shared mailboxes and placeholder accounts (info@, Customer Service, guest). Both return `gender: null` and no name parts. `personal` means no such marker was found; it does not confirm the input is a real name. null when the input is not a name at all. One of `personal`, `organization`, `role`, `null`. |
| `gender` | string \| null | null means the dataset has no confident answer. This is not an error. One of `male`, `female`, `null`. |
| `probability` | integer | Share of people with this name who have the reported gender. 0 when unknown. Measured against held-out data, answers at 80+ come true about 95% of the time, so this number runs slightly conservative rather than optimistic. Range 0–100. |
| `sample_size` | integer | How many real people the answer is based on. A probability of 100 from a sample of 3 is weaker than 85 from a sample of 90,000 — read the two fields together. 0 means the source recorded proportions rather than counts. |
| `country` | string \| null | The country the answer came from, when narrowed. |
| `took_ms` | integer | Whole milliseconds spent resolving this value on our side; network time is not included. |
| `source` | string | How the answer was reached. `db` exact match; `script` matched after transliterating a non-Latin script; `fuzzy` closest spelling; `llm` language model; `none` no answer. Use it to decide how much to trust a row. One of `db`, `script`, `fuzzy`, `llm`, `cache`, `none`. |
| `confidence` | string | Sample size bucket: high ≥100 people, medium ≥25, low ≥1, unverified when the source gave proportions instead of counts. One of `high`, `medium`, `low`, `unverified`, `unknown`. |
| `matched_as` | string \| null | For `fuzzy` and `script` sources, the dictionary entry that was actually matched. Always inspect this before accepting a fuzzy result. |

Example: Recognised name:

```json
{
    "credits_charged": 1,
    "credits_remaining": 4821,
    "data_version": "2026.08",
    "request_id": "req_9c1f5b2a7e0d4a13",
    "query": "Ayşe Yılmaz",
    "name": "Ayşe",
    "first_name": "Ayşe",
    "middle_name": null,
    "last_name": "Yılmaz",
    "name_type": "personal",
    "gender": "female",
    "country": null,
    "sample_size": 655,
    "probability": 99,
    "took_ms": 1,
    "source": "db",
    "confidence": "high",
    "matched_as": null
}
```

Example: Name not in the dataset — a successful "we do not know":

```json
{
    "credits_charged": 1,
    "credits_remaining": 4820,
    "data_version": "2026.08",
    "request_id": "req_1d7e3c9048ba52f6",
    "query": "Qzzxvv",
    "name": "Qzzxvv",
    "first_name": "Qzzxvv",
    "middle_name": null,
    "last_name": null,
    "name_type": "personal",
    "gender": null,
    "country": null,
    "sample_size": 0,
    "probability": 0,
    "took_ms": 1,
    "source": "none",
    "confidence": "unknown",
    "matched_as": null
}
```

Example: Resolved by closest spelling — note the capped probability:

```json
{
    "credits_charged": 1,
    "credits_remaining": 4819,
    "data_version": "2026.08",
    "request_id": "req_44a0be71c3925fd8",
    "query": "Micheal",
    "name": "Micheal",
    "first_name": "Micheal",
    "middle_name": null,
    "last_name": null,
    "name_type": "personal",
    "gender": "male",
    "country": null,
    "sample_size": 152388,
    "probability": 86,
    "took_ms": 2,
    "source": "fuzzy",
    "confidence": "high",
    "matched_as": "michael"
}
```

## Notes for this endpoint

- Send the username unchanged; digits, separators and common suffixes are stripped server-side.
- Placeholder handles such as `user123`, `admin` or `guest` return `gender: null`. An ordinary word that happens to be attested as a name somewhere still resolves, for example `xX_gamer_Xx` as "Gamer" with `confidence: unverified`. For username input, treat `unverified` as unknown unless you have a reason to trust it.
- Read `probability` together with `sample_size` and `confidence`: 100 from 3 people is weaker than 85 from 90,000. Pick a threshold for this use case (for example `probability >= 80` and `confidence` of `high` or `medium`) and treat anything below it as unknown.
- When `source` is `fuzzy` or `script`, check `matched_as` before accepting the result. `llm` answers only appear when `ai_fallback` is true; label them as less certain.
- Use `first_name`, `middle_name` and `last_name` from the response instead of splitting names in your own code. They are returned even when `gender` is null and cost no extra credit. `last_name` is always null for usernames.
- `name_type` is `organization` or `role` when the input is a company or a shared mailbox; those rows return `gender: null` and still cost a credit, so filter them out of lists before sending when you can.
- Pass `country` whenever you know it. Some names change gender across borders: Andrea is male in Italy and female in Germany. Without it the answer is the worldwide aggregate.
- Leave `best_guess` off unless you apply your own `probability` threshold afterwards; it returns a guess where the API would otherwise return null.
- `ai_fallback` sends the value to a third-party AI provider and fails with 422 `ai_consent_required` until the account owner enables AI lookups in the dashboard. Leave it off unless the user asked for it.
- Each call resolves one value and costs one credit, unknown results included. For lists, use `POST https://namegender.com/api/v1/gender/bulk`.

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

- Send `jane_doe_92` and confirm `name` in the response shows the first name that was extracted.
- Send `user123` and confirm it is stored as unknown, and that `xX_gamer_Xx` is kept out by your `unverified` rule.
- 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.
