Packages

Official Elixir client for the UniRate API — free currency exchange rates, historical data, and VAT rates.

Current section

Files

Jump to
unirate_api README.md
Raw

README.md

# UniRate Elixir Client

Official Elixir client for the [UniRate API](https://unirateapi.com) — free
currency exchange rates, historical data, and VAT rates.

Built on the Erlang/OTP standard library only (`:httpc` for HTTP and the OTP 27
`:json` module for parsing): **zero third-party dependencies**.

## Requirements

- Elixir `~> 1.17`
- **Erlang/OTP 27 or newer** (the `:json` module ships with OTP 27)

## Installation

Add `unirate_api` to your `deps` in `mix.exs`:

```elixir
def deps do
  [
    {:unirate_api, "~> 0.1.0"}
  ]
end
```

Then run `mix deps.get`.

## Quick start

```elixir
client = UniRate.Client.new("your-api-key")

# A single exchange rate.
UniRate.Client.get_rate(client, "USD", "EUR")
#=> 0.92

# All rates for a base currency.
UniRate.Client.get_rate(client, "USD")
#=> %{"EUR" => 0.92, "GBP" => 0.79, ...}

# Convert an amount.
UniRate.Client.convert(client, "EUR", from: "USD", amount: 100)
#=> 92.5

# Supported currencies.
UniRate.Client.get_supported_currencies(client)
#=> ["USD", "EUR", "GBP", ...]

# VAT rate for a country.
UniRate.Client.get_vat_rate(client, "DE")
#=> %{"country_code" => "DE", "country_name" => "Germany", "vat_rate" => 19.0}
```

Currency and country codes are upper-cased automatically before being sent.

## Configuration

```elixir
UniRate.Client.new("your-api-key",
  timeout: 30_000,                      # request timeout in milliseconds
  base_url: "https://api.unirateapi.com"
)
```

## API

### Current data (free tier)

| Method | Returns |
|---|---|
| `get_rate(client, from \\ "USD", to \\ nil, opts \\ [])` | a number when `to` is set, else a `%{code => rate}` map |
| `convert(client, to, opts \\ [])` | the converted amount (options: `:amount`, `:from`) |
| `get_supported_currencies(client, opts \\ [])` | a list of currency-code strings |
| `get_vat_rates(client, opts \\ [])` | the full VAT response (option: `:country`) |
| `get_vat_rate(client, country, opts \\ [])` | the `vat_data` object for one ISO-3166 alpha-2 country |

### Historical data (Pro plan)

These endpoints require a Pro subscription and return `403` on the free tier
(surfaced as a `UniRate.Error` with `reason: :api` and `status: 403`).

| Method |
|---|
| `get_historical_rate(client, date, opts \\ [])` — options `:amount`, `:from`, `:to` |
| `get_historical_rates(client, date, opts \\ [])` — options `:amount`, `:base` |
| `convert_historical(client, amount, from, to, date, opts \\ [])` |
| `get_time_series(client, start_date, end_date, opts \\ [])` — options `:amount`, `:base`, `:currencies` |
| `get_historical_limits(client, opts \\ [])` |

Every method also accepts `:format` (`"json"` default, or `"xml"` / `"csv"` /
`"tsv"`) and `:callback`. When `:format` is not `"json"`, the raw response body
string is returned instead of a parsed value.

## Error handling

Every failure is a `UniRate.Error` — rescue the whole family with one clause and
narrow on `:reason`:

```elixir
try do
  UniRate.Client.get_rate(client, "USD", "ZZZ")
rescue
  e in UniRate.Error ->
    case e.reason do
      :invalid_currency -> IO.puts("unknown currency")
      :authentication   -> IO.puts("bad API key")
      :rate_limit       -> IO.puts("slow down")
      :api              -> IO.puts("API error #{e.status}: #{e.message}")
      :transport        -> IO.puts("network error: #{e.message}")
      _                 -> reraise(e, __STACKTRACE__)
    end
end
```

| Reason | HTTP | Meaning |
|---|---|---|
| `:invalid_date` | 400 | Invalid request parameters |
| `:authentication` | 401 | Missing or invalid API key |
| `:api` | 403 | Endpoint requires a Pro subscription |
| `:invalid_currency` | 404 | Currency not found or no data available |
| `:rate_limit` | 429 | Rate limit exceeded |
| `:api` | 503 / other | Service unavailable / generic (carries `status` + `body`) |
| `:transport` | — | Network/transport failure or unparseable response |

## Rate limits

The free tier is rate-limited. On HTTP 429 the client raises a `UniRate.Error`
with `reason: :rate_limit`; back off and retry.

## Testing

```bash
mix test                              # mock tests only (default)
UNIRATE_API_KEY=... mix test --include live   # + live free-tier tests
```

Mock tests inject a transport function via the `:http` option, so they never hit
the network.

## Related clients

UniRate ships official clients for many languages — Python, Node/TypeScript,
Go, Rust, Ruby, PHP, Java, Swift, and more. See
[github.com/UniRate-API](https://github.com/UniRate-API).

## License

MIT © 2026 Unirate Team. See [LICENSE](LICENSE).