The official Ruby client library for the InternetData API.
InternetData publishes IP and ASN databases: geolocation, anonymity, ownership and network data, as gzipped CSV and as MMDB. This library lists the databases your organization is licensed for, tells you what is inside one before you fetch it, downloads it, and verifies the bytes you got.
Getting Started
gem install internetdataOr add it to your Gemfile:
gem 'internetdata'Requires Ruby 3.3 or newer.
Usage
Every database published today needs an API key carrying the db.download scope. The Open databases are licensed to every organization from the start and are free under CC BY-SA 4.0, and list marks them open. The rest are licensed by contract, one family at a time: if your organization doesn't have a license yet, request an evaluation. api_key: is nevertheless optional - a client built without one sends no Authorization header at all, ready for a database served without a license.
require 'internetdata'
client = InternetData::Client.new(api_key: ENV['INTERNETDATA_API_KEY'])
client.database.list.each do |database|
puts [database.base, database.standing, database.versions.map(&:id).join(', ')].join(' ')
endlist returns one entry per database FAMILY, because a license is held against the family while a download names a specific version. standing is licensed if the family is yours today, expired if the term has ended, and unlicensed if it is published but has never been bought. versions carries the ids you pass everywhere else, oldest first, and the formats each one is actually built in.
What is inside a database
meta = client.database.metadata('bogon_ip_v1')
meta.updated # => '2026-09-04', the day this build was generated
meta.entries # => rows in the build
meta.size['csvgz'] # => bytes you are about to move
meta.schema['csvgz'] # => [#<DatabaseMetadataColumn name="ip" type="string" ...>, ...]
meta.sample['mmdb'] # => a few real rowsPoll updated and entries to decide whether today's build is worth fetching; both are free of the transfer. size is also the honest way to budget a download before starting one.
Downloading
download streams a file to a path and returns the number of bytes written. Nothing larger than a chunk is ever held in memory, whatever the database weighs:
written = client.database.download('bogon_ip_v1', 'mmdb', './bogon_ip_v1.mmdb')The bytes go to a neighboring .part file and the name only appears once the transfer completes, so an interruption cannot leave a truncated file that reads as a whole database, and a refresh that fails does not destroy the copy already there.
Or take the link and run the transfer yourself. The API answers a redirect to time-limited object storage, and that URL authorizes itself, so it can be handed to something holding no API key:
url = client.database.download_url('bogon_ip_v1', 'mmdb')Or, for a small database, take the bytes directly:
bytes = client.database.download_bytes('bogon_asn_v1', 'csvgz')download_bytes holds the whole file in memory and the catalog spans seven orders of magnitude, from a few hundred bytes to over 5 GiB, so use download for anything you have not checked with metadata first.
Verifying what you got
sums = client.database.checksums('bogon_ip_v1', 'mmdb')
sums.sha256 # also md5, sha1, sha512Download history
Your organization's recent attempts, newest first, refusals included, because a denial is what answers "it stopped working" and its absence answers nothing:
client.database.downloads(limit: 20).each do |attempt|
puts [attempt.created, attempt.dataset_id, attempt.format, attempt.outcome].join(' ')
endTimeouts
client = InternetData::Client.new(api_key: ENV['INTERNETDATA_API_KEY'], timeout: 10)
catalog = client.database.list(timeout: 5)timeout is in seconds and bounds each attempt, body included, so a call that is retried can take longer in total. It defaults to 30 seconds, and 0 means no bound. The client's value is the default: from 2.1.0, list, metadata, checksums, downloads and download_url each take timeout: for that call alone. A negative value, anything that isn't a number, and anything past 2147483.647 seconds (the longest curl holds) raise ArgumentError where you pass them, before any request.
download and download_bytes take no timeout: and raise ArgumentError if handed one, rather than accepting it and quietly doing nothing: a transfer runs to gigabytes and minutes, so any bound that suits a JSON call would abandon a healthy download. They bound only their connection with the client's value. download_url does take one, because minting the link is an ordinary API request - it bounds that request, not whatever you do with the link afterwards.
Errors
Failures raise an InternetData::Error carrying a kind and a retryable? flag:
begin
client.database.download('vpn_ip_v1', 'mmdb', './vpn_ip_v1.mmdb')
rescue InternetData::Error => e
warn "#{e.kind} #{e.status} #{e.retryable?}: #{e.rc}"
endkind is one of :bad_request, :unauthorized, :forbidden, :rate_limited, :quota_exceeded, :server_error or :network. rc is the API's own result code, such as NOT_LICENSED or LICENSE_EXPIRED, which is usually the specific thing you want to read.
:rate_limited is a 429 that carries Retry-After, and the library retries it for you, waiting as long as it says, or its own backoff from 250 ms when that's past about 24.8 days. :quota_exceeded is a 429 without one, which it never retries. InternetData's database API has no quota, so you won't see :quota_exceeded from it.
Sign in with OAuth (device flow)
A program running on a person's own machine can let them sign in with their browser and pick one of their API keys, instead of asking them to paste one.
client = InternetData::Client.new
device = client.oauth.device_authorization('your-client-id', scope: 'account.read apikeys.read apikeys.reveal')
puts "Open #{device.verification_uri} and enter #{device.user_code}"
token = client.oauth.poll_device_token('your-client-id', device)
raise 'no API key was picked' if token.apikey.nil?
keyed = InternetData::Client.new(api_key: token.apikey)poll_device_token raises InternetData::OauthAccessDeniedError when the person refuses and InternetData::OauthExpiredTokenError when the code expires first. Client IDs are issued on request from support@internetdata.io, and client.oauth.revoke('your-client-id', token.refresh_token) signs the machine out.
Sign in with OAuth (authorization code)
An app that can take a browser redirect signs the person in there instead, with a PKCE pair made for that one sign-in:
client = InternetData::Client.new
redirect_uri = 'http://127.0.0.1:8765/callback'
pkce = client.oauth.create_pkce
url = client.oauth.authorization_url('your-client-id', redirect_uri, pkce.challenge,
scope: 'apikeys.use', state: 'your-state')
# Open url in the browser. Its redirect to redirect_uri carries code and state.
token = client.oauth.exchange_authorization_code('your-client-id', code, pkce.verifier, redirect_uri)Check that state came back as you sent it before you exchange code, which works once. The client ID can also be the https URL of a client metadata document your app serves, and such an app is never handed a key, so token.apikey stays nil.
Register a client
An app with no client ID, and no client metadata document to use as one, registers itself, then signs in with the client_id it gets back as the client ID for authorization_url and exchange_authorization_code:
client = InternetData::Client.new
registration = client.oauth.register(['http://127.0.0.1:8765/callback'], client_name: 'Your App')Each redirect URI must be https on a public host name, or http on a loopback address, which then matches any port. Nothing is kept until a person approves the client, so an app with no client ID saved can simply register again.
Other Libraries
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, and Ruby. See our GitHub at https://github.com/internetdata for more.
About InternetData
Geolocation, anonymity, ownership and network databases for IP addresses and AS numbers, licensed as files you download and query yourself.
License
This project is licensed under the MIT License.