Official, dependency-free Go 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
- ✅ Never panics, never returns a Go error — all errors come back as
Success: false - ✅ No external dependencies — only the Go standard library
- ✅ Go 1.21+
go get github.com/IPWhois/ipwhois-goThe same Client 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.
free := ipwhois.New() // Free plan — no API key
paid := ipwhois.New("YOUR_API_KEY") // Paid plan — with API keyEverything else (Lookup, options, error handling) is identical.
package main
import (
"fmt"
"github.com/IPWhois/ipwhois-go"
)
func main() {
client := ipwhois.New() // no API key
info := client.Lookup("8.8.8.8")
if !info.Success {
fmt.Println("Lookup failed:", info.Message)
return
}
fmt.Printf("%s %s\n", info.Country, info.Flag.Emoji)
// → United States 🇺🇸
fmt.Printf("%s, %s\n", info.City, info.Region)
// → Mountain View, California
}Get an API key at https://ipwhois.io and pass it to New:
package main
import (
"fmt"
"github.com/IPWhois/ipwhois-go"
)
func main() {
client := ipwhois.New("YOUR_API_KEY") // with API key
info := client.Lookup("8.8.8.8")
if !info.Success {
fmt.Println("Lookup failed:", info.Message)
return
}
fmt.Printf("%s %s\n", info.Country, info.Flag.Emoji)
// → United States 🇺🇸
fmt.Printf("%s, %s\n", info.City, info.Region)
// → Mountain View, California
}ℹ️ Pass an empty string to look up your own public IP:
client.Lookup("")— works on both plans.
Every option below can be passed per call, or set once on the client as a default.
| Option | Type | Plans needed | Description |
|---|---|---|---|
Language |
string |
Free + Paid | One of: en, ru, de, es, pt-BR, fr, zh-CN, ja |
Fields |
[]string |
Free + Paid | Restrict the response to specific fields (e.g. []string{"country", "city"}). nil = inherit, []string{} = clear default whitelist |
Rate |
*bool |
Basic and above | Include the rate block (Limit, Remaining). nil = inherit, &true/&false = override |
Security |
*bool |
Business and above | Include the security block (proxy/vpn/tor/hosting). nil = inherit, &true/&false = override |
Security and Rate are pointers, and Fields distinguishes nil from
an empty slice, so per-call options can always override or reset
client-wide defaults. Use the Bool helper to keep things tidy:
client := ipwhois.New("KEY").
SetSecurity(true). // default: on for every call
SetFields([]string{"country", "city"}) // default: only these two
client.Lookup("8.8.8.8") // security on, fields filtered
client.Lookup("1.1.1.1", ipwhois.LookupOptions{Security: ipwhois.Bool(false)}) // security explicitly off
client.Lookup("9.9.9.9", ipwhois.LookupOptions{Fields: []string{}}) // every field returnedEvery option can be passed two ways: per call (as the second argument
to Lookup / BulkLookup) 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 fluent setters — SetLanguage, SetFields,
SetSecurity, SetRate, SetTimeout, SetConnectTimeout, SetUserAgent,
SetSSL, SetHTTPClient — and can be chained:
// Free plan
client := ipwhois.New().
SetLanguage("en").
SetFields([]string{"success", "country", "city", "flag.emoji"}).
SetTimeout(8 * time.Second)// Paid plan
client := ipwhois.New("YOUR_API_KEY").
SetLanguage("en").
SetFields([]string{"success", "country", "city", "flag.emoji"}).
SetTimeout(8 * time.Second)Either client behaves the same way at call time — per-call options always win over the defaults:
client.Lookup("8.8.8.8") // uses lang=en, the field whitelist, and timeout=8s
client.Lookup("1.1.1.1", ipwhois.LookupOptions{Language: "de"}) // overrides lang for this single call only
⚠️ When you restrict fields withSetFields(or per-callLookupOptions.Fields), 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), call SetSSL(false):
// Free plan
client := ipwhois.New().SetSSL(false)// Paid plan
client := ipwhois.New("YOUR_API_KEY").SetSSL(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.
client := ipwhois.New("YOUR_API_KEY")
results := client.BulkLookup([]string{
"8.8.8.8",
"1.1.1.1",
"208.67.222.222",
"2c0f:fb50:4003::", // IPv6 is fine — mix freely
})
if !results.Success {
// Whole-batch failure — network down, bad API key, rate limit, …
fmt.Println("bulk failed:", results.Message)
return
}
for _, row := range results.Results {
if !row.Success {
// Per-IP errors (e.g. "Invalid IP address") are returned inline.
// The rest of the batch is still usable.
fmt.Printf("skip %s: %s\n", row.IP, row.Message)
continue
}
fmt.Printf("%s → %s\n", row.IP, row.Country)
}ℹ️ Bulk requires an API key. Calling
BulkLookupwithout one will fail at the API level.
The library never panics and never returns a Go error. Every failure —
invalid IP, bad API key, rate limit, network outage, bad options — comes
back inside the response with Success: false and a Message. Just check
info.Success after every call:
info := client.Lookup("8.8.8.8")
if !info.Success {
log.Printf("Lookup failed: %s", info.Message)
return
}
fmt.Println(info.Country)This means an outage of the ipwhois.io API (or of your server's DNS, connection, etc.) will never surface as a fatal error in your application — you decide how to react.
Every error response has Success: false, a human-readable Message,
and an ErrorType so you can branch on 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) |
info := client.Lookup("8.8.8.8")
if !info.Success {
if info.HTTPStatus == 429 {
time.Sleep(time.Duration(info.RetryAfter) * time.Second)
// …retry
}
if info.ErrorType == ipwhois.ErrorTypeNetwork {
// DNS failure, connection refused, timeout, …
}
log.Printf("Error: %s", info.Message)
return
}A successful response includes (depending on your plan and selected options):
The library exposes every documented field on the Response struct
(Country, City, Flag.Emoji, Connection.ISP, …). The full raw body
is always available in Response.Raw as []byte — useful as an escape
hatch for fields not yet typed by this struct.
For the full field reference, see the official documentation.
An error response looks like:
{
"success": false,
"message": "Rate limit exceeded",
"error_type": "api", // 'api' / 'network' / 'environment' / 'invalid_argument'
"http_status": 429, // present for HTTP 4xx / 5xx
"retry_after": 60 // additionally present on HTTP 429 — free plan only
}A Client is safe for concurrent use by multiple goroutines once it has
been fully configured (i.e. after the last Set*** call). Configure it
during application startup, then share the same instance across handlers.
For advanced cases — proxies, custom transports, mocking — you can supply
your own *http.Client:
client := ipwhois.New("YOUR_API_KEY").
SetHTTPClient(&http.Client{
Timeout: 15 * time.Second,
Transport: myCustomTransport,
})When set, the library defers entirely to your client for deadline
management: SetTimeout and SetConnectTimeout are ignored, and the
library does not add a context timeout of its own on top. If your
custom client has no Timeout, the request has no client-side deadline.
- Go 1.21 or newer
- No external dependencies
Issues and pull requests are welcome on GitHub.
MIT © ipwhois.io
{ "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 } }