Email extraction workflow · API V2

Extract names from email addresses with API V2

Build an email-to-name enrichment workflow with GenderAPI V2. Preserve original contact data, inspect the selected name signal, and handle unknown results and batch billing.

Can an email address provide a usable name?

Sometimes. An email's local part can contain a name candidate, but an extracted name is an inference rather than a verified contact name. Send type: email to the V2 gender endpoint and inspect data.name, data.match and data.source together. The same response includes a gender estimate when available; you do not need a second name lookup just to obtain that field.

This guide focuses on storing and reviewing enrichment in a contact workflow. To inspect one response interactively, use the email lookup tool. Unknown names and gender: null are normal outcomes that your workflow should retain.

Try the V2 API

Analyze an email

Dataset + AI

Submit one value and inspect the complete response, including unknown results and credit usage.

Always AI (default): 2 credits, including an unknown result. Fallback: 1 credit, including AI fallback or an unknown result. Off, dataset only without AI: 1 credit per completed lookup, including an unknown result. With nickname inference enabled: 1 credit for a usable dataset result, or 2 credits total for AI, including an unknown result.

The submitted full email address goes to GenderAPI.io and is sent to the configured AI service in Always AI mode (the default) or when fallback needs it, but not in Off mode. Processing details.

No saved API key. The shared IP trial is available while credits remain.

Real response captured on 1 October 2026 below. Select Analyze email to run a live lookup.

Real V2 response · captured 1 October 2026
NameMichael
GenderMale
Confidence98%
SourceAI model
Credits used2

This real response was captured on 1 October 2026; only the account balance is replaced. Live values may differ. A prediction is not verified identity.

Keep source data separate from inferred fields

Start with a stable contact ID and preserve the original email in your own record. Do not overwrite a name that a person has supplied with a candidate extracted from an address. Store inferred fields separately so that reviewers can see what changed, where it came from and whether the operation completed.

The field names in the first column below are an example application design, not additional API response fields. Choose storage and access rules appropriate for your application; these suggestions do not establish a service retention promise.

Suggested application fieldV2 evidence to retainHow to interpret it
contact_id / original_emailYour original record / data.input.valueKeep your source record and submitted value; do not use an inferred name as a unique identifier.
inferred_namedata.nameA selected or inferred name, possibly null. It is not a verified full name or a separate surname field.
name_matchdata.matchRetain candidate, method and scope. A substring or token match does not prove the mailbox owner's name.
gender_inferencedata.gender / data.result_status / data.reasonPreserve unknown outcomes as JSON null instead of substituting a guessed category.
inference_evidencedata.source / data.confidence_kind / data.confidence / data.sample_countKeep dataset counts separate from AI scores. Neither score alone verifies an individual's identity.
operation_recordmeta.request_id / meta.usageRetain the operation reference and billing outcome for troubleshooting and retry decisions.

Inspect a dataset-only email lookup first

This request makes the processing choice explicit: options.ai_mode: off uses the dataset only. It costs 1 credit when the operation succeeds, including an unknown result. Set GENDERAPI_API_KEY on your server and keep the key out of client-side application code. The sample address is an example input, not a guaranteed name or gender result.

Without a relevant country context, omit country. An email domain does not establish residence or nationality. POST sends the address in JSON instead of including it in the request URL.

Choose your language. Set up your API key, then run the example on your server.

Every prediction request is a new billable operation, including retries. These examples do not automatically retry. Check billing status before sending another request.

Before you run: access and error handling

Run these examples on your server. Set GENDERAPI_API_KEY in the process environment to your existing API key. Confirm meta.access.mode is api_key: an unrecognized key can fall back to the IP trial.

HTTP 4xx and 5xx JSON responses preserve the error body and return a nonzero exit status. Check code, action and meta.usage.billing_status before retrying.

Error and retry guide →
cURL
curl --silent --show-error --fail-with-body --max-time 30 \
  --request POST 'https://api.genderapi.io/api/v2/gender' \
  --header "Authorization: Bearer ${GENDERAPI_API_KEY:?Set GENDERAPI_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
  "type": "email",
  "value": "alice.smith@example.com",
  "options": {
    "ai_mode": "off"
  }
}'

cURL 7.76+ in a POSIX shell. Run in your terminal. Runtime documentation

JavaScript / Node.js
// Server-side Node.js. Save as example.mjs.
const apiKey = process.env.GENDERAPI_API_KEY;
if (!apiKey) throw new Error("Set GENDERAPI_API_KEY");
const body = {
  "type": "email",
  "value": "alice.smith@example.com",
  "options": {
    "ai_mode": "off"
  }
};

