Prerequisites
- Go 1.21 or higher
- An ip-api.io API key — get one free
- No other dependencies — the client is standard library only
Install the module
Add the official ipapi-go module to your project. It has
zero dependencies — nothing to pull in beyond the standard library.
go get github.com/ip-api-io/ipapi-go Construct the client
Import the package and build one client with your API key. Read the key from an
environment variable — never hardcode it. The API rejects keyless requests with
401.
package main
import (
"context"
"os"
ipapi "github.com/ip-api-io/ipapi-go"
)
func main() {
client := ipapi.NewClient(ipapi.WithAPIKey(os.Getenv("IP_API_IO_KEY")))
ctx := context.Background()
_ = client
_ = ctx
}
Every method takes a context.Context first, so you can attach per-request
timeouts, deadlines, and cancellation. Reuse a single client across goroutines — it is
safe for concurrent use.
Look up an IP address
Call LookupIP(ctx, ip) for any IPv4 or IPv6 address, or
Lookup(ctx) to resolve the caller's own IP. Responses are typed structs;
nullable fields are pointers — guard with a nil check before dereferencing.
info, err := client.LookupIP(ctx, "8.8.8.8")
if err != nil {
log.Fatal(err)
}
fmt.Println(info.IP) // "8.8.8.8"
if info.ISP != nil {
fmt.Println(*info.ISP) // "Google LLC"
}
if info.Location.Country != nil {
fmt.Println(*info.Location.Country) // "United States"
}
fmt.Println(info.SuspiciousFactors.IsDatacenter) // true
// Resolve the caller's own IP:
me, _ := client.Lookup(ctx)
fmt.Println(me.IP) Country and City are *string because they're
absent for private ranges or unrecognized addresses. Boolean flags such as
info.SuspiciousFactors.IsVPN are plain bool values.
Detect VPN, proxy, Tor & batch lookups
The SuspiciousFactors struct carries seven boolean flags on every
lookup — this is what sets ip-api.io apart from plain geolocation APIs. You get
security intelligence in the same call, at no extra cost.
info, _ := client.LookupIP(ctx, "185.220.101.45") // example Tor exit node
f := info.SuspiciousFactors
if f.IsVPN || f.IsProxy || f.IsTorNode {
fmt.Println("Anonymized traffic — consider a CAPTCHA or block")
}
if f.IsDatacenter {
fmt.Println("Cloud/datacenter IP — likely automated")
}
if f.IsThreat {
fmt.Println("Active threat signal — block this request")
}
isRisky := f.IsVPN || f.IsProxy || f.IsTorNode || f.IsThreat
Need to check many IPs at once? LookupBatch takes up to 100 addresses in a
single request (it returns an error if the slice is empty or longer than 100):
batch, err := client.LookupBatch(ctx, []string{"8.8.8.8", "1.1.1.1", "9.9.9.9"})
if err != nil {
log.Fatal(err)
}
fmt.Println(batch.TotalProcessed, batch.SuccessfulLookups)
for ip, info := range batch.Results {
fmt.Println(ip, info.SuspiciousFactors.IsVPN)
} client.RiskScoreIP(ctx, ip) for a combined
fraud risk score from
0 to 100.
Production-ready error handling
Every method returns (*T, error). Use errors.As to inspect
the typed error — the client never retries on its own, so on a rate
limit RateLimitError.Reset tells you exactly when to try again.
import "errors"
info, err := client.LookupIP(ctx, "8.8.8.8")
if err != nil {
var rateErr *ipapi.RateLimitError
var authErr *ipapi.AuthenticationError
var invErr *ipapi.InvalidRequestError
var srvErr *ipapi.ServerError
switch {
case errors.As(err, &rateErr):
fmt.Printf("Rate limited. Remaining: %d. Resets at %d\n", rateErr.Remaining, rateErr.Reset)
case errors.As(err, &authErr):
fmt.Println("Invalid or missing API key — get one free at https://ip-api.io")
case errors.As(err, &invErr):
fmt.Println("Bad request:", invErr.Message)
case errors.As(err, &srvErr):
fmt.Println("ip-api.io is having issues — try again later")
default:
fmt.Println("transport / decode error:", err)
}
return
}
_ = info RateLimitError.Reset (a Unix timestamp) to schedule the next attempt.
Transport failures (DNS, connect, timeout, decode) are wrapped standard errors and fall
through to the default branch — they are not an *APIError.
In an HTTP server, run the lookup server-side and pass the real client IP from the
X-Forwarded-For header:
func checkHandler(client *ipapi.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
clientIP := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0])
if clientIP == "" {
clientIP, _, _ = net.SplitHostPort(r.RemoteAddr)
}
info, err := client.LookupIP(r.Context(), clientIP)
if err != nil {
// Treat ip-api.io as a non-critical dependency
json.NewEncoder(w).Encode(map[string]any{"ip": clientIP, "country": nil})
return
}
json.NewEncoder(w).Encode(map[string]any{
"ip": info.IP,
"country": info.Location.Country,
"is_vpn": info.SuspiciousFactors.IsVPN,
"is_threat": info.SuspiciousFactors.IsThreat,
})
}
}