[<img src="https://cdn.ipregistry.co/icons/favicon-96x96.png" alt="Ipregistry" width="64"/>](https://ipregistry.co/)
# Ipregistry Erlang Client Library
[](LICENSE)
[](https://hex.pm/packages/ipregistry)
[](https://hexdocs.pm/ipregistry)
[](https://github.com/ipregistry/ipregistry-erlang/actions/workflows/ci.yml)
This is the official Erlang client library for the [Ipregistry](https://ipregistry.co) 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](https://ipregistry.co).
### Installation
Requires Erlang/OTP 27 or later.
Add the [Hex package](https://hex.pm/packages/ipregistry) to your `rebar.config`:
```erlang
{deps, [ipregistry]}.
```
or to your Elixir project's `mix.exs`:
```elixir
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
```erlang
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:
```erlang
{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:
```erlang
{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:
```erlang
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
```erlang
{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:
```erlang
{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:
```erlang
%% 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:
```erlang
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}`:
```erlang
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`:
```bash
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:
```bash
IPREGISTRY_API_KEY=your_key make system-tests
```
Without Erlang installed locally, run everything in a container:
```bash
make docker-check
```
### Releasing
Releases are cut from the [Release workflow](.github/workflows/release.yml): 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](https://hex.pm/packages/ipregistry).
## Other Languages
Ipregistry client libraries are available in many languages: https://ipregistry.co/docs/libraries