Official, lightweight .NET client for the ipwhois.io IP Geolocation API. Zero dependencies on modern .NET (uses System.Text.Json on netstandard2.0).
- ✅ 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
- ✅ Robust error handling —
LookupAsync/BulkLookupAsyncreturnSuccess = falseinstead of throwing on API, HTTP, network, timeout, and validation failures - ✅ Async/await,
CancellationTokensupport - ✅ Plays nicely with
IHttpClientFactory - ✅ Multi-targets
netstandard2.0,net6.0,net8.0
dotnet add package IPWhoisor, from the Package Manager Console:
Install-Package IPWhoisThe same IPWhoisClient class is used for both plans. The only difference
is whether you pass an API key:
- Free plan — create the client without arguments. No API key, no signup required. Suitable for low-traffic and non-commercial use.
- Paid plan — create the client with your API key from https://ipwhois.io. Higher limits, plus access to bulk lookups and threat-detection data.
using var free = new IPWhoisClient(); // Free plan — no API key
using var paid = new IPWhoisClient("YOUR_API_KEY"); // Paid plan — with API keyEverything else (LookupAsync, options, error handling) is identical.
using IPWhois;
using var client = new IPWhoisClient(); // no API key
var info = await client.LookupAsync("8.8.8.8");
Console.WriteLine($"{info.Country} {info.Flag?.Emoji}");
// → United States 🇺🇸
Console.WriteLine($"{info.City}, {info.Region}");
// → Mountain View, CaliforniaGet an API key at https://ipwhois.io and pass it to the constructor:
using IPWhois;
using var client = new IPWhoisClient("YOUR_API_KEY"); // with API key
var info = await client.LookupAsync("8.8.8.8");
Console.WriteLine($"{info.Country} {info.Flag?.Emoji}");
// → United States 🇺🇸
Console.WriteLine($"{info.City}, {info.Region}");
// → Mountain View, Californiaℹ️ Pass nothing (or
null) to look up your own public IP:await client.LookupAsync();— works on both plans.
Every option below can be passed per call via LookupOptions, or set once
on the client as a default.
| Option | C# Type | Plans needed | Description |
|---|---|---|---|
Language |
string |
Free + Paid | One of: "en", "ru", "de", "es", "pt-BR", "fr", "zh-CN", "ja" |
Fields |
IEnumerable<string> |
Free + Paid | Restrict the response to specific fields (e.g. new[] { "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) |
Every option can be passed two ways: per call (as the second argument to
LookupAsync / BulkLookupAsync) or once as a default on the client.
Per-call options always override the defaults.
Defaults are set with fluent setters — SetLanguage, SetFields,
SetSecurity, SetRate, SetTimeout, SetUserAgent, SetSsl — and can
be chained:
using IPWhois;
// Free plan
using var client = new IPWhoisClient()
.SetLanguage("en")
.SetFields(new[] { "success", "country", "city", "flag.emoji" })
.SetTimeout(8);using IPWhois;
// Paid plan
using var client = new IPWhoisClient("YOUR_API_KEY")
.SetLanguage("en")
.SetFields(new[] { "success", "country", "city", "flag.emoji" })
.SetTimeout(8);…or via the IPWhoisClientOptions object at construction time:
using var client = new IPWhoisClient(
apiKey: "YOUR_API_KEY",
options: new IPWhoisClientOptions
{
Language = "en",
Fields = new[] { "success", "country", "city", "flag.emoji" },
Timeout = TimeSpan.FromSeconds(8),
});Per-call options always win over the defaults:
await client.LookupAsync("8.8.8.8"); // uses lang=en, field whitelist, timeout=8
await client.LookupAsync("1.1.1.1", new LookupOptions { Language = "de" }); // overrides lang for this single call only
⚠️ When you restrict fields withSetFields()(or the per-callFieldsoption), the API only returns the fields you ask for. Always include"success"in the list if you rely oninfo.Successfor error checking — otherwise the field will be missing on responses.
ℹ️
SetSecurity(true)requires Business+ andSetRate(true)requires Basic+. See the table above for what's available where.
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), set Ssl
on the constructor options or call SetSsl(false):
using IPWhois;
// Free plan
using var client = new IPWhoisClient(apiKey: null,
options: new IPWhoisClientOptions { Ssl = false });using IPWhois;
// Paid plan
using var client = new IPWhoisClient("YOUR_API_KEY",
new IPWhoisClientOptions { Ssl = false });ℹ️ HTTPS is strongly recommended for production traffic — your API key is sent in the query string and would otherwise travel in clear text.
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.
using IPWhois;
using var client = new IPWhoisClient("YOUR_API_KEY");
var bulk = await client.BulkLookupAsync(new[]
{
"8.8.8.8",
"1.1.1.1",
"208.67.222.222",
"2c0f:fb50:4003::", // IPv6 is fine — mix freely
});
if (!bulk.Success)
{
// Whole-batch failure (network down, bad API key, rate limit, …).
Console.Error.WriteLine($"Bulk failed: {bulk.Message} (HTTP {bulk.HttpStatus})");
return;
}
foreach (var row in bulk.Results)
{
if (!row.Success)
{
// Per-IP errors (e.g. "Invalid IP address") are returned inline —
// the rest of the batch is still usable.
Console.WriteLine($"skip {row.Ip}: {row.Message}");
continue;
}
Console.WriteLine($"{row.Ip} → {row.Country}");
}ℹ️ Bulk requires an API key. Calling
BulkLookupAsyncwithout one will fail at the API level.
LookupAsync and BulkLookupAsync do not throw for any of:
- API errors (invalid IP, bad key, rate limit, "Reserved range", …)
- HTTP errors (4xx / 5xx)
- Network failures (DNS, connection refused, connection reset, …)
HttpClient.Timeouttimeouts- Malformed JSON
- Validation errors (e.g. unsupported
Language, too many IPs in a bulk)
Every one of those comes back on the response object with Success = false,
a populated Message, and an ErrorType you can branch on. Just check
info.Success after every call:
var info = await client.LookupAsync("8.8.8.8");
if (!info.Success)
{
Console.Error.WriteLine($"Lookup failed: {info.Message}");
return;
}
Console.WriteLine(info.Country);This means an outage of the ipwhois.io API (or of your server's DNS, connection, etc.) will never surface as an unhandled exception in your application — you decide how to react.
There are two cases where the methods do throw, both of them deliberate:
CancellationTokencancellation. Cancelling the supplied token — whether directly (cts.Cancel()), viaCancelAfter, or through aCancellationTokenSourceconstructed with a timeout — propagates asOperationCanceledException, which is the standard .NET convention for cooperative cancellation. If you'd rather have a deadline that doesn't throw, setHttpClient.Timeout(orSetTimeout()) instead: that one is translated into aSuccess = falseerror response.- Programmer errors. The constructor and the fluent
Set*setters throwArgumentException/ArgumentNullExceptionfor clearly invalid inputs (e.g.SetFields(null),SetUserAgent("")). These represent misuse of the API, not runtime conditions, and aren't caught.
Every error response sets Success = false, populates Message, and tags
ErrorType with the category of the failure. Some errors include extra
fields you can branch on:
| Field | When it's present |
|---|---|
Success |
Always — false for error responses (true for successful responses) |
Message |
Always — human-readable description of what went wrong |
ErrorType |
Always — one of "api", "network", "environment", or "invalid_argument" |
HttpStatus |
On HTTP 4xx / 5xx responses |
RetryAfter |
On HTTP 429 — free plan only (the paid endpoint does not send a Retry-After header) |
var info = await client.LookupAsync("8.8.8.8");
if (!info.Success)
{
if (info.HttpStatus == 429)
{
await Task.Delay(TimeSpan.FromSeconds(info.RetryAfter ?? 60));
// …retry
}
if (info.ErrorType == "network")
{
// DNS failure, connection refused, timeout, …
}
Console.Error.WriteLine($"Error: {info.Message}");
return;
}A successful response includes (depending on your plan and selected options):
info.Ip // "8.8.4.4"
info.Success // true
info.Type // "IPv4"
info.Country // "United States"
info.CountryCode // "US"
info.Region // "California"
info.City // "Mountain View"
info.Latitude // 37.3860517
info.Longitude // -122.0838511
info.Flag?.Emoji // "🇺🇸"
info.Connection?.Isp // "Google LLC"
info.Timezone?.Id // "America/Los_Angeles"
info.Currency?.Code // "USD"
info.Security?.Vpn // false (Business+)
info.Rate?.Remaining // 50155 (Basic+)If you need a field that isn't modelled by IPWhoisResponse, or if you've
restricted the response with a custom Fields whitelist, info.Raw gives
you the underlying JsonElement:
var asnDescription = info.Raw
.GetProperty("connection")
.GetProperty("org")
.GetString();For the full field reference, see the official API documentation.
For ASP.NET Core (or any host that uses IHttpClientFactory), inject a
shared HttpClient so DNS rotation works properly:
services.AddHttpClient("ipwhois");
services.AddSingleton<IPWhoisClient>(sp =>
{
var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("ipwhois");
return new IPWhoisClient(
apiKey: builder.Configuration["IPWhois:ApiKey"],
options: null,
httpClient: http);
});When you supply your own HttpClient, the library will not dispose of
it — that's the factory's job.
All async methods accept an optional CancellationToken:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var info = await client.LookupAsync("8.8.8.8", cancellationToken: cts.Token);Cancellation through the supplied CancellationToken — including
CancelAfter and constructors that take a TimeSpan — propagates as
OperationCanceledException. That's the standard .NET convention and
intentional: callers usually want a thrown exception to unwind the
call stack on cancellation.
If you'd rather have a deadline that comes back as Success = false
instead of throwing, configure HttpClient.Timeout (via the constructor
or SetTimeout()):
using var client = new IPWhoisClient("YOUR_API_KEY").SetTimeout(2);
var info = await client.LookupAsync("8.8.8.8");
// info.Success == false, info.ErrorType == "network" on timeout- .NET 6 / .NET 8 / .NET Standard 2.0 (covers .NET Framework 4.6.1+ and every modern .NET runtime)
System.Text.Json(built into .NET 6+; pulled in automatically on netstandard2.0)
Issues and pull requests are welcome on GitHub.
MIT © ipwhois.io