Hex.pm CI Coverage License: MIT

Production-grade Elixir SDK for the Rapyd fintech-as-a-service platform.

Features

  • Full API surface — Collect, Disburse, Wallet, Issuing, Partner, Webhook, Resource
  • 178+ service methods across 7 domain-aligned service modules
  • HMAC-SHA256 request signing — every request automatically authenticated
  • Webhook verification — constant-time HMAC comparison, typed event structs, dispatch router
  • Full-jitter exponential backoff — configurable retries on 429/5xx
  • Structured errors — 17 semantic error types, retryability flag, operation ID
  • Swappable HTTP client — inject a Mox mock for zero-network tests
  • Full typespec coverage@spec and @type on all public APIs
  • Zero business-logic dependencies — only req and jason

Installation

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

Quick Start

# Build a client (do this once at application start, store in your context/config)
client = Rapyd.new!(
  access_key: System.fetch_env!("RAPYD_ACCESS_KEY"),
  secret_key:  System.fetch_env!("RAPYD_SECRET_KEY"),
  sandbox:     true   # false for production
)

# Accept a payment
{: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"
    }
  }
})

IO.puts("Payment created: #{payment["id"]} status=#{payment["status"]}")

Configuration

client = Rapyd.new!(
  access_key:  "your_access_key",     # required
  secret_key:  "your_secret_key",     # required
  sandbox:     true,                  # default: true
  max_retries: 4,                     # default: 4 (1 = no retries)
  timeout:     30_000,                # default: 30 000 ms
  http_client: Rapyd.HTTP.Client    # default; swap for tests
)

Services

Collect

alias Rapyd.Services.Collect

# Payments
{:ok, payment}      = Collect.create_payment(client, params)
{:ok, payment}      = Collect.get_payment(client, "pay_id")
{:ok, payments}     = Collect.list_payments(client, %{currency: "USD"})
{:ok, payment}      = Collect.capture_payment(client, "pay_id")
{:ok, _}            = Collect.cancel_payment(client, "pay_id")

# Hosted checkout
{:ok, page}         = Collect.create_checkout_page(client, params)

# Refunds
{:ok, refund}       = Collect.create_refund(client, %{payment: "pay_id", amount: 25})

# Customers & subscriptions
{:ok, customer}     = Collect.create_customer(client, %{email: "user@example.com"})
{:ok, plan}         = Collect.create_plan(client, params)
{:ok, sub}          = Collect.create_subscription(client, %{customer: "cus_id", plan: "plan_id"})

Disburse

alias Rapyd.Services.Disburse

{:ok, beneficiary}  = Disburse.create_beneficiary(client, params)
{:ok, payout}       = Disburse.create_payout(client, params)
{:ok, payout}       = Disburse.confirm_payout(client, "payout_id")
{:ok, methods}      = Disburse.list_payout_method_types(client, %{sender_country: "US"})

Wallet

alias Rapyd.Services.Wallet

{:ok, wallet}       = Wallet.create_wallet(client, %{type: "person", first_name: "Ada"})
{:ok, _}            = Wallet.change_wallet_status(client, "ew_id", "disable")
{:ok, contact}      = Wallet.create_contact(client, "ew_id", params)
{:ok, txns}         = Wallet.list_wallet_transactions(client, "ew_id", %{currency: "USD"})
{:ok, va}           = Wallet.create_virtual_account(client, %{ewallet: "ew_id", country: "US"})
{:ok, ident}        = Wallet.create_identity_verification(client, params)

Issuing

alias Rapyd.Services.Issuing

{:ok, card}         = Issuing.issue_card(client, %{card_program: "cp_id", ewallet_contact: "con_id"})
{:ok, card}         = Issuing.activate_card(client, "card_id")
{:ok, card}         = Issuing.block_card(client, "card_id")
{:ok, details}      = Issuing.get_card_details(client, "card_id")   # PCI scope
{:ok, txns}         = Issuing.list_card_transactions(client, "card_id")

Partner

alias Rapyd.Services.Partner

{:ok, org}          = Partner.create_organization(client, params)
{:ok, app}          = Partner.create_application(client, params)
{:ok, app}          = Partner.submit_application(client, "app_id")
{:ok, account}      = Partner.create_settlement_bank_account(client, params)

Webhook Handling

Always verify the signature before trusting event data:

defmodule MyApp.WebhookController do
  alias Rapyd.{Services.Webhook, Types.WebhookEvent}

  def handle(conn) do
    raw_body = conn.assigns[:raw_body]

    headers = %{
      salt:      get_req_header(conn, "salt")      |> List.first(),
      timestamp: get_req_header(conn, "timestamp") |> List.first(),
      signature: get_req_header(conn, "signature") |> List.first()
    }

    case Webhook.parse_and_verify(client, raw_body, headers) do
      {:ok, event} ->
        dispatch(event)
        send_resp(conn, 200, "ok")

      {:error, %Rapyd.Error{type: :webhook_signature}} ->
        send_resp(conn, 401, "invalid signature")
    end
  end

  defp dispatch(event) do
    WebhookEvent.dispatch(event, %{
      "PAYMENT_SUCCEEDED" => &handle_payment/1,
      "PAYOUT_COMPLETED"  => &handle_payout/1,
      default:              &log_event/1
    })
  end
end

Error Handling

case Rapyd.Services.Collect.create_payment(client, params) do
  {:ok, payment} ->
    {:ok, payment}

  {:error, %Rapyd.Error{type: :insufficient_funds}} ->
    {:error, :payment_method_declined}

  {:error, %Rapyd.Error{type: :rate_limit, retryable?: true}} ->
    Process.sleep(2_000)
    retry(params)

  {:error, %Rapyd.Error{type: :unauthorized}} ->
    raise "Invalid Rapyd credentials — check RAPYD_ACCESS_KEY / RAPYD_SECRET_KEY"

  {:error, %Rapyd.Error{} = err} ->
    Logger.error("Rapyd API error",
      type:      err.type,
      code:      err.error_code,
      http:      err.status_code,
      operation: err.operation_id
    )
    {:error, :rapyd_error}
end

Error types

TypeHTTPRetryable
:payment_not_found404
:payment_failed400
:payment_canceled400
:insufficient_funds400
:card_declined400
:expired_card400
:invalid_card400
:do_not_honor400
:fraud400
:rate_limit429
:unauthorized401
:forbidden403
:not_found404
:validation
:webhook_signature
:network
:timeout
:api_error / :unknown5xx

Testing

Inject a Mox mock to test without network calls:

# test/test_helper.exs
Mox.defmock(MyApp.MockHTTP, for: Rapyd.HTTP.Behaviour)

# in your test
client = Rapyd.new!(
  access_key:  "key",
  secret_key:  "secret",
  http_client: MyApp.MockHTTP
)

MyApp.MockHTTP
|> expect(:request, fn _client, :post, "/v1/payments", _body, [] ->
  {:ok, %{"id" => "pay_test", "status" => "ACT"}}
end)

License

MIT — see LICENSE.