Language · Rust
Call the Finowl API from Rust with reqwest
Rust's type system suits Finowl's API well. Serde derives turn breach and gender responses into structs with exactly the fields you use, and reqwest's async client, running on tokio, gives you connection pooling, TLS and request timeouts out of the box. A missing field or an unexpected type becomes a deserialization error you handle once, rather than a surprise three functions later. The examples compile as ordinary binaries and move easily into an Axum or Actix handler.
The client below holds one reqwest::Client and the key from FINOWL_API_KEY, and exposes a generic post method that serializes any request body and deserializes into whatever type you ask for. Errors come back as a thiserror enum, so a match can tell invalid input, a rejected key and a rate limit with its Retry-After duration apart from transport failures. Each API page shows a self-contained main function to paste into src/main.rs.
STEP 1
A reusable Rust client
Keep your key in the FINOWL_API_KEY environment variable and route every call through one helper, so authentication, timeouts and error handling live in one place.
cargo add reqwest --features json
cargo add serde --features derive
cargo add serde_json thiserror
cargo add tokio --features full// src/finowl.rs
use reqwest::{header::RETRY_AFTER, Client, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum FinowlError {
#[error("invalid input (400)")]
InvalidInput,
#[error("missing or invalid API key (401)")]
Unauthorized,
#[error("rate limited, retry after {0:?}")]
RateLimited(Option<Duration>),
#[error("Finowl API error {status}: {code}")]
Api { status: StatusCode, code: String },
#[error(transparent)]
Http(#[from] reqwest::Error),
}
#[derive(Deserialize)] struct ErrorBody { error: ErrorCode }
#[derive(Deserialize)] struct ErrorCode { code: String }
pub struct Finowl { http: Client, key: String }
impl Finowl {
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
let key = std::env::var("FINOWL_API_KEY")?;
let http = Client::builder().timeout(Duration::from_secs(15)).build()?;
Ok(Self { http, key })
}
pub async fn post<B: Serialize, T: DeserializeOwned>(&self, path: &str, body: &B) -> Result<T, FinowlError> {
let res = self.http.post(format!("https://api.finowl.co{path}"))
.bearer_auth(&self.key).json(body).send().await?;
let status = res.status();
if status.is_success() {
return Ok(res.json().await?);
}
let retry_after = res.headers().get(RETRY_AFTER)
.and_then(|v| v.to_str().ok()?.parse().ok()).map(Duration::from_secs);
let code = res.json::<ErrorBody>().await.map(|b| b.error.code).unwrap_or_default();
Err(match (status, code.as_str()) {
(StatusCode::BAD_REQUEST, _) => FinowlError::InvalidInput,
(StatusCode::UNAUTHORIZED, _) => FinowlError::Unauthorized,
(StatusCode::TOO_MANY_REQUESTS, "rate_limited") => FinowlError::RateLimited(retry_after),
_ => FinowlError::Api { status, code },
})
}
}STEP 2
Call the API you need

Check email breaches in Rust with reqwest and serde
Breach records arrive with PascalCase keys like Title, BreachDate and DataClasses. In Rust you keep idiomatic snake_case field names and add #[serde(rename_all = "PascalCase")] to the Breach struct, and serde maps breach_date to BreachDate and data_classes to DataClasses for you. Declare only the fields you need; serde ignores unknown keys by default, so the struct stays small and focused on what your code reads.

Check breached passwords in Rust with reqwest
The Rust password example borrows the password as &str and serializes it through a small PasswordQuery struct, so the value is never copied into a long-lived String or formatted into an error. Finowl hashes it with SHA-1, searches by a five-character prefix with k-anonymity and padding, and returns breached and count. The breach_count function returns Result<u64, CheckError>, which keeps the caller's handling explicit.

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.
A FEW GOOD QUESTIONS
Rust questions
Other languages and frameworks
Ship with Rust today
100 free credits every month. Try every request in the sandbox first.