README
View Source<img src="https://cdn.ipregistry.co/icons/favicon-96x96.png" alt="Ipregistry" width="64"/>
Ipregistry Erlang Client Library
This is the official Erlang client library for the Ipregistry IP geolocation and threat data API, allowing you to look up your own IP address or specified ones. Responses return multiple data points including carrier, company, currency, location, time zone, threat information, and more. The library can also parse raw User-Agent strings.
The library has zero external dependencies — it is built entirely on Erlang/OTP (httpc, ssl, and the json
module), with explicit TLS peer verification. It works from Erlang and from Elixir.
Getting Started
You'll need an Ipregistry API key, which you can get along with 100,000 free lookups by signing up for a free account at https://ipregistry.co.
Installation
Requires Erlang/OTP 27 or later.
Add the Hex package to your rebar.config:
{deps, [ipregistry]}.or to your Elixir project's mix.exs:
defp deps do
[{:ipregistry, "~> 1.0"}]
endThen add ipregistry to your application's applications list (or release) so its OTP dependencies (inets, ssl)
are started with your system. In an interactive shell, application:ensure_all_started(ipregistry) does the same —
and ipregistry:new/1,2 calls it for you.
Quick start
Single IP lookup
Client = ipregistry:new(<<"YOUR_API_KEY">>),
%% Look up data for a given IPv4 or IPv6 address. Binaries, strings, and
%% inet:ip_address() tuples are all accepted.
{ok, Info} = ipregistry:lookup(Client, <<"54.85.132.205">>),
%% Responses are maps with binary keys, exactly as returned by the API.
%% ipregistry:get/2,3 walks nested fields conveniently:
CountryName = ipregistry:get(Info, [location, country, name]),
IsVpn = ipregistry:get(Info, [security, is_vpn], false).A client is a plain immutable map: build it once (for example in your application's init), share it freely between
processes, and never worry about synchronization.
Origin IP lookup
To look up the IP address the request is sent from — no argument needed — use origin_lookup. The response
additionally carries parsed User-Agent data under the <<"user_agent">> key:
{ok, Origin} = ipregistry:origin_lookup(Client),
io:format("~ts ~ts~n", [
ipregistry:get(Origin, [ip]),
ipregistry:get(Origin, [location, country, name])
]).Batch IP lookup
batch_lookup resolves many IP addresses in a single request. Each entry may independently succeed or fail (for
example on an invalid address), so results are inspected element by element:
{ok, Results} = ipregistry:batch_lookup(Client, [
<<"73.2.2.2">>, <<"8.8.8.8">>, <<"2001:67c:2e8:22::c100:68b">>
]),
lists:foreach(
fun({ok, Info}) ->
io:format("~ts~n", [ipregistry:get(Info, [location, country, name])]);
({error, {api_error, #{code := Code, message := Message}}}) ->
io:format("entry failed: ~ts (~ts)~n", [Message, Code])
end,
Results).The Ipregistry API accepts up to 1024 IP addresses per request. batch_lookup transparently splits larger lists into
several requests, dispatched with bounded concurrency, and reassembles the results in input order — so you can pass an
arbitrarily long list without hitting TOO_MANY_IPS. Tune the behavior when needed:
Client = ipregistry:new(<<"YOUR_API_KEY">>, #{
max_batch_size => 256, %% addresses per request (up to 1024)
batch_concurrency => 2 %% concurrent sub-requests (default 4)
}).An overall {error, ...} return means the whole request failed (for example on authentication or network errors),
not that an individual entry did.
User-Agent parsing
{ok, [{ok, Parsed}]} = ipregistry:parse_user_agents(Client, [
<<"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36">>
]),
Browser = ipregistry:get(Parsed, [name]).Lookup options
Every lookup function takes an optional trailing options map:
{ok, Info} = ipregistry:lookup(Client, <<"8.8.8.8">>, #{
%% Select response fields with Ipregistry's field selector syntax.
%% Reduces payload size and, in some cases, credit usage. See
%% https://ipregistry.co/docs/filtering-selecting-fields
fields => <<"location.country,security">>,
%% Enable reverse-DNS hostname resolution (disabled by default).
hostname => true,
%% Arbitrary extra query parameters, for options without a dedicated key.
params => #{}
}).Client options
ipregistry:new/2 accepts the following options:
| Option | Default | Description |
|---|---|---|
base_url | <<"https://api.ipregistry.co">> | API base URL. Use ipregistry:eu_base_url() to have your data processed in the EU only, or point at a private deployment. |
timeout | 15000 | Per-request timeout in milliseconds (connect and receive). |
max_retries | 3 | Automatic retries performed in addition to the initial attempt. 0 disables retries. |
retry_interval | 1000 | Base backoff in milliseconds; successive retries wait exponentially longer (interval * 2^attempt). A Retry-After header on a 429 response takes precedence. |
retry_on_server_error | true | Retry 5xx responses. Transient transport errors are always retried up to max_retries. |
retry_on_too_many_requests | false | Retry 429 responses, honoring Retry-After. Ipregistry does not rate limit by default (it is opt-in per API key). |
max_batch_size | 1024 | Addresses per batch request (capped at the API limit of 1024). |
batch_concurrency | 4 | Concurrent sub-requests when a batch is split into chunks. Set to 1 for strictly sequential dispatch. |
cache | undefined | Name (or pid) of an ipregistry_cache process to memoize lookups. |
user_agent | IpregistryClient/Erlang/<version> | Value of the User-Agent header sent with requests. |
Caching
By default no cache is used, so data is never stale. To enable in-process caching, start an ipregistry_cache — a
gen_server owning an ETS table with time-based expiration and bounded size — and reference it from the client:
%% Standalone:
{ok, _} = ipregistry_cache:start_link(#{
name => ipregistry_cache, %% registered name (default)
ttl => 600000, %% entry lifetime in ms (default: 10 minutes)
max_size => 4096 %% max entries; oldest evicted first (default)
}),
Client = ipregistry:new(<<"YOUR_API_KEY">>, #{cache => ipregistry_cache}),
{ok, Info1} = ipregistry:lookup(Client, <<"8.8.8.8">>), %% hits the API
{ok, Info2} = ipregistry:lookup(Client, <<"8.8.8.8">>). %% served from ETSIn an OTP application, put it under your supervision tree instead:
init([]) ->
Children = [
ipregistry_cache:child_spec(#{name => ipregistry_cache, ttl => 600000})
],
{ok, {#{strategy => one_for_one}, Children}}.Cache reads are served directly from the ETS table without going through the server process. Writes are best-effort: if the cache process is down (for example while its supervisor restarts it), lookups keep working without caching rather than failing. Origin lookups are never cached, and batch lookups reuse cached entries, requesting only the misses.
Error handling
All lookup functions return {ok, Result} or {error, Reason}:
case ipregistry:lookup(Client, <<"8.8.8.8">>) of
{ok, Info} ->
use(Info);
{error, {api_error, #{code := <<"INSUFFICIENT_CREDITS">>}}} ->
%% The API rejected the request. `code' is one of the codes listed
%% at https://ipregistry.co/docs/errors, and the map also carries
%% `message', `resolution', and the HTTP `status'.
handle_quota();
{error, {client_error, Reason}} ->
%% The request never got a valid API response: invalid input,
%% transport error, or an undecodable payload.
handle_transport(Reason)
end.Misconfiguration (ipregistry:new/2 with an unknown option, an empty API key, and so on) raises an error instead,
since it is a programming mistake rather than a runtime condition.
Development
Everyday tasks are wrapped in a Makefile:
make # compile, format check, xref, dialyzer, eunit, common test
make eunit # unit tests
make ct # behavior tests against a local mock API server (offline)
make fmt # format code with erlfmt
make docs # generate documentation with ex_doc
Unit and behavior tests are fully offline: the Common Test suite spins up a local mock HTTP server that plays the role of the Ipregistry API. Live system tests run against the real API, consume credits, and only run when an API key is provided:
IPREGISTRY_API_KEY=your_key make system-tests
Without Erlang installed locally, run everything in a container:
make docker-check
Releasing
Releases are cut from the Release workflow: bump {vsn, "X.Y.Z"} in
src/ipregistry.app.src, add a ## [X.Y.Z] section to CHANGELOG.md, then trigger the workflow with the version.
It runs the full test gate (including live system tests), tags vX.Y.Z, creates a GitHub release with the changelog
section as notes, and publishes the package to Hex.pm.
Other Languages
Ipregistry client libraries are available in many languages: https://ipregistry.co/docs/libraries