# NameGender API > Infer gender from names, emails and usernames, with the evidence behind each answer. 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. ## 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 `. - `X-Api-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 ### Gender from a name #### Endpoint - `GET https://namegender.com/api/v1/gender` with query-string parameters - `POST https://namegender.com/api/v1/gender` with a JSON body Pass a full name or a first name. Titles, punctuation and surnames are stripped before lookup; `name` in the response shows what was actually looked up. #### 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 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 | Return the 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?name=Ay%C5%9Fe%20Y%C4%B1lmaz" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" curl "https://namegender.com/api/v1/gender?name=Ay%C5%9Fe%20Y%C4%B1lmaz&country=TR" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" curl -X POST "https://namegender.com/api/v1/gender" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Ayşe Yılmaz","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 name as the user typed it. Titles, punctuation and surnames are stripped server-side, so do not split names yourself; `name` in the response shows what was actually looked up. - 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`. ### Gender from an email address #### Endpoint - `GET https://namegender.com/api/v1/gender/email` with query-string parameters - `POST https://namegender.com/api/v1/gender/email` with a JSON body The local part is split on separators and digits, then resolved as a name. `john.doe@example.com` resolves as "John". #### 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 | | --- | --- | --- | --- | | `email` | 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 email: 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/email?email=john.doe%40example.com" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" curl "https://namegender.com/api/v1/gender/email?email=john.doe%40example.com&country=TR" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" curl -X POST "https://namegender.com/api/v1/gender/email" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"john.doe@example.com","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 whole address. Only the local part is used: `john.doe@example.com` resolves as "John". - Role addresses (admin, info, sales, support, noreply and similar) and an initial followed only by a surname (`j.smith@example.com`) return `gender: null`. They still cost one credit each, so skip role addresses before sending when you can. - 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`. ### Gender from a username #### 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`. ### Resolve many names in one request #### Endpoint - `POST https://namegender.com/api/v1/gender/bulk` with a JSON body Exists so that 100 names do not cost 100 HTTP round trips. Names are resolved in a single batch lookup. Credits are checked for the whole batch up front: either every name is processed or none is. #### Request Send a JSON body with `Content-Type: application/json`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `names` | string[] | yes | The values to resolve, each at most 200 characters. Each one costs one credit. 1–100 items. | | `country` | string | no | ISO-3166 alpha-2 code applied to every value in the batch. Exactly 2 characters. | | `type` | string | no | How to read the values in `names`. One of `name`, `email`, `username`. Default `name`. | | `ai_fallback` | boolean | no | Ask a language model for values the dataset cannot answer. Requires AI lookups to be enabled in account settings; otherwise the batch fails with `ai_consent_required`. Default `false`. | | `best_guess` | boolean | no | Return the most likely gender even below the confidence threshold, instead of null. For `email` and `username` batches it applies to the extracted first name. Default `false`. | #### Example requests ```bash curl -X POST "https://namegender.com/api/v1/gender/bulk" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"names":["Ayşe","Mehmet","Andrea"],"country":"TR"}' ``` #### Response HTTP 200. Every name resolved. Results are returned in the order they were sent. | 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. | | `took_ms` | integer | Whole milliseconds spent resolving the batch on our side, network time excluded. | | `summary` | object | Counts for the whole batch. | | `summary.total` | integer | Values sent. | | `summary.identified` | integer | Values with a non-null `gender`. | | `summary.unknown` | integer | Values with `gender: null`. | | `summary.match_rate` | number | Percentage identified. | | `results` | object[] | Same order as the names you sent. | | `results[].query` | string | The input exactly as you sent it. | | `results[].name` | string \| null | The first name actually looked up, after stripping titles and surnames. | | `results[].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`. | | `results[].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. | | `results[].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. | | `results[].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`. | | `results[].gender` | string \| null | null means the dataset has no confident answer. This is not an error. One of `male`, `female`, `null`. | | `results[].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. | | `results[].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. | | `results[].country` | string \| null | The country the answer came from, when narrowed. | | `results[].took_ms` | integer | Whole milliseconds spent resolving this value on our side; network time is not included. | | `results[].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`. | | `results[].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`. | | `results[].matched_as` | string \| null | For `fuzzy` and `script` sources, the dictionary entry that was actually matched. Always inspect this before accepting a fuzzy result. | #### Notes for this endpoint - Send at most 100 values per request. Split longer lists into chunks of 100 and send the chunks sequentially or through a small bounded worker pool. - Set `type` to `email` or `username` when the values are not names. One request cannot mix types. Role addresses and placeholder handles return `gender: null` but still cost a credit, so skip them before sending. - `results` come back in the order you sent them. Join them to your records by position. - Credits are checked for the whole batch before any work starts: a 402 means nothing in that batch was processed or charged, so the whole chunk can be resent after topping up. - Every value costs one credit, unknown results included. Remove duplicates before sending and map the answers back afterwards. - 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. ### Which countries a name is recorded in #### 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 birth-registration statistics exist for seven countries only: the US, UK, France, Canada, Spain, Ireland and Norway. 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 France first at 59%, 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.08", "request_id": "req_6b2d90e14fa37c58", "took_ms": 3, "name": "Mehmet", "basis": { "counted_sources": [ "ssa", "ons", "insee", "statcan", "ine", "cso", "ssb" ], "counted_countries": 5, "attested_countries": 14, "note": "Shares are calculated only across countries that publish counted birth registration statistics…" }, "registrations": [ { "country": "FR", "count": 3775, "share": 58.97, "gender": "male", "probability": 99, "source": "insee" }, { "country": "GB", "count": 1130, "share": 17.65, "gender": "male", "probability": 99, "source": "ons" } ], "attested_in": [ "AL", "AU", "BE", "CA", "CH", "ES", "FR", "GB", "MK", "NO", "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. ### Credit balance and account status #### Endpoint - `GET https://namegender.com/api/v1/me` Costs no credits. Use it to show remaining balance in your own dashboard, or to check a key is live before a large batch. #### Example requests ```bash curl "https://namegender.com/api/v1/me" \ -H "Authorization: Bearer $NAMEGENDER_API_KEY" ``` #### Response HTTP 200. Account status. | Field | Type | Description | | --- | --- | --- | | `email` | string | Account email address. | | `credits_remaining` | integer | Purchased credits plus whatever is left of today's free quota. | | `purchased_credits` | integer | Paid balance. Credits stay on your balance until you spend them. | | `free_today` | integer | Free credits left today. Resets at midnight UTC. | | `free_daily_limit` | integer | Free credits granted per day. | | `lifetime_requests` | integer | Requests made with this account so far. | | `data_version` | string \| null | Current name-dataset snapshot. | #### Notes for this endpoint - Costs no credits. Call it before a large job to check that `credits_remaining` covers the job, and to confirm the key is valid. - Do not poll it in a tight loop; once per job or per dashboard view is enough. It counts toward the rate limit. ### File jobs (CSV and XLSX) #### 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 code per row. | | `country` | string | no | Default country (ISO 3166-1 alpha-2) for rows without one. Exactly 2 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 code per row. | | `country` | string | no | Default country (ISO 3166-1 alpha-2) for rows without one. Exactly 2 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. ### Webhooks #### Events Each event is a `POST` with a JSON body to the endpoint URL the account registered. Credit events are checked hourly and can arrive up to an hour late; they are a heads-up, not a balance feed. Dashboard: https://namegender.com/panel/webhooks. | Event | Summary | Body | | --- | --- | --- | | `batch.completed` | A file job completed | `data.object` is the job, exactly as `GET /api/v1/batches/{id}` returns it. `result.url` needs your API key to download. | | `batch.failed` | A file job failed | `data.object` is the job with `status: failed`; branch on `error.code`. A failed job is not charged. | | `credits.low` | Credits will run out within a week | Sent once when the balance, at the average paid usage of the last 14 days, will last 7 days or less. Subscription credits and an upcoming renewal are taken into account. Checked hourly, so it can arrive up to an hour after the threshold is crossed; for an exact balance use `credits_remaining` on any API response. | | `credits.depleted` | Credits ran out | Sent once when the balance reaches zero and requests are being refused with `402 no_credits`. Checked hourly. | | `webhook.test` | Test event from the dashboard | Sent only by "Send test event" in the dashboard, only to that endpoint. | #### Request headers | Header | Description | | --- | --- | | `NameGender-Signature` | `t=,v1=`. `v1` is HMAC-SHA256 of `"."` with the endpoint secret. During a secret rotation a second `v1` is present for 24 hours; accept the request if any `v1` matches. Reject a `t` more than 5 minutes from your clock. | | `NameGender-Event-Id` | Same as `id` in the body, and the same on every retry. Deduplicate on it. | | `NameGender-Delivery-Attempt` | 1 on the first try, then 2, 3… up to 8. | #### Body | Field | Type | Description | | --- | --- | --- | | `id` | string | Event id; stable across retries. | | `type` | string | New types can be added. Answer 2xx to a type you do not recognise and ignore it. | | `created_at` | string | | | `api_version` | string | | | `data` | object | | | `data.object` | object | | `data.object` for `batch.completed` and `batch.failed` is the file job, the same object `GET https://namegender.com/api/v1/batches/{id}` returns. For `credits.low` and `credits.depleted` it is a `CreditsAlert`: `kind`, `credits_remaining`, `purchased`, `subscription`, `daily_burn`, `runway_days`, `since`. #### Signature ```text header = "t=1727172264,v1=5f3c…,v1=9a01…" # one or two v1 values signed = t + "." + raw_body # raw bytes as received expected = hex(HMAC_SHA256(key=secret, msg=signed)) valid = any(constant_time_equal(expected, v) for v in v1_values) and abs(now - t) <= 300 ``` #### Rules for receiving webhooks - **Verify before anything else**, and answer 400 to a request that fails. Do not parse or act on an unverified body. - **Use the raw body.** Read the request body as bytes before any JSON middleware. Parsing and re-serialising changes whitespace, key order and escaping, and the signature will not match. In Express use `express.raw({ type: 'application/json' })` on this route; in Flask `request.get_data()`; in Django `request.body`; in PHP `file_get_contents('php://input')`. - **Compare in constant time** (`crypto.timingSafeEqual`, `hmac.compare_digest`, `hash_equals`), and accept the request if any `v1` matches: during a secret rotation there are two. - **Answer 2xx within 10 seconds, then do the work.** Queue slow processing. A redirect, a timeout or any non-2xx status is treated as a failure and retried. - **Deduplicate on `id`** (also the `NameGender-Event-Id` header). Retries carry the same id, and a request can be delivered more than once. Store processed ids and skip repeats. - **Ignore event types you do not handle.** New types are added over time, and an endpoint with no event filter receives them. Answer 2xx and do nothing; do not throw on an unknown `type`. - **Do not rely on order.** Read the job with `GET https://namegender.com/api/v1/batches/{id}` when the current state matters. - **Keep the secret on the server**, out of logs and out of client code, like the API key. - **Downloading the result needs the API key**, not the webhook secret: `result.url` is an authenticated API URL. Answer with any 2xx within 10 seconds, then do the work. Anything else (including a redirect) is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h; after 8 attempts the event is given up. Order is not guaranteed. ## 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 "" parameter is required. | | `invalid_input` | 422 | The "" 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 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. ## Per-endpoint instruction files - [Gender from a name](https://namegender.com/docs/ai/name.md) - [Gender from an email address](https://namegender.com/docs/ai/email.md) - [Gender from a username](https://namegender.com/docs/ai/username.md) - [Resolve many names in one request](https://namegender.com/docs/ai/bulk.md) - [Which countries a name is recorded in](https://namegender.com/docs/ai/countries.md) - [Credit balance and account status](https://namegender.com/docs/ai/account.md) - [File jobs (CSV and XLSX)](https://namegender.com/docs/ai/batches.md) - [Webhooks](https://namegender.com/docs/ai/webhooks.md)