Rust SDK

ip-api.io Rust SDK
cargo add ip-api-io

ip-api-io is the official async client for Rust — built on tokio and reqwest, with serde-typed responses. One client wraps IP geolocation, VPN/proxy/Tor detection, email validation, and fraud risk scoring, returning Result<T, Error> with a typed error enum and batch helpers, so you write the call, not the plumbing.

Location
Mountain View, US
Threat signals
No VPN or proxy
Runtime
Async — Tokio
client.lookup_ip("8.8.8.8").await?
{
"ip": "8.8.8.8",
"isp": "Google LLC",
"location": {
"country": "United States",
"city": "Mountain View",
"timezone": "America/Los_Angeles"
},
"suspicious_factors": {
"is_vpn": false,
"is_datacenter": true
}
}

What you'll build

  • Add the crate and construct one async client
  • Look up any IP — or the caller's IP — with one call
  • Detect VPN, proxy, Tor, and threats; batch up to 100 IPs
  • Match the typed error enum the production-ready way
Trusted by thousands of businesses
Fast JSON API responses
Real-time validation
Simple integration, SDKs & examples

Prerequisites

  • A recent stable Rust toolchain (2021 edition)
  • An async runtime such as Tokio
  • An ip-api.io API key — get one free
1

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 = &quot;1&quot;
# tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }
2

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.

3

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);
Option for nullable fields: location fields like 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.
4

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);
}
Tip: want a single block / review / allow decision instead of raw flags? Use client.risk_score_ip(ip).await? for a combined fraud risk score from 0 to 100.
5

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}");
    }
}
No automatic retries: wrap calls in your own retry/backoff logic and use the 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 })),
    }
}
Reference

Response & method reference

lookup_ip resolves to a typed IpInfo with three parts: the top-level ip/isp/asn, a suspicious_factors struct, and a location struct. Nullable fields are Option<T>None for private ranges or unrecognized addresses.

client .lookup_ip("78.55.53.58").await?
{
  "ip": "78.55.53.58",
  "isp": "Deutsche Telekom AG",
  "asn": "AS3320",
  "suspicious_factors": {
    "is_proxy":      false,
    "is_tor_node":   false,
    "is_spam":       false,
    "is_crawler":    false,
    "is_datacenter": false,
    "is_vpn":        false,
    "is_threat":     false
  },
  "location": {
    "country":           "Germany",
    "country_code":      "DE",
    "city":              "Berlin",
    "latitude":          52.5694,
    "longitude":         13.3753,
    "zip":               "13409",
    "timezone":          "Europe/Berlin",
    "local_time":        "2024-05-20T22:16:52+02:00",
    "local_time_unix":   1716236212,
    "is_daylight_savings": true
  }
}

suspicious_factors — security signals (all bool)

FieldDescription
is_vpnVPN service, corporate gateway, or self-hosted VPN detected
is_proxyHTTP, HTTPS, or SOCKS proxy (~99.5% accuracy)
is_tor_nodeTor exit node, relay, or bridge (updated in real time)
is_datacenterCloud provider (AWS, GCP, Azure), VPS, or hosting facility
is_threatActive security threat — malware C&C, botnet, or DDoS source
is_spamAssociated with spam, phishing, or malware email campaigns
is_crawlerKnown web crawler, scraper, or bot

location — geographic and timezone data

FieldTypeDescription
countryOption<String>Full country name in English (ISO 3166-1)
country_codeOption<String>ISO 3166-1 alpha-2 code (e.g. "DE", "US")
cityOption<String>City or municipality name (85–95% accuracy)
latitudeOption<f64>Decimal degrees, WGS84 — ~50 km median accuracy radius
longitudeOption<f64>Decimal degrees, WGS84 — ~50 km median accuracy radius
zipOption<String>Postal or ZIP code in country-specific format
timezoneOption<String>IANA timezone identifier (e.g. "America/Los_Angeles")
local_timeOption<String>Current local time in ISO 8601 with UTC offset
local_time_unixOption<i64>Unix timestamp (seconds) in local timezone
is_daylight_savingsOption<bool>True if currently observing DST; None if not applicable

Client methods

MethodReturnsWhat it does
lookup() / lookup_ip(ip)IpInfoGeolocate the caller's IP, or a specific IP
lookup_batch(ips)BatchIpLookupGeolocate up to 100 IPs in one call
risk_score() / risk_score_ip(ip)RiskScoreFraud risk score (0–100) for an IP
tor_check(ip)TorDetectionCheck whether an IP is a Tor node
email_info(email)EmailInfoBasic email validation (syntax, MX, disposable)
validate_email(email)AdvancedEmailValidationFull validation incl. SMTP deliverability
asn(ip)AsnLookupAutonomous System Number lookup
whois(domain)WhoisWHOIS domain registration info
domain_age(domain)DomainAgeDomain registration date and age in days
rate_limit()RateLimitInfoRemaining quota and plan limits
Full surface: the client also exposes validate_email_batch, email_risk_score, ip_reputation, reverse_dns, forward_dns, mx_records, domain_age_batch, and usage_summary. See the README and per-feature guides.
FAQ

Frequently asked questions

Common questions about the ip-api.io Rust SDK.

Is the ip-api-io crate official?

Yes. ip-api-io is the official Rust client for ip-api.io, maintained by the ip-api.io team and published to crates.io from github.com/ip-api-io/ipapi-rust. It is MIT licensed and not affiliated with ip-api.com or ipapi.com.

How do I install the ip-api.io Rust SDK?

Run cargo add ip-api-io (or add ip-api-io = "1" to Cargo.toml), then bring the client into scope and construct it. The crate name uses dashes; the import path uses underscores:

// cargo add ip-api-io
use ip_api_io::Client;

let client = Client::new(std::env::var("IP_API_IO_KEY")?);

It targets a recent stable toolchain and an async runtime such as Tokio.

Is the client async?

Yes. Every method is async and returns a Result<T, Error>, so you .await? each call inside a Tokio (or other) runtime. The client is built on reqwest and is cheap to clone across tasks — it shares one connection pool.

How are nullable fields represented?

Responses are serde-derived structs. Nullable fields are Option<T> — match or if let Some(..) before use, e.g. if let Some(country) = &info.location.country { … }. Boolean flags such as info.suspicious_factors.is_vpn are plain bool.

How do I handle rate limits?

Every method returns Result<T, Error>. match on the Error enum for the RateLimit variant, which carries limit, remaining, and reset from the response headers. The client never retries automatically — schedule your retry after reset.

Pricing

Pricing

Start using IP-API.io to make your website safer and more user-friendly. Keep out unwanted bots, show visitors content that's relevant to where they are, and spot risky IP addresses quickly. It's perfect for making online shopping more personal and keeping your site secure. Get started today with one of the plans!

Small
€10 /mo
100,000 geo ip requests
10,000 advanced email validation requests
Location data
Email validation
Risk score calculation
Currency data
Time zone data
Threat data
Unlimited support
HTTPS encryption
Create Account to Subscribe
Medium
€20 /mo
300,000 geo ip requests
25,000 advanced email validation requests
Location data
Email validation
Risk score calculation
Currency data
Time zone data
Threat data
Unlimited support
HTTPS encryption
Create Account to Subscribe

Need support?

Explore how IP-API.io can enhance your security, provide robust bot protection, and improve IP geolocation accuracy for your applications.

Contact Support

Found a bug in the SDK?

The ip-api-io crate is open source. Open an issue or pull request on GitHub and we'll take a look.

Open on GitHub