RevolutClient
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 - Exponential backoff + jitter on retryable errors (5xx, network failures, 429)
- Token-bucket rate limiting (optional, per-client)
- Telemetry events on every request (
:start,:stop,:exception) - Type-safe error hierarchy — pattern-match precisely on
RevolutClient.Error.* - Webhook behaviour —
use RevolutClient.Webhookto 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
@specand@doccoverage — usemix docsto build HexDocs locally
Installation
Add revolut_client to your mix.exs:
def deps do
[
{:revolut_client, "~> 1.0"}
]
endQuickstart
# 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
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)
config :revolut_client,
environment: :sandbox,
timeout_ms: 10_000,
max_attempts: 2Per-client options always override application-level defaults.
Error handling
All functions return {:ok, result} or {:error, RevolutClient.Error.t()}:
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__
endError 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
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
endProcess in a Phoenix controller
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
endRaw verification (no macro)
RevolutClient.Webhook.verify(raw_body, signature_header, secret)
# => :ok | {:error, %RevolutClient.Error.Webhook{}}Telemetry
: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
mix deps.get
mix test
mix test --cover # with coverage report
mix credo --strict
mix dialyzerLicense
MIT — see LICENSE.