defmodule Rapyd do
@moduledoc """
Production-grade Elixir SDK for the [Rapyd](https://www.rapyd.net/)
fintech-as-a-service platform.
## Quick Start
client = Rapyd.new!(
access_key: System.fetch_env!("RAPYD_ACCESS_KEY"),
secret_key: System.fetch_env!("RAPYD_SECRET_KEY"),
sandbox: true
)
{:ok, payment} = Rapyd.Services.Collect.create_payment(client, %{
amount: 100.00,
currency: "USD",
payment_method: %{
type: "us_visa_card",
fields: %{
number: "4111111111111111",
expiration_month: "12",
expiration_year: "2026",
cvv: "123"
}
}
})
## Services
All API interaction is routed through domain-aligned service modules:
| Service | Description |
|---|---|
| `Rapyd.Services.Collect` | Accept payments, refunds, subscriptions, customers |
| `Rapyd.Services.Disburse` | Send payouts to beneficiaries worldwide |
| `Rapyd.Services.Wallet` | Manage eWallets, contacts, virtual accounts, KYC |
| `Rapyd.Services.Issuing` | Issue and manage physical and virtual cards |
| `Rapyd.Services.Partner` | PayFac / KYB onboarding for sub-merchants |
| `Rapyd.Services.Webhook` | Verify and route incoming Rapyd webhook events |
| `Rapyd.Services.Resource` | Reference data: FX rates, countries, currencies |
## Error Handling
All API calls return `{:ok, result}` or `{:error, %Rapyd.Error{}}`.
case Rapyd.Services.Collect.create_payment(client, req) do
{:ok, payment} ->
IO.inspect(payment.id)
{:error, %Rapyd.Error{type: :insufficient_funds}} ->
# prompt customer for another payment method
{:error, %Rapyd.Error{type: :rate_limit}} ->
# back off and retry
{:error, %Rapyd.Error{} = err} ->
Logger.error("payment failed", code: err.error_code, op: err.operation_id)
end
## Configuration
Options accepted by `new/1` and `new!/1`:
| Option | Type | Default | Description |
|---|---|---|---|
| `:access_key` | `String.t()` | required | Rapyd access key |
| `:secret_key` | `String.t()` | required | Rapyd secret key |
| `:sandbox` | `boolean()` | `true` | Use sandbox (`true`) or production (`false`) |
| `:base_url` | `String.t()` | auto | Override the API base URL |
| `:max_retries` | `non_neg_integer()` | `4` | Max request attempts (1 = no retries) |
| `:timeout` | `non_neg_integer()` | `30_000` | Request timeout in milliseconds |
| `:http_client` | module | `Rapyd.HTTP.Client` | Swappable HTTP client module |
"""
alias Rapyd.Client
@sandbox_url "https://sandboxapi.rapyd.net"
@production_url "https://api.rapyd.net"
@doc """
Builds a new `Rapyd.Client` from the given options.
Returns `{:ok, client}` or `{:error, reason}`.
## Examples
{:ok, client} = Rapyd.new(
access_key: "your_key",
secret_key: "your_secret",
sandbox: true
)
"""
@spec new(keyword()) :: {:ok, Client.t()} | {:error, String.t()}
def new(opts) do
with {:ok, access_key} <- require_opt(opts, :access_key),
{:ok, secret_key} <- require_opt(opts, :secret_key) do
{:ok, build_client(opts, access_key, secret_key)}
end
end
@doc """
Builds a new `Rapyd.Client` from the given options, raising on error.
## Examples
client = Rapyd.new!(
access_key: System.fetch_env!("RAPYD_ACCESS_KEY"),
secret_key: System.fetch_env!("RAPYD_SECRET_KEY")
)
"""
@spec new!(keyword()) :: Client.t()
def new!(opts) do
case new(opts) do
{:ok, client} -> client
{:error, reason} -> raise ArgumentError, "Rapyd.new!/1 failed: #{reason}"
end
end
@doc """
Returns the SDK version string.
"""
@spec version() :: String.t()
def version, do: "1.0.0"
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp build_client(opts, access_key, secret_key) do
sandbox = Keyword.get(opts, :sandbox, true)
%Client{
access_key: access_key,
secret_key: secret_key,
sandbox: sandbox,
base_url: resolve_base_url(opts, sandbox),
max_retries: Keyword.get(opts, :max_retries, 4),
timeout: Keyword.get(opts, :timeout, 30_000),
http_client: Keyword.get(opts, :http_client, Rapyd.HTTP.Client)
}
end
defp resolve_base_url(opts, sandbox) do
case Keyword.get(opts, :base_url) do
nil -> if sandbox, do: @sandbox_url, else: @production_url
url -> url
end
end
defp require_opt(opts, key) do
case Keyword.get(opts, key) do
nil -> {:error, "#{key} is required"}
"" -> {:error, "#{key} must not be empty"}
val -> {:ok, val}
end
end
end