# Rapyd
[](https://hex.pm/packages/rapyd)
[](https://github.com/iamkanishka/rapyd/actions)
[](https://coveralls.io/github/iamkanishka/rapyd)
[](LICENSE)
Production-grade Elixir SDK for the [Rapyd](https://www.rapyd.net/) 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
```elixir
def deps do
[
{:rapyd, "~> 1.0"}
]
end
```
## Quick Start
```elixir
# 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
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
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:
```elixir
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
```elixir
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
| Type | HTTP | Retryable |
|---|---|---|
| `:payment_not_found` | 404 | ✗ |
| `:payment_failed` | 400 | ✗ |
| `:payment_canceled` | 400 | ✗ |
| `:insufficient_funds` | 400 | ✗ |
| `:card_declined` | 400 | ✗ |
| `:expired_card` | 400 | ✗ |
| `:invalid_card` | 400 | ✗ |
| `:do_not_honor` | 400 | ✗ |
| `:fraud` | 400 | ✗ |
| `:rate_limit` | 429 | ✓ |
| `:unauthorized` | 401 | ✗ |
| `:forbidden` | 403 | ✗ |
| `:not_found` | 404 | ✗ |
| `:validation` | — | ✗ |
| `:webhook_signature` | — | ✗ |
| `:network` | — | ✓ |
| `:timeout` | — | ✓ |
| `:api_error` / `:unknown` | 5xx | ✓ |
## Testing
Inject a `Mox` mock to test without network calls:
```elixir
# 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](LICENSE).