Continue → Overview
← All notes

Using a name gender API in production: batching, retries and the unknown row

7 min read guide api batch

Running a name gender API in production comes down to five decisions: send names in batches, group them by country, retry only errors that can succeed on a second attempt, store the evidence fields next to the answer, and treat unknown as a valid value. Get those right and the integration is a few dozen lines. Get them wrong and the failures are quiet: wasted credits, a French list classified with US data, or a column where "unknown" was silently filled with a default.

The examples use the NameGender API, but most of the checklist applies to any name gender API. If you are still deciding what one does, start with what a name gender API is.

1. Keep the key on the server

Read the key from an environment variable and call the API from your backend. A key in browser or mobile code can be copied by anyone who opens the developer tools, and every lookup they make is charged to you. Send it in the Authorization: Bearer header; the API does not accept keys in the URL.

Prefer POST with a JSON body over GET. The names then stay out of web server and proxy access logs, which matters when they belong to your customers.

2. Deduplicate, then batch

Each value you send costs one credit, including repeats and names that come back unknown. Customer lists repeat first names heavily: a 200,000-row list typically has around 16,000 unique ones. Remove duplicates before sending and map the answers back afterwards.

Then use the bulk endpoint, which accepts up to 100 values per request. Results come back in the same order you sent them, so you can join them by position.

Group by country before batching. The country field applies to the whole batch. If your list mixes countries, send one set of batches per country and a separate set for rows with no country. Sending a mixed list with one country code gives every row that country's answer.

3. Retry only what can succeed

The HTTP status is the success signal. Branch on the machine-readable error field, never on the message text, which is translated and can change.

Status Meaning What to do
200 with gender: null No confident answer Store as unknown. Do not retry.
429 rate_limited Too many requests Wait for Retry-After seconds, then retry
500, 503 Server error or dataset reload Retry up to 3 times with backoff: 1 s, 2 s, 4 s
402 no_credits Balance used up Stop the job and alert someone
400, 401, 403, 422 Bad input, key or account problem Do not retry. Fix the request.

A 402 on the bulk endpoint means nothing in that batch was processed or charged, because credits are checked for the whole batch first. Once the balance is topped up, the same batch can be sent again. On a free account, the daily credits reset at midnight UTC.

The default rate limit is 1,200 requests per minute per key. With 100 names per request, that is far above what most jobs need, so a small fixed number of workers is enough. Do not start one request per row in parallel.

4. A minimal Python client

import os
import time

import requests

API_URL = "https://namegender.com/api/v1/gender/bulk"

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['NAMEGENDER_API_KEY']}"


def resolve_batch(names, country=None, max_attempts=4):
    body = {"names": names}
    if country:
        body["country"] = country

    for attempt in range(max_attempts):
        response = session.post(API_URL, json=body, timeout=10)

        if response.status_code == 200:
            return response.json()["results"]

        if response.status_code == 429:
            time.sleep(int(response.headers.get("Retry-After", "1")))
            continue

        if response.status_code in (500, 503) and attempt < max_attempts - 1:
            time.sleep(2 ** attempt)
            continue

        try:
            error = response.json().get("error", "unknown_error")
        except ValueError:
            error = "unknown_error"
        request_id = response.headers.get("X-Request-Id")
        raise RuntimeError(f"{response.status_code} {error} (request_id={request_id})")

    raise RuntimeError("Gave up after repeated rate limiting or server errors")


def resolve_names(names, country=None):
    unique = sorted({n.strip() for n in names if n and n.strip()})
    answers = {}

    for start in range(0, len(unique), 100):
        batch = unique[start:start + 100]
        for query, result in zip(batch, resolve_batch(batch, country)):
            answers[query] = result

    return answers

Call resolve_names once per country group. The returned dictionary maps each unique input to its full result, so you can write the fields back to every row that shares that name.

5. Store the evidence, not just the label

Save these fields together:

Field Why you will need it later
gender The answer, with null kept as null
probability and sample_size To change your threshold without calling the API again
confidence and source To tell counted records from spelling matches and uncounted data
matched_as To audit fuzzy matches such as Michealmichael
data_version To explain why an answer changed between two runs
request_id To trace a specific call with support

If you store only male or female, you cannot tighten the rule later, and you cannot find which rows were guesses. Never fill unknown with a default. Unknowns are not spread evenly: they cluster in particular countries and scripts, so filling them in adds a bias that is invisible afterwards.

6. Decide the threshold in code

Write the rule once, next to the code that uses the answer:

def usable_gender(result, min_probability=90):
    if result["gender"] is None or result["probability"] < min_probability:
        return None
    if result["source"] == "llm":
        return None
    return result["gender"]

Two trade-offs to decide for your own data:

  • confidence: "unverified" means the answer comes from records without counts. In markets with no published name statistics, such as Turkey, correct answers arrive this way. Rejecting all of them removes most answers for those countries.
  • source: "fuzzy" is usually a spelling variant, but check a sample of matched_as values from your own list before accepting them automatically.

Our guide to probability, confidence and sample_size explains how these fields relate to measured accuracy.

7. Cache results, and refresh them when the data changes

Apart from llm answers, the result for the same name, country and data version does not change. Cache results keyed on the normalised name and country, and store data_version with each entry. When the version in new responses changes, re-check the cached names that matter to you rather than all of them at once.

Before you go live

  • Test 200 to 500 rows from your real data where the answer is known, grouped by country.
  • Filter out role addresses such as info@ and support@ before sending email lists; they return unknown but still cost a credit.
  • Log request_id on every failure.
  • Show unknown as its own category in any report built on the results.
  • Do not use the result for decisions about employment, credit, insurance, health or eligibility.

Frequently asked questions

How many names can I send to a gender API in one request? The NameGender bulk endpoint accepts up to 100 values per request. For longer lists, split them into chunks of 100 and send them one after another or through a few parallel workers.

Should I retry when the API returns unknown? No. An unknown result is a complete answer: the data has no confident gender for that name. Retrying costs another credit and returns the same result.

Can I call a gender API from the browser? Not directly. The key would be visible to anyone using the page. Call the API from your server and pass only the result to the browser.

How do I handle a list with names from many countries? Group the rows by country, send each group with its own country code, and send rows without a country separately. One country code applies to every name in a batch.

Every claim on this page is measurable against your own list. The free tier is enough to check it.