const response = await fetch("https://api.genderapi.io/api/v2/gender", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(body),
  signal: AbortSignal.timeout(30_000),
  redirect: "error",
});
const raw = await response.text();
if (!(response.headers.get("content-type") ?? "").includes("json")) {
  throw new Error(`HTTP ${response.status}: expected JSON; request ID ${response.headers.get("x-request-id")}`);
}
const result = JSON.parse(raw); // Also accepts application/problem+json.
if (!response.ok) {
  console.error(`HTTP ${response.status}`, result);
  process.exitCode = 1; // A retry is a new operation; inspect billing first.
} else {
  console.log(JSON.stringify(result, null, 2));
  // For batches, inspect every item: HTTP 200 may contain item errors.
}

Node.js 22+; built-in fetch. Save as example.mjs and run node example.mjs. Runtime documentation

Python
import json
import os
import sys
import urllib.error
import urllib.request

api_key = os.environ.get("GENDERAPI_API_KEY")
if not api_key:
    raise RuntimeError("Set GENDERAPI_API_KEY")
body = json.loads("{\"type\":\"email\",\"value\":\"alice.smith@example.com\",\"options\":{\"ai_mode\":\"off\"}}")

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

request = urllib.request.Request(
    "https://api.genderapi.io/api/v2/gender",
    method="POST",
    data=json.dumps(body).encode("utf-8"),
    headers={
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json",
    },
)
opener = urllib.request.build_opener(NoRedirect())
try:
    response = opener.open(request, timeout=30)
except urllib.error.HTTPError as error:
    response = error  # Keep the Problem Details body on non-2xx responses.
with response:
    status = response.status
    raw = response.read().decode("utf-8")
    if "json" not in response.headers.get("Content-Type", ""):
        raise RuntimeError(f"HTTP {status}: expected a JSON response")
    result = json.loads(raw)
print(json.dumps(result, indent=2), file=sys.stderr if status >= 300 else sys.stdout)
if not 200 <= status < 300:
    sys.exit(1)  # Inspect code, action and billing before retrying.
# For batches, inspect every item even when HTTP status is 200.

Python 3.10+; standard library. Save as example.py and run python3 example.py. Runtime documentation

PHP
<?php
$apiKey = getenv('GENDERAPI_API_KEY');
if (!$apiKey) {
    throw new RuntimeException('Set GENDERAPI_API_KEY');
}
$body = json_decode('{"type":"email","value":"alice.smith@example.com","options":{"ai_mode":"off"}}', true, 512, JSON_THROW_ON_ERROR);

$ch = curl_init('https://api.genderapi.io/api/v2/gender');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: '';
curl_close($ch);
if (strpos($contentType, 'json') === false) {
    throw new RuntimeException("HTTP $status: expected a JSON response");
}
$result = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
$output = json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL;
if ($status < 200 || $status >= 300) {
    fwrite(STDERR, $output); // Preserve the Problem Details body.
    exit(1);
}
echo $output;
// For batches, inspect every item; HTTP 200 can contain item errors.

PHP 8+ with the cURL extension. Save as example.php and run php example.php. Runtime documentation

Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

public class GenderApiExample {
    private static String requiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) throw new IllegalStateException("Set " + name);
        return value;
    }

    public static void main(String[] args) throws Exception {
        String apiKey = requiredEnv("GENDERAPI_API_KEY");
        String body = "{\"type\":\"email\",\"value\":\"alice.smith@example.com\",\"options\":{\"ai_mode\":\"off\"}}";
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .followRedirects(HttpClient.Redirect.NEVER)
            .build();
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.genderapi.io/api/v2/gender"))
            .timeout(Duration.ofSeconds(30))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
            .build();
        HttpResponse<String> response = client.send(request,
            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (!response.headers().firstValue("content-type").orElse("").contains("json")) {
            throw new IllegalStateException("HTTP " + response.statusCode() + ": expected JSON");
        }
        // JSON text; parse with your application's JSON library when integrating.
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            System.err.println(response.body()); // Includes Problem Details.
            System.exit(1);
        }
        System.out.println(response.body());
        // For batches, inspect each item's data or error, including on HTTP 200.
    }
}

Java 17+; standard HTTP client. Save as GenderApiExample.java and run java GenderApiExample.java. Runtime documentation

C# / .NET
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

string RequiredEnv(string name) =>
    !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(name))
        ? Environment.GetEnvironmentVariable(name)!
        : throw new InvalidOperationException($"Set {name}");

var apiKey = RequiredEnv("GENDERAPI_API_KEY");
using var handler = new HttpClientHandler { AllowAutoRedirect = false };
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.genderapi.io/api/v2/gender");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Content = new StringContent("{\"type\":\"email\",\"value\":\"alice.smith@example.com\",\"options\":{\"ai_mode\":\"off\"}}", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!(response.Content.Headers.ContentType?.MediaType?.Contains("json") ?? false))
    throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: expected JSON");
using var result = JsonDocument.Parse(raw);
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(result.RootElement); // Preserve Problem Details.
    Environment.ExitCode = 1;
}
else
{
    Console.WriteLine(result.RootElement);
    // For batches, inspect every item even on HTTP 200.
}

.NET 8+ console application. Use as Program.cs in a console project, then run dotnet run. Runtime documentation

Go
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

