Prerequisites
- A recent stable Rust toolchain (2021 edition)
- An async runtime such as Tokio
- An ip-api.io API key — get one free
Add the crate
Add the official ip-api-io crate (and a runtime) to your project. The
crate name uses dashes; the import path uses underscores (ip_api_io).
cargo add ip-api-io
cargo add tokio --features full
# ...or in Cargo.toml:
# [dependencies]
# ip-api-io = "1"
# tokio = { version = "1", features = ["full"] } Construct the client
Bring the client into scope and build one with your API key. Read the key from an
environment variable — never hardcode it. The API rejects keyless requests with
401.
use ip_api_io::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("IP_API_IO_KEY")?);
// ... use the client ...
Ok(())
}
The client wraps a reqwest connection pool and is cheap to
clone — share it across tasks instead of building a new one per request.
Look up an IP address
Call lookup_ip(ip) for any IPv4 or IPv6 address, or lookup()
to resolve the caller's own IP. Responses are typed structs; nullable fields are
Option<T> — match before use.
let info = client.lookup_ip("8.8.8.8").await?;
println!("{}", info.ip); // "8.8.8.8"
if let Some(isp) = &info.isp {
println!("{isp}"); // "Google LLC"
}
if let Some(country) = &info.location.country {
println!("{country}"); // "United States"
}
println!("{}", info.suspicious_factors.is_datacenter); // true
// Resolve the caller's own IP:
let me = client.lookup().await?;
println!("{}", me.ip); country and city are Option<String>
because they're absent for private ranges or unrecognized addresses. Boolean flags such
as info.suspicious_factors.is_vpn are plain bool values.
Detect VPN, proxy, Tor & batch lookups
The suspicious_factors struct carries seven boolean fields on every
lookup — this is what sets ip-api.io apart from plain geolocation APIs. You get
security intelligence in the same call, at no extra cost.
let info = client.lookup_ip("185.220.101.45").await?; // example Tor exit node
let f = &info.suspicious_factors;
if f.is_vpn || f.is_proxy || f.is_tor_node {
println!("Anonymized traffic — consider a CAPTCHA or block");
}
if f.is_datacenter {
println!("Cloud/datacenter IP — likely automated");
}
if f.is_threat {
println!("Active threat signal — block this request");
}
let risky = f.is_vpn || f.is_proxy || f.is_tor_node || f.is_threat;
Need to check many IPs at once? lookup_batch takes up to 100 addresses in
a single request (it returns an Error if the slice is empty or longer
than 100):
let batch = client
.lookup_batch(&["8.8.8.8", "1.1.1.1", "9.9.9.9"])
.await?;
println!("{} {}", batch.total_processed, batch.successful_lookups);
for (ip, info) in &batch.results {
println!("{ip} {}", info.suspicious_factors.is_vpn);
} client.risk_score_ip(ip).await? for a combined
fraud risk score from
0 to 100.
Production-ready error handling
Every method returns Result<T, Error>. match on the
Error enum — the client never retries on its own, so on a
rate limit the RateLimit variant's reset tells you exactly
when to try again.
use ip_api_io::Error;
match client.lookup_ip("8.8.8.8").await {
Ok(info) => println!("{}", info.ip),
Err(Error::RateLimit { remaining, reset, .. }) => {
println!("Rate limited. Remaining: {remaining}. Resets at {reset}");
}
Err(Error::Authentication { .. }) => {
println!("Invalid or missing API key — get one free at https://ip-api.io");
}
Err(Error::InvalidRequest { message, .. }) => {
println!("Bad request: {message}");
}
Err(Error::Server { .. }) => {
println!("ip-api.io is having issues — try again later");
}
Err(Error::Transport(e)) => {
println!("transport / decode error: {e}");
}
} RateLimit variant's reset (a Unix timestamp) to
schedule the next attempt. Transport failures (DNS, connect, timeout, decode) surface
as Error::Transport rather than an API status error.
In an Axum handler, run the lookup server-side and pass the real client IP from the
X-Forwarded-For header:
async fn check(
State(client): State<Client>,
headers: HeaderMap,
) -> Json<serde_json::Value> {
let client_ip = headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split(',').next())
.map(str::trim)
.unwrap_or("0.0.0.0");
match client.lookup_ip(client_ip).await {
Ok(info) => Json(json!({
"ip": info.ip,
"country": info.location.country,
"is_vpn": info.suspicious_factors.is_vpn,
"is_threat": info.suspicious_factors.is_threat,
})),
// Treat ip-api.io as a non-critical dependency
Err(_) => Json(json!({ "ip": client_ip, "country": null })),
}
}