# RevolutClient
[](https://hex.pm/packages/revolut_client)
[](https://hexdocs.pm/revolut_client)
[](https://github.com/iamkanishka/revolut_client/actions)
[](https://coveralls.io/github/iamkanishka/revolut_client?branch=main)
Production-grade Elixir SDK for the complete **Revolut Developer API**.
| API | Module | Description |
| --------------- | ------------------------------ | ------------------------------------------------------------- |
| Merchant API | `RevolutClient.Merchant` | Orders, customers, subscriptions, disputes, payouts, webhooks |
| Business API | `RevolutClient.Business` | Accounts, cards, payments, team members, webhooks v1/v2 |
| Open Banking | `RevolutClient.OpenBanking` | Full AISP + PISP (OB v3.1) |
| Crypto Ramp | `RevolutClient.CryptoRamp` | Fiat ↔ crypto on-ramp partner API v2 |
| Crypto Exchange | `RevolutClient.CryptoExchange` | Revolut X trading API |
| Webhooks | `RevolutClient.Webhook` | HMAC verification + typed event dispatch |
---
## Features
- **Zero custom HTTP layer** — backed by [`Req`](https://hex.pm/packages/req)
- **Exponential backoff + jitter** on retryable errors (5xx, network failures, 429)
- **Token-bucket rate limiting** (optional, per-client)
- **[Telemetry](https://hex.pm/packages/telemetry)** events on every request (`:start`, `:stop`, `:exception`)
- **Type-safe error hierarchy** — pattern-match precisely on `RevolutClient.Error.*`
- **Webhook behaviour** — `use RevolutClient.Webhook` to build a verified event handler in seconds
- **HMAC-SHA256 signature verification** with constant-time comparison and replay-attack protection
- **Mockable HTTP adapter** — swap in `RevolutClient.MockHTTP` (Mox) for all tests
- **Full `@spec` and `@doc` coverage** — use `mix docs` to build HexDocs locally
---
## Installation
Add `revolut_client` to your `mix.exs`:
```elixir
def deps do
[
{:revolut_client, "~> 1.0"}
]
end
```
---
## Quickstart
```elixir
# Build a config
config = RevolutClient.Config.new!(
api_key: System.fetch_env!("REVOLUT_MERCHANT_KEY"),
environment: :sandbox
)
# Create a Merchant client
merchant = RevolutClient.merchant(config)
# Create an order
{:ok, order} = RevolutClient.Merchant.create_order(merchant, %{
amount: 1000, # minor units (pence)
currency: "GBP",
description: "Widget order"
})
# Capture it
{:ok, _} = RevolutClient.Merchant.capture_order(merchant, order["id"])
```
---
## Configuration
### Per-client
```elixir
config = RevolutClient.Config.new!(
api_key: "sk_live_...",
environment: :prod,
timeout_ms: 15_000,
max_attempts: 3,
initial_delay_ms: 250,
rate_limit: %{requests_per_second: 10, burst: 20}
)
```
### Application-level defaults (`config.exs`)
```elixir
config :revolut_client,
environment: :sandbox,
timeout_ms: 10_000,
max_attempts: 2
```
Per-client options always override application-level defaults.
---
## Error handling
All functions return `{:ok, result}` or `{:error, RevolutClient.Error.t()}`:
```elixir
case RevolutClient.Merchant.get_order(client, order_id) do
{:ok, order} -> process(order)
{:error, %RevolutClient.Error.API{status_code: 404}} -> {:reply, :not_found, state}
{:error, %RevolutClient.Error.API{retryable?: true}} -> retry_later()
{:error, %RevolutClient.Error.Network{}} -> retry_later()
{:error, err} -> reraise err, __STACKTRACE__
end
```
Error types:
| Type | When |
| ----------------------------------- | -------------------------------------- |
| `RevolutClient.Error.API` | Non-2xx HTTP response from Revolut |
| `RevolutClient.Error.Network` | Transport failure (timeout, DNS, TLS) |
| `RevolutClient.Error.Validation` | Bad argument caught before request |
| `RevolutClient.Error.Configuration` | SDK misconfiguration |
| `RevolutClient.Error.Webhook` | Invalid signature or malformed payload |
| `RevolutClient.Error.Serialization` | JSON encode/decode failure |
---
## Webhooks
### Define a handler
```elixir
defmodule MyApp.RevolutWebhook do
use RevolutClient.Webhook,
secret: System.fetch_env!("REVOLUT_WEBHOOK_SECRET")
@impl RevolutClient.Webhook
def handle_event("ORDER_COMPLETED", payload, _meta) do
MyApp.Orders.fulfill(payload["order_id"])
:ok
end
@impl RevolutClient.Webhook
def handle_event(_type, _payload, _meta), do: :ok
end
```
### Process in a Phoenix controller
```elixir
def webhook(conn, _params) do
raw_body = conn.assigns[:raw_body]
signature = get_req_header(conn, "revolut-signature") |> List.first()
case MyApp.RevolutWebhook.process(raw_body, signature) do
{:ok, _} -> send_resp(conn, 200, "")
{:error, %RevolutClient.Error.Webhook{}} -> send_resp(conn, 401, "")
{:error, _} -> send_resp(conn, 400, "")
end
end
```
### Raw verification (no macro)
```elixir
RevolutClient.Webhook.verify(raw_body, signature_header, secret)
# => :ok | {:error, %RevolutClient.Error.Webhook{}}
```
---
## Telemetry
```elixir
:telemetry.attach(
"my-revolut-logger",
[:revolut_client, :request, :stop],
fn _event, %{duration: dur}, %{status: status, url: url}, _cfg ->
Logger.info("Revolut #{url} -> #{status} (#{System.convert_time_unit(dur, :native, :millisecond)}ms)")
end,
nil
)
```
Events emitted:
| Event | Measurements | Metadata |
| ------------------- | ------------- | ------------------------------ |
| `[..., :start]` | `system_time` | `method, url, attempt` |
| `[..., :stop]` | `duration` | `status, attempt, method, url` |
| `[..., :exception]` | `duration` | `reason, attempt, retryable` |
---
## Running tests
```bash
mix deps.get
mix test
mix test --cover # with coverage report
mix credo --strict
mix dialyzer
```
---
## License
MIT — see [LICENSE](LICENSE).