<img src="https://cdn.ipregistry.co/icons/favicon-96x96.png" alt="Ipregistry" width="64"/>

Ipregistry Erlang Client Library

License Hex.pm Hex Docs CI

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"}]
end

Then 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:

OptionDefaultDescription
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.
timeout15000Per-request timeout in milliseconds (connect and receive).
max_retries3Automatic retries performed in addition to the initial attempt. 0 disables retries.
retry_interval1000Base backoff in milliseconds; successive retries wait exponentially longer (interval * 2^attempt). A Retry-After header on a 429 response takes precedence.
retry_on_server_errortrueRetry 5xx responses. Transient transport errors are always retried up to max_retries.
retry_on_too_many_requestsfalseRetry 429 responses, honoring Retry-After. Ipregistry does not rate limit by default (it is opt-in per API key).
max_batch_size1024Addresses per batch request (capped at the API limit of 1024).
batch_concurrency4Concurrent sub-requests when a batch is split into chunks. Set to 1 for strictly sequential dispatch.
cacheundefinedName (or pid) of an ipregistry_cache process to memoize lookups.
user_agentIpregistryClient/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 ETS

In 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