ipwhois
Official, async Rust client for the ipwhois.io IP Geolocation API.
- ✅ Single and bulk IP lookups (IPv4 and IPv6)
- ✅ Works with both the Free and Paid plans
- ✅ HTTPS by default
- ✅ Localisation, field selection, threat detection, rate info
- ✅ Errors as values — every fallible call returns
Result<_, Error> - ✅ Strongly-typed responses, with a
extrabucket for forward compatibility - ✅
tokio+reqwest, rustls TLS, no native OpenSSL needed
Installation
[]
= "1.2"
= { = "1", = ["macros", "rt-multi-thread"] }
The crate is published as ipwhois-rust but imported as ipwhois, so
your code reads use ipwhois::IpWhois; — the package name carries the
language suffix for crates.io disambiguation; the library identifier
stays short and on-brand.
Free vs Paid plan
The same IpWhois type is used for both plans. The only difference is
whether you pass an API key:
- Free plan — construct the client with
IpWhois::new(). No API key, no signup required. Suitable for low-traffic and non-commercial use. - Paid plan — construct with
IpWhois::with_key("…")using your key from https://ipwhois.io. Higher limits, plus access to bulk lookups and threat-detection data.
use IpWhois;
let free = new; // Free plan — no API key
let paid = with_key; // Paid plan — with API key
// Or, if you'd rather catch an empty / whitespace-only key up-front
// instead of letting the API reject it:
let paid = try_with_key?;
# Ok::
Everything else (lookup, options, error handling) is identical.
Quick start — Free plan (no API key)
use IpWhois;
async
Quick start — Paid plan (with API key)
Get an API key at https://ipwhois.io and pass it to with_key:
use IpWhois;
async
ℹ️ To look up your own public IP, call
ipwhois.lookup_self().await?— works on both plans.
Lookup options
Every option below can be passed per call (via lookup_with /
bulk_lookup_with) or set once on the client as a default.
| Option | Type | Plans needed | Description |
|---|---|---|---|
lang |
&str |
Free + Paid | One of: en, ru, de, es, pt-BR, fr, zh-CN, ja |
fields |
IntoIterator |
Free + Paid | Restrict the response to specific fields (e.g. ["country", "city"]) |
rate |
bool |
Basic and above | Include the rate block (limit, remaining) |
security |
bool |
Business and above | Include the security block (proxy/vpn/tor/hosting) |
Setting defaults once
Every option can be passed two ways: per call (as the second argument
to lookup_with / bulk_lookup_with) or once as a default on the
client. Per-call options always override the defaults, so it's safe to
set sensible defaults and only override what differs for a specific call.
Defaults are set with the consuming builder methods — with_language,
with_fields, with_security, with_rate, with_timeout,
with_connect_timeout, with_user_agent — and can be chained:
use IpWhois;
// Free plan
let ipwhois = new
.with_language
.with_fields
.with_timeout;
use IpWhois;
// Paid plan
let ipwhois = with_key
.with_language
.with_fields
.with_timeout;
Either client behaves the same way at call time — per-call options always win over the defaults:
# use ;
# async
⚠️ When you restrict fields with
with_fields(or the per-callOptions::with_fields), the API only returns the fields you ask for. Always include"success"in the list if you rely oninfo.successfor status checking — otherwise the field will be missing on responses.
ℹ️
with_security(true)requires Business+ andwith_rate(true)requires Basic+. See the table above for what's available where.
HTTPS Encryption
By default, all requests are sent over HTTPS. If you need to disable it
(for example, in environments without an up-to-date CA bundle), call
with_ssl(false):
use IpWhois;
// Free plan
let ipwhois = new.with_ssl;
use IpWhois;
// Paid plan
let ipwhois = with_key.with_ssl;
ℹ️ HTTPS is strongly recommended for production traffic — your API key is sent in the query string and would otherwise travel in clear text.
Bulk lookup (Paid plan only)
The bulk endpoint sends up to 100 IPs in a single GET request. Each address counts as one credit. Available on the Business and Unlimited plans.
use IpWhois;
# async
ℹ️ Bulk requires an API key. Calling
bulk_lookupwithout one will fail at the API level.
Error handling
The public API returns Result for every fallible operation and does
not intentionally panic. Every failure — invalid IP, bad API key,
rate limit, network outage, bad options — comes back as Err(Error).
Match on the error or check error_type() for the category:
use ;
# async
This means an outage of the ipwhois.io API (or of your server's DNS,
connection, etc.) surfaces as a regular Result::Err you decide how to
react to, rather than an unwind through your code.
Error variants and metadata
Every error carries a human-readable message. The category is exposed
both via the enum variant and via Error::error_type(), which returns
a stable string ("api", "network", or "invalid_argument"):
| Variant | error_type() |
When it's returned |
|---|---|---|
Error::Api |
"api" |
API-level failures: HTTP 4xx / 5xx, malformed JSON, HTTP 2xx + success: false |
Error::Network |
"network" |
DNS, connection, TLS, timeout — the request never reached the API meaningfully |
Error::InvalidArgument |
"invalid_argument" |
Caller-side: unsupported language, empty bulk list, more than 100 IPs |
Error::Api additionally carries:
http_status: Option<u16>— present on HTTP 4xx / 5xx responses.retry_after: Option<u64>— present on HTTP 429 responses on the free plan only (the paid endpoint does not send aRetry-Afterheader).
Convenience accessors are also available: e.http_status(),
e.retry_after(), e.message().
Response shape
A successful response includes (depending on your plan and selected
options) — every field is Option<…> because the API only returns the
fields you ask for when fields is set:
{
"ip": "8.8.4.4",
"success": true,
"type": "IPv4",
"continent": "North America",
"continent_code": "NA",
"country": "United States",
"country_code": "US",
"region": "California",
"region_code": "CA",
"city": "Mountain View",
"latitude": 37.3860517,
"longitude": -122.0838511,
"is_eu": false,
"postal": "94039",
"calling_code": "1",
"capital": "Washington D.C.",
"borders": "CA,MX",
"flag": {
"img": "https://cdn.ipwhois.io/flags/us.svg",
"emoji": "🇺🇸",
"emoji_unicode": "U+1F1FA U+1F1F8"
},
"connection": {
"asn": 15169,
"org": "Google LLC",
"isp": "Google LLC",
"domain": "google.com"
},
"timezone": {
"id": "America/Los_Angeles",
"abbr": "PDT",
"is_dst": true,
"offset": -25200,
"utc": "-07:00",
"current_time": "2026-05-08T14:31:48-07:00"
},
"currency": {
"name": "US Dollar",
"code": "USD",
"symbol": "$",
"plural": "US dollars",
"exchange_rate": 1
},
"security": {
"anonymous": false,
"proxy": false,
"vpn": false,
"tor": false,
"hosting": false
},
"rate": {
"limit": 250000,
"remaining": 50155
}
}
For the full field reference, see the official documentation.
Any field not yet modelled by this crate lands in
LookupResponse::extra: HashMap<String, serde_json::Value> — so a
server-side schema bump won't break your code.
Requirements
- Rust 1.75 or newer
- A
tokioruntime (or any other compatible async runtime supported byreqwest)
Contributing
Issues and pull requests are welcome on GitHub.
License
MIT © ipwhois.io