func requiredEnv(name string) string {
    value := os.Getenv(name)
    if value == "" { panic("Set " + name) }
    return value
}

func run() error {
    apiKey := requiredEnv("GENDERAPI_API_KEY")
    body := "{\"type\":\"email\",\"value\":\"alice.smith@example.com\",\"options\":{\"ai_mode\":\"off\"}}"
    request, err := http.NewRequest("POST", "https://api.genderapi.io/api/v2/gender", strings.NewReader(body))
    if err != nil { return err }
    request.Header.Set("Authorization", "Bearer " + apiKey)
    request.Header.Set("Content-Type", "application/json")
    client := &http.Client{
        Timeout: 30 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    response, err := client.Do(request)
    if err != nil { return err }
    defer response.Body.Close()
    raw, err := io.ReadAll(response.Body)
    if err != nil { return err }
    if !strings.Contains(response.Header.Get("Content-Type"), "json") || !json.Valid(raw) {
        return fmt.Errorf("HTTP %d: expected a JSON response", response.StatusCode)
    }
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        return fmt.Errorf("HTTP %d: %s", response.StatusCode, raw)
    }
    fmt.Println(string(raw))
    // For batches, inspect every item even on HTTP 200.
    return nil
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err) // Keep Problem Details for error handling.
        os.Exit(1)
    }
}

Go 1.22+; standard library. Save as main.go and run go run main.go. Runtime documentation

Review what the selected name actually means

Dataset matching uses the part before @, removes a plus-addressing suffix, normalizes text and looks for name candidates. Ordinary email lookup can add bounded prefix and suffix candidates for a concatenated local part. When several records match, the API selects the available record with the largest stored total; that is not necessarily the intended first name.

If a country is supplied, each candidate prefers an available country row and otherwise can use its global row. data.match.scope describes the selected lookup. data.country and country_source describe a returned association; they do not verify the contact's nationality. Role addresses such as support@example.com and opaque identifiers may have no usable personal-name signal.

ResultWorkflow decision
Dataset matchInspect match.name and match.method before accepting inferred_name. Keep any existing person-supplied name unchanged.
AI resultThe final response has no selected dataset row; match.method is model_inference and sample_count is null. Evaluate this evidence separately.
Unknown genderKeep gender as null and inspect reason. A tied dataset row can still contain a name and sample_count; absence of a gender does not always mean absence of a name candidate.
Nickname inference with name: nullforceToGenderize can return a gender association without a real given name. It has not extracted a contact name for your CRM.

Choose additional inference deliberately

A single lookup defaults to ordinary AI fallback unless you set off. Ordinary fallback tries the dataset first and costs 1 credit total, including AI when needed. always asks AI directly and costs 2. forceToGenderize permits nickname interpretation without a real given name: a resolved dataset result costs 1 credit; an AI result costs 2 total. Successful unknown results are charged in each mode.

When forceToGenderize is true, omit ai_mode or use fallback; off and always conflict with it. Nickname mode does not add the ordinary substring candidates. If AI runs, it receives the submitted email value, including the domain and plus suffix even though the prompt instructs it to ignore those portions. Use the published processing documentation when choosing a mode for your application.

Apply the workflow to a contact list

  • Split API-key jobs into batches of at most 50 items, or at most 10 for the shared IP trial. Each item uses type: email and its own value, country and options.
  • Give every item a unique id within its batch and keep your own source-row mapping. A returned index is zero-based within that submitted batch.
  • Set fallback explicitly on batch items when you want ordinary AI fallback; the batch default is off unless forceToGenderize enables nickname fallback.
  • Read each item even when HTTP status is 200. Store successful identified and unknown results; keep operational errors separate.
  • Before retrying an item, check meta.usage.billing_status and the error guidance. With confirmed billing, retry only failed items whose errors warrant another attempt. A network failure or unconfirmed billing is a reason to pause and reconcile, not to resubmit the entire list blindly.

Frequently asked questions

Does V2 extract a verified first name and surname from an email?

No. data.name is a selected or inferred name candidate and may be null. V2 does not provide a separate verified surname field. Preserve any name the person supplied and store the candidate separately.

Is an email lookup that returns null free?

A successful unknown lookup is billable. Dataset-only and ordinary AI fallback cost 1 credit total; always costs 2. With forceToGenderize, a resolved dataset result costs 1 and AI costs 2 total. Operational failures require checking the returned billing status.

Can I use the same workflow for bulk email lists?

Yes. POST /api/v2/gender/batch accepts up to 50 items with API-key access or 10 with the IP trial. Set options per item, map results by id or index, and inspect every item for a prediction or an error.

Published June 14, 2025Last reviewed

Connect your application

Use the complete V2 contract

Choose request options and handle both identified and unknown outcomes.

Continue testing

Use your GenderAPI key

Paste the API key from your GenderAPI account to keep testing after this IP address has used its shared 10-credit trial for the current 24-hour window.

Stored only for this browser tab. It is cleared when the tab is closed.