Rust client for the ipdata.co IP geolocation and threat intelligence API.
Add to your Cargo.toml:
[dependencies]
ipdata = "0.1"let client = ipdata::IpData::new("your-api-key");
let info = client.lookup("8.8.8.8").await?;
println!("{} is in {}", info.ip, info.country_name.unwrap_or_default());let info = client.lookup_self().await?;Request only specific fields to reduce response size:
let info = client.lookup_fields("8.8.8.8", &["ip", "country_name", "asn"]).await?;let asn = client.lookup_field("8.8.8.8", "asn").await?;Look up to 100 IPs in a single request (requires a paid API key):
let results = client.bulk(&["8.8.8.8", "1.1.1.1"]).await?;
for info in &results {
println!("{} -> {}", info.ip, info.country_name.as_deref().unwrap_or("unknown"));
}Get specific data with strongly-typed return values:
let asn = client.asn("8.8.8.8").await?;
println!("ASN: {} ({})", asn.asn, asn.name);
let threat = client.threat("8.8.8.8").await?;
println!("Is threat: {}", threat.is_threat);
let tz = client.time_zone("8.8.8.8").await?;
println!("Timezone: {}", tz.name);
let currency = client.currency("8.8.8.8").await?;
println!("Currency: {} ({})", currency.name, currency.code);
let carrier = client.carrier("8.8.8.8").await?;
println!("Carrier: {}", carrier.name);Use the EU-specific endpoint backed by datacenters in Frankfurt, Paris, and Ireland:
let client = ipdata::IpData::eu("your-api-key");let client = ipdata::IpData::with_base_url("your-api-key", "https://custom-endpoint.example.com");An IpData client keeps a persistent HTTPS connection pool for as long as it lives. Create one client per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new client pays a fresh TLS handshake. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to HTTP/1.1 keep-alive otherwise.
IpData is Send + Sync: share one instance across tasks by reference or in an Arc, or clone() it; clones are cheap and share the same pool. There is no close method: dropping the last clone releases the pool and its connections. To bring your own reqwest::Client (a pool shared with the rest of your application, timeouts, a proxy or TLS settings), use IpData::with_client; the client is used as given, so set the User-Agent yourself if you want to keep the SDK's ipdata-rust/<version> header.
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let client = ipdata::IpData::with_client("your-api-key", "https://api.ipdata.co", http);The default client, used by IpData::new, IpData::eu and IpData::with_base_url, negotiates HTTP/2 through ALPN with automatic HTTP/1.1 fallback and is tuned for reuse: pooled connections idle for 60 s are closed, HTTP/2 connections are kept alive with a PING every 30 s, and TCP keep-alive probes run every 30 s. No total request timeout is set. A bulk lookup (POST /bulk) whose pooled connection turns out to be dead before any response byte arrives is sent again exactly once on a fresh connection; HTTP error statuses are never retried.
match client.lookup("8.8.8.8").await {
Ok(info) => println!("{}", info.ip),
Err(ipdata::Error::Api { status, message }) => {
eprintln!("API error ({}): {}", status, message);
}
Err(ipdata::Error::InvalidIp(ip)) => {
eprintln!("Bad IP: {}", ip);
}
Err(e) => eprintln!("Error: {}", e),
}The main response type is IpInfo which includes:
- Geolocation:
city,region,country_name,country_code,continent_name,latitude,longitude,postal - Flags:
is_eu(EU member country), plus two flags the API sends only when they are true, so the typed fields readfalsewhen the key is absent:is_satellite: the IP belongs to a satellite internet provideris_anycast: the IP is an anycast address announced from several locations at once
- ASN:
asn(struct withasn,name,domain,route,asn_type) - Company:
company(struct withname,domain,network,company_type) - Carrier:
carrier(struct withname,mcc,mnc) - Currency:
currency(struct withname,code,symbol,native,plural) - Time Zone:
time_zone(struct withname,abbreviation,offset,is_dst,current_time) - Organisation:
organisation(ISP / organization name) - Threat:
threat(struct withis_tor,is_vpn,is_proxy,is_anonymous,is_threat,is_bogon,blocklists,scores, and more) - Blocklist entries: each
Blocklistinthreat.blocklistshasname,siteandblocklist_type, plus three fields that areNonewhen a feed does not report them (ipdata's own vpn feed, for example):first_seen: first day the IP was seen on the list, as aYYYY-MM-DDstring (UTC)last_seen: most recent day the IP was seen on the list, as aYYYY-MM-DDstring (UTC)days_seen: number of days the IP has been seen on the list
- Languages:
languages(vec ofLanguagewithname,native,code)
All response structs ignore unknown keys and decode a missing key and an explicit null alike: Option fields read None, every other field reads its default ("", false, 0, 0.0 or an empty list), so a null anywhere in a response does not fail the lookup.
Releases are cut by CI; nothing is tagged by hand. Bump version in Cargo.toml in a pull request and merge it: once the tests on the merge commit pass, CI tags it vX.Y.Z, creates the GitHub release with generated notes and publishes the crate to crates.io. A merge whose version is already tagged publishes nothing. A release published by hand on GitHub still runs the same publish workflow.
MIT