Skip to content

IPWhois/ipwhois-csharp

Repository files navigation

IPWhois — official .NET client

NuGet .NET License

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 / BulkLookupAsync return Success = false instead of throwing on API, HTTP, network, timeout, and validation failures
  • ✅ Async/await, CancellationToken support
  • ✅ Plays nicely with IHttpClientFactory
  • ✅ Multi-targets netstandard2.0, net6.0, net8.0

Installation

dotnet add package IPWhois

or, from the Package Manager Console:

Install-Package IPWhois

Free vs Paid plan

The 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 key

Everything else (LookupAsync, options, error handling) is identical.

Quick start — Free plan (no API key)

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, California

Quick start — Paid plan (with API key)

Get 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.

Lookup options

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)

Setting defaults once

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 with SetFields() (or the per-call Fields option), the API only returns the fields you ask for. Always include "success" in the list if you rely on info.Success for error checking — otherwise the field will be missing on responses.

ℹ️ SetSecurity(true) requires Business+ and SetRate(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), 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.

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.

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 BulkLookupAsync without one will fail at the API level.

Error handling

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.Timeout timeouts
  • 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:

  1. CancellationToken cancellation. Cancelling the supplied token — whether directly (cts.Cancel()), via CancelAfter, or through a CancellationTokenSource constructed with a timeout — propagates as OperationCanceledException, which is the standard .NET convention for cooperative cancellation. If you'd rather have a deadline that doesn't throw, set HttpClient.Timeout (or SetTimeout()) instead: that one is translated into a Success = false error response.
  2. Programmer errors. The constructor and the fluent Set* setters throw ArgumentException / ArgumentNullException for clearly invalid inputs (e.g. SetFields(null), SetUserAgent("")). These represent misuse of the API, not runtime conditions, and aren't caught.

Error response fields

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;
}

Response shape

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.

Using with IHttpClientFactory

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.

Cancellation

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

Requirements

  • .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)

Contributing

Issues and pull requests are welcome on GitHub.

License

MIT © ipwhois.io

Packages

 
 
 

Contributors

Languages