Packages
revolut_client
1.0.0
Production-grade Elixir SDK for the complete Revolut Developer API — Merchant, Business, Open Banking, Crypto Ramp, and Crypto Exchange.
Current section
Files
Jump to
Current section
Files
lib/revolut_client/cryptoramp.ex
defmodule RevolutClient.CryptoRamp do
@moduledoc """
Revolut Crypto Ramp Partner API client (v2).
## Resources
- **Config** — retrieve partner widget configuration
- **Quotes** — get a buy/sell quote for a crypto-fiat pair
- **Buy Redirect** — generate a hosted checkout URL
- **Orders** — list and retrieve ramp orders
- **Webhooks** — CRUD management + rotate signing secret
- **Signature verification** — `verify_webhook_signature/3`
## Usage
config = RevolutClient.Config.new!(api_key: "partner_key", environment: :sandbox)
client = RevolutClient.CryptoRamp.new(config)
{:ok, quote} = RevolutClient.CryptoRamp.get_quote(client, %{
amount: 100,
fiat_currency: "GBP",
crypto_currency: "BTC",
side: "buy"
})
## Webhook verification
secret = "whsec_..."
payload = ~s({"event":"ORDER_COMPLETED",...})
signature = "t=...,v1=..."
case RevolutClient.CryptoRamp.verify_webhook_signature(client, payload, signature) do
:ok -> # process event
{:error, err} -> # reject
end
"""
alias RevolutClient.{Client, Config, Error, Webhook}
@type t :: %__MODULE__{
http: Client.t(),
webhook_secret: String.t() | nil
}
defstruct [:http, :webhook_secret]
# ---------------------------------------------------------------------------
# Constructor
# ---------------------------------------------------------------------------
@doc "Create a new Crypto Ramp client."
@spec new(Config.t()) :: t()
def new(%Config{} = config) do
%__MODULE__{http: Client.new(:cryptoramp, config)}
end
@doc "Return a copy of the client with a webhook signing secret attached."
@spec with_webhook_secret(t(), String.t()) :: t()
def with_webhook_secret(%__MODULE__{} = client, secret)
when is_binary(secret) and secret != "" do
%{client | webhook_secret: secret}
end
# ---------------------------------------------------------------------------
# CONFIG
# ---------------------------------------------------------------------------
@doc "Retrieve partner widget configuration."
@spec get_config(t()) :: {:ok, map()} | {:error, Error.t()}
def get_config(%__MODULE__{http: http}), do: Client.get(http, "/config")
# ---------------------------------------------------------------------------
# QUOTES
# ---------------------------------------------------------------------------
@doc """
Get a buy/sell quote.
Required fields:
- `:amount` — amount in fiat minor units
- `:fiat_currency` — ISO 4217 fiat code
- `:crypto_currency` — crypto ticker (e.g. `"BTC"`)
- `:side` — `"buy"` or `"sell"`
"""
@spec get_quote(t(), map()) :: {:ok, map()} | {:error, Error.t()}
def get_quote(%__MODULE__{http: http}, params) do
with :ok <- require!(params[:amount], "amount"),
:ok <- require!(params[:fiat_currency], "fiat_currency"),
:ok <- require!(params[:crypto_currency], "crypto_currency"),
:ok <- require!(params[:side], "side") do
query = %{
amount: params[:amount],
fiat_currency: params[:fiat_currency],
crypto_currency: params[:crypto_currency],
side: params[:side]
}
Client.get(http, "/quotes", query: query)
end
end
# ---------------------------------------------------------------------------
# BUY REDIRECT
# ---------------------------------------------------------------------------
@doc """
Generate a hosted buy redirect URL for the Revolut Ramp widget.
Required fields:
- `:fiat_currency` — ISO 4217 fiat code
- `:crypto_currency` — crypto ticker
- `:amount` — fiat amount in minor units
- `:redirect_url` — URL to redirect the user back to after checkout
"""
@spec get_buy_redirect(t(), map()) :: {:ok, map()} | {:error, Error.t()}
def get_buy_redirect(%__MODULE__{http: http}, params) do
with :ok <- require!(params[:fiat_currency], "fiat_currency"),
:ok <- require!(params[:crypto_currency], "crypto_currency"),
:ok <- require!(params[:amount], "amount"),
:ok <- require!(params[:redirect_url], "redirect_url") do
Client.post(http, "/buy-redirect", body: params)
end
end
# ---------------------------------------------------------------------------
# ORDERS
# ---------------------------------------------------------------------------
@doc "List ramp orders with optional cursor-based pagination."
@spec list_orders(t(), map()) :: {:ok, map()} | {:error, Error.t()}
def list_orders(%__MODULE__{http: http}, params \\ %{}) do
query =
reject_nils(%{
state: params[:state],
fiat_currency: params[:fiat_currency],
crypto_currency: params[:crypto_currency],
from: params[:date_from],
to: params[:date_to],
limit: params[:limit],
cursor: params[:cursor]
})
Client.get(http, "/orders", query: query)
end
@doc "Retrieve a specific ramp order by ID."
@spec get_order(t(), String.t()) :: {:ok, map()} | {:error, Error.t()}
def get_order(%__MODULE__{http: http}, order_id) do
with :ok <- require!(order_id, "order_id") do
Client.get(http, "/orders/#{order_id}")
end
end
# ---------------------------------------------------------------------------
# WEBHOOKS
# ---------------------------------------------------------------------------
@doc """
Create a ramp webhook.
Required fields: `:url`, `:events` (non-empty list of event type strings).
"""
@spec create_webhook(t(), map()) :: {:ok, map()} | {:error, Error.t()}
def create_webhook(%__MODULE__{http: http}, params) do
cond do
is_nil(params[:url]) or params[:url] == "" ->
{:error, %Error.Validation{field: "url", message: "is required"}}
Enum.empty?(params[:events] || []) ->
{:error,
%Error.Validation{field: "events", message: "at least one event type is required"}}
true ->
Client.post(http, "/webhooks", body: params)
end
end
@doc "Retrieve a ramp webhook."
@spec get_webhook(t(), String.t()) :: {:ok, map()} | {:error, Error.t()}
def get_webhook(%__MODULE__{http: http}, webhook_id) do
with :ok <- require!(webhook_id, "webhook_id") do
Client.get(http, "/webhooks/#{webhook_id}")
end
end
@doc "Update a ramp webhook."
@spec update_webhook(t(), String.t(), map()) :: {:ok, map()} | {:error, Error.t()}
def update_webhook(%__MODULE__{http: http}, webhook_id, params) do
with :ok <- require!(webhook_id, "webhook_id") do
Client.patch(http, "/webhooks/#{webhook_id}", body: params)
end
end
@doc "Delete a ramp webhook."
@spec delete_webhook(t(), String.t()) :: {:ok, nil} | {:error, Error.t()}
def delete_webhook(%__MODULE__{http: http}, webhook_id) do
with :ok <- require!(webhook_id, "webhook_id") do
Client.delete(http, "/webhooks/#{webhook_id}")
end
end
@doc "List all ramp webhooks."
@spec list_webhooks(t()) :: {:ok, list()} | {:error, Error.t()}
def list_webhooks(%__MODULE__{http: http}), do: Client.get(http, "/webhooks")
@doc """
Rotate the signing secret for a ramp webhook.
Optionally supply `%{expiration_period: \"P1D\"}` (max `\"P7D\"`) to keep the
old secret valid for a grace period during rollover.
"""
@spec rotate_webhook_secret(t(), String.t(), map()) :: {:ok, map()} | {:error, Error.t()}
def rotate_webhook_secret(%__MODULE__{http: http}, webhook_id, params \\ %{}) do
with :ok <- require!(webhook_id, "webhook_id") do
Client.post(http, "/webhooks/#{webhook_id}/rotate-signing-secret", body: params)
end
end
# ---------------------------------------------------------------------------
# SIGNATURE VERIFICATION
# ---------------------------------------------------------------------------
@doc """
Verify a `Revolut-Signature` header for an incoming ramp webhook.
The client must have been configured via `with_webhook_secret/2`.
Returns `:ok` on success, `{:error, RevolutClient.Error.Webhook}` on failure.
Performs replay-attack protection using the timestamp embedded in the header
(rejects events older than 5 minutes by default).
## Parameters
- `client` — client with `webhook_secret` set
- `raw_body` — the raw request body binary (before any JSON parsing)
- `signature_header` — the `Revolut-Signature` header string
- `tolerance_seconds` — maximum age in seconds (default `300`)
"""
@spec verify_webhook_signature(t(), String.t(), String.t(), non_neg_integer()) ::
:ok | {:error, Error.Webhook.t()}
def verify_webhook_signature(
%__MODULE__{webhook_secret: secret},
raw_body,
signature_header,
tolerance_seconds \\ 300
) do
case secret do
nil ->
{:error,
%Error.Webhook{
message: "no webhook_secret configured — call with_webhook_secret/2 first"
}}
secret ->
Webhook.Crypto.verify(raw_body, signature_header, secret, tolerance_seconds)
end
end
@doc """
Parse and verify an incoming ramp webhook in one call.
Returns `{:ok, decoded_map}` if the signature is valid, otherwise an error.
"""
@spec parse_webhook_payload(t(), String.t(), String.t()) ::
{:ok, map()} | {:error, Error.t()}
def parse_webhook_payload(%__MODULE__{} = client, raw_body, signature_header) do
with :ok <- verify_webhook_signature(client, raw_body, signature_header) do
case Jason.decode(raw_body) do
{:ok, map} ->
{:ok, map}
{:error, reason} ->
{:error, %Error.Serialization{message: "invalid JSON webhook payload", cause: reason}}
end
end
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp require!(nil, field), do: {:error, %Error.Validation{field: field, message: "is required"}}
defp require!("", field), do: {:error, %Error.Validation{field: field, message: "is required"}}
defp require!(_, _), do: :ok
defp reject_nils(map), do: Enum.reject(map, fn {_, v} -> is_nil(v) end) |> Map.new()
end