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