RevolutClient

Hex.pmHex DocsCICoverage Status

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


Installation

Add revolut_client to your mix.exs:

def deps do
  [
    {:revolut_client, "~> 1.0"}
  ]
end

Quickstart

# 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: 2

Per-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__
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

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

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)

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_timemethod, url, attempt
[..., :stop]durationstatus, attempt, method, url
[..., :exception]durationreason, attempt, retryable

Running tests

mix deps.get
mix test
mix test --cover   # with coverage report
mix credo --strict
mix dialyzer

License

MIT — see LICENSE.