Skip to content
Finowl

Rust · Gender prediction

Batch gender predictions in Rust with serde

This example uses /v1/gender/batch to send three items in one request: two names with country hints and one email address. The Item struct uses r#type because type is a reserved word in Rust, and skip_serializing_if drops the country field when it is None. Each result carries either data with a prediction or error with a code and status, and the code matches on both Options to cover every combination.

Batches are all or nothing at validation time, so a 400 means none of the items ran. A 200 can still contain item-level errors, and those items should be retried individually later rather than by resending the whole batch. The rate-limit loop resends only when a Retry-After header is present, because a rate_limited response means the batch never ran. Unknown predictions are printed with their reason.

Steps

  1. 1

    Paste the example into src/main.rs and export FINOWL_API_KEY.

  2. 2

    Build items with unique ids of up to 64 characters, at most 50 per request.

  3. 3

    Run cargo run and check that each id in the output matches an id you sent.

  4. 4

    Persist identified and unknown results, and queue only item-level errors for a later single retry.

  5. 5

    Show predictions in aggregate views, or as a suggestion that users can correct.

Install
cargo add reqwest --features json
cargo add serde --features derive
cargo add serde_json thiserror
cargo add tokio --features full
Batch gender predictions in Rust with serde
use reqwest::{header::RETRY_AFTER, Client, StatusCode};
use serde::{Deserialize, Serialize};
use std::{error::Error, time::Duration};

#[derive(Serialize)]
struct Item<'a> {
    id: &'a str,
    r#type: &'a str,
    value: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    country: Option<&'a str>,
}

#[derive(Deserialize)]
struct Prediction {
    result_status: String,
    gender: Option<String>,
    confidence: Option<f64>,
    reason: Option<String>,
}

#[derive(Deserialize)]
struct ItemError { code: String, status: u16 }

#[derive(Deserialize)]
struct ItemResult { id: String, data: Option<Prediction>, error: Option<ItemError> }

#[derive(Deserialize)]
struct BatchResponse { data: Vec<ItemResult> }

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let key = std::env::var("FINOWL_API_KEY")?;
    let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
    let items = [
        Item { id: "c1", r#type: "name", value: "Andrea", country: Some("IT") },
        Item { id: "c2", r#type: "name", value: "Jean", country: Some("fr") },
        Item { id: "c3", r#type: "email", value: "li.wei@example.com", country: None },
    ];

    let res = loop {
        let res = client.post("https://api.finowl.co/v1/gender/batch")
            .bearer_auth(&key).json(&serde_json::json!({ "items": items })).send().await?;
        if res.status() != StatusCode::TOO_MANY_REQUESTS { break res; }
        // rate_limited means the batch never ran, so resending it is safe.
        let Some(secs) = res.headers().get(RETRY_AFTER).and_then(|v| v.to_str().ok()?.parse().ok()) else {
            return Err("quota_exceeded: buy credits or wait for the monthly reset".into());
        };
        tokio::time::sleep(Duration::from_secs(secs)).await;
    };

    match res.status() {
        StatusCode::BAD_REQUEST => return Err("batch failed validation; no items ran".into()),
        StatusCode::UNAUTHORIZED => return Err("FINOWL_API_KEY is missing or invalid".into()),
        s if !s.is_success() => return Err(format!("Finowl returned {s}; do not resend blindly").into()),
        _ => {}
    }

    for item in res.json::<BatchResponse>().await?.data {
        match (item.data, item.error) {
            (Some(p), _) if p.result_status == "unknown" => println!("{}: unknown ({:?})", item.id, p.reason),
            (Some(p), _) => println!("{}: {} {:.2}", item.id, p.gender.as_deref().unwrap_or("-"), p.confidence.unwrap_or_default()),
            (None, Some(e)) => println!("{}: item error {} ({})", item.id, e.code, e.status),
            (None, None) => println!("{}: empty result", item.id),
        }
    }
    Ok(())
}

IN PRODUCTION

Production tips

  • Use your database primary key as the item id. Results come back in request order with index and id, so you can update rows without keeping a separate mapping table in memory.

  • Batch items default to ai_mode off, which keeps them dataset-only. The charged_credits value on each result tells you exactly what was billed for that item.

  • Spread large jobs over time. Each batch counts as one of your 60 requests per minute, so 50 items per request lets a single worker process thousands of names a minute.

Each lookup costs 1 credit (3 when it requests AI), with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Batch gender predictions in Rust with serde

Grab an API key, paste the example and run it. 100 free credits every month.