Prerequisites
- Ruby 3.0 or higher
- An ip-api.io API key — get one free
- No other dependencies — the gem is built on
net/http
Install the gem
Add the official ip-api-io gem to your project. It is
pure Ruby — nothing to pull in beyond the standard library.
gem install ip-api-io
# ...or in a Gemfile:
# gem "ip-api-io" Construct the client
Require the gem 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.
require "ipapi"
client = Ipapi::Client.new(api_key: ENV.fetch("IP_API_IO_KEY"))
The client is thread-safe — build one and reuse it across requests, Sidekiq jobs, or
threads. In Rails, a memoized client in an initializer reading from
Rails.application.credentials or ENV is a good pattern.
Look up an IP address
Call lookup_ip(ip) for any IPv4 or IPv6 address, or lookup
to resolve the caller's own IP. Responses are typed objects; absent fields return
nil, so guard with &..
info = client.lookup_ip("8.8.8.8")
puts info.ip # "8.8.8.8"
puts info.isp # "Google LLC"
puts info.location.country # "United States"
puts info.suspicious_factors.datacenter? # true
# Resolve the caller's own IP:
me = client.lookup
puts me.ip country and city return nil for private ranges
or unrecognized addresses — reach for the safe-navigation operator, e.g.
info.location&.city. Boolean signals such as
info.suspicious_factors.vpn? are predicate methods.
Detect VPN, proxy, Tor & batch lookups
The suspicious_factors object carries seven boolean predicates 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.lookup_ip("185.220.101.45") # example Tor exit node
f = info.suspicious_factors
if f.vpn? || f.proxy? || f.tor_node?
puts "Anonymized traffic — consider a CAPTCHA or block"
end
puts "Cloud/datacenter IP — likely automated" if f.datacenter?
puts "Active threat signal — block this request" if f.threat?
risky = f.vpn? || f.proxy? || f.tor_node? || f.threat?
Need to check many IPs at once? lookup_batch takes up to 100 addresses in
a single request (it raises ArgumentError if the array is empty or longer
than 100):
batch = client.lookup_batch(["8.8.8.8", "1.1.1.1", "9.9.9.9"])
puts batch.total_processed
puts batch.successful_lookups
batch.results.each do |ip, info|
puts "#{ip} #{info.suspicious_factors.vpn?}"
end client.risk_score_ip(ip) for a combined
fraud risk score from
0 to 100.
Production-ready error handling
Errors are raised as exceptions under Ipapi::Error. Rescue the specific
subclass you care about — the client never retries on its own, so on a
rate limit RateLimitError#reset tells you exactly when to try again.
begin
info = client.lookup_ip("8.8.8.8")
puts info.ip
rescue Ipapi::RateLimitError => e
puts "Rate limited. Remaining: #{e.remaining}. Resets at #{e.reset}"
rescue Ipapi::AuthenticationError
puts "Invalid or missing API key — get one free at https://ip-api.io"
rescue Ipapi::InvalidRequestError => e
puts "Bad request: #{e.message}"
rescue Ipapi::ServerError
puts "ip-api.io is having issues — try again later"
rescue Ipapi::Error => e
puts "Transport / decode error: #{e.message}"
end RateLimitError#reset (a Unix timestamp) to schedule the next attempt.
Transport failures (DNS, connect, timeout, parse) are raised as
Ipapi::Error and caught by the final rescue.
In a Rails controller, run the lookup server-side and pass the real client IP from the
X-Forwarded-For header (request.remote_ip already resolves it):
class CheckoutController < ApplicationController
def create
info = IPAPI_CLIENT.lookup_ip(request.remote_ip)
render json: {
ip: info.ip,
country: info.location&.country,
is_vpn: info.suspicious_factors.vpn?,
is_threat: info.suspicious_factors.threat?
}
rescue Ipapi::Error
# Treat ip-api.io as a non-critical dependency
render json: { ip: request.remote_ip, country: nil }
end
end