# NameGender API: Which countries a name is recorded in

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

## Your task

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

This is not a "country of origin" lookup, and it is deliberately not named like one: the data cannot support that claim, and the difference is measurable.

Counted name statistics exist for eleven countries only: birth registrations in the US, UK, France, Canada, Spain, Ireland, Norway, Austria and Israel, and living-resident counts in Poland and Switzerland. Everything else is presence data — it says a name occurs in a country, not how many people carry it. So a query for `Mehmet` ranks Switzerland and France first, at about 29% each, and Turkey does not appear in the ranking at all, because Turkey publishes no counts.

The response therefore keeps the two apart. `registrations` is measured volume and is comparable, but only among the counting countries. `attested_in` is presence, and that is where Turkey, Japan and India show up. Neither field is a claim about any person's origin or ethnicity.

**Credits.** One credit per request. `ai_fallback` and `best_guess` do not apply here and are ignored.

## 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 | yes | The first name to look up. At most 200 characters. |
| `limit` | integer | no | How many counted countries to list in `registrations`. Shares and `basis.counted_countries` are not affected, and `attested_in` is never cut. Range 1–100. Default `25`. |

## Example requests

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

curl -X POST "https://namegender.com/api/v1/gender/countries" \
  -H "Authorization: Bearer $NAMEGENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Mehmet"}'
```

## Response

HTTP 200. Country distribution, with the basis of the numbers stated in the response itself.

| Field | Type | Description |
| --- | --- | --- |
| `credits_charged` | integer | How many credits this call took from the balance: always 1. |
| `credits_remaining` | integer | Balance once this call is paid for. |
| `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. |
| `took_ms` | integer | Whole milliseconds spent building the distribution on our side, network time excluded. |
| `name` | string | The name that was looked up. |
| `basis` | object | What the numbers rest on. Carried in the response rather than left to the documentation, because the person reading a percentage is not reading the docs. |
| `basis.counted_sources` | string[] | The statistical offices that publish observation counts. |
| `basis.counted_countries` | integer | Countries with counted registrations for this name, before `limit` is applied. Can be larger than the length of `registrations`. |
| `basis.attested_countries` | integer | Countries in `attested_in`, which `limit` never cuts. |
| `basis.note` | string | Plain-language caveat. Show it next to the percentages. |
| `registrations` | object[] | Countries with counted registrations, most first, cut to `limit`. Shares are calculated across every counted country before the cut, so they sum to 100 only when nothing is cut. |
| `registrations[].country` | string | ISO 3166-1 alpha-2. |
| `registrations[].count` | integer | Observed registrations. |
| `registrations[].share` | number | This country's percentage of the name's registrations across all counting countries, including any that `limit` cut from the list. |
| `registrations[].gender` | string \| null | Majority gender for the name in this country. |
| `registrations[].probability` | integer | Share, 0–100, carrying that gender in this country. |
| `registrations[].source` | string | Statistical office the count comes from. |
| `attested_in` | string[] | Every country the name occurs in, counted or not, alphabetically. |

Example:

```json
{
    "credits_charged": 1,
    "credits_remaining": 49999,
    "data_version": "2026.10",
    "request_id": "req_6b2d90e14fa37c58",
    "took_ms": 3,
    "name": "Mehmet",
    "basis": {
        "counted_sources": [
            "ssa",
            "ons",
            "nrs",
            "nisra",
            "insee",
            "statcan",
            "ine",
            "cso",
            "ssb",
            "statat",
            "cbs",
            "pesel",
            "bfs"
        ],
        "counted_countries": 8,
        "attested_countries": 16,
        "note": "Shares are calculated only across countries that publish counted name statistics…"
    },
    "registrations": [
        {
            "country": "CH",
            "count": 3890,
            "share": 29.45,
            "gender": "male",
            "probability": 100,
            "source": "bfs"
        },
        {
            "country": "FR",
            "count": 3775,
            "share": 28.58,
            "gender": "male",
            "probability": 100,
            "source": "insee"
        }
    ],
    "attested_in": [
        "AL",
        "AT",
        "AU",
        "BE",
        "CA",
        "CH",
        "ES",
        "FR",
        "GB",
        "KS",
        "MK",
        "NO",
        "PL",
        "SE",
        "TR",
        "US"
    ]
}
```

## Notes for this endpoint

- This is not a country-of-origin or ethnicity endpoint. Never label its output that way in a user interface or a data export.
- Keep `registrations` and `attested_in` apart. `share` values are comparable only among counted countries; `attested_in` records presence without any weight, so do not compute shares from it.
- `limit` shortens `registrations` without recalculating `share`: shares stay relative to every counted country, so a cut list does not sum to 100. Do not rescale them, and read `basis.counted_countries` as the uncut count.
- Show the `basis.note` text, or an equivalent, wherever the percentages are displayed.
- Each request costs one credit. `ai_fallback` and `best_guess` do not apply to this endpoint.

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