Paysafe
A production-grade Elixir client for the Paysafe payments platform — covering the Payments API, Payment Scheduler API, Applications (onboarding) API, value-added services, and webhooks.
Features
- 🔌 Complete API coverage — Payment Handles, Payments, Settlements, Refunds, Payouts, Verifications, Customer Vault, Scheduler (Plans & Subscriptions), Applications/Onboarding, FX Rates, Customer Identity (KYC), Bank Account Validation, Network Tokenization, Account Updater.
- 🔒 Secure webhooks — HMAC-SHA256 signature verification with constant-time comparison, typed event parsing, topic-based routing.
- 🔁 Resilient by default — exponential backoff retry on transient failures, token-bucket rate limiting, configurable timeouts.
- 📊 Observable —
:telemetryspans on every request ([:paysafe, :request, :start | :stop | :exception]). - 🧱 Typed everywhere — every API response is parsed into a typed struct
(
Paysafe.Types.*); every error is a structured%Paysafe.Error{}with akind,code, andretryable?flag. - ✅ Battle-tested — config validated via
NimbleOptions; full unit + HTTP integration test suite usingBypass.
Installation
Add paysafe to your list of dependencies in mix.exs:
def deps do
[
{:paysafe, "~> 1.0.0"}
]
end
Configuration
Build a config struct from your Paysafe Business Portal credentials:
config = Paysafe.Config.new!(
username: "1001062690",
password: System.get_env("PAYSAFE_PASSWORD"),
environment: :test, # :test | :production
account_id: "1009688230"
)
Or load from application config (config/runtime.exs):
config :paysafe,
username: System.get_env("PAYSAFE_USERNAME"),
password: System.get_env("PAYSAFE_PASSWORD"),
environment: :test,
account_id: System.get_env("PAYSAFE_ACCOUNT_ID")
config = Paysafe.Config.from_env!()
Every config option:
| Option | Default | Description |
|---|---|---|
:username | (required) | API username from the Business Portal. |
:password | (required) | API password from the Business Portal. |
:environment | :test | :test or :production. |
:account_id | nil | Default Paysafe account ID. Only needed if your API key has multiple accounts configured for the same payment method/currency combination — sent in the request body where applicable, never in the URL path. |
:base_url_override | nil | Override the computed base URL (testing/proxy use cases). |
:timeout | 30_000 | Connect timeout in ms. |
:recv_timeout | 30_000 | Receive timeout in ms. |
:max_retries | 3 | Max retries on transient failures. |
:retry_delay | 500 | Base backoff delay in ms (exponential). |
:rate_limit | {1_000, 100} | {scale_ms, limit} token-bucket rate limit. |
:telemetry_prefix | [:paysafe] | Telemetry event prefix. |
:http_options | [] | Extra options merged into every Req call. |
Quick start — card payment
# 1. Create a payment handle (tokenizes the card)
{:ok, handle} = Paysafe.create_payment_handle(config, %{
merchant_ref_num: "order-#{System.unique_integer([:positive])}",
amount: 5000, # $50.00, in minor units
currency_code: "USD",
payment_type: "CARD",
transaction_type: "PAYMENT",
card: %{
card_num: "4111111111111111",
card_expiry: %{month: 12, year: 2030},
cvv: "123",
holder_name: "Jane Doe"
},
billing_details: %{
street: "123 Main St",
city: "New York",
state: "NY",
country: "US",
zip: "10001"
}
})
# 2. Check whether a redirect (3DS / APM) is required
case Paysafe.handle_action(handle) do
{:redirect, url} ->
# send the customer to `url` to complete authentication
url
:proceed ->
# 3. Submit the actual payment
{:ok, payment} = Paysafe.create_payment(config, %{
merchant_ref_num: "order-001",
amount: 5000,
currency_code: "USD",
settle_with_auth: true,
payment_handle_token: handle.payment_handle_token
})
payment.status #=> :completed
end
Saved cards (Customer Vault) & recurring billing
# Create a customer profile
{:ok, customer} = Paysafe.create_customer(config, %{
merchant_customer_id: "cust-001",
first_name: "Jane",
last_name: "Doe",
email: "jane@example.com"
})
# Convert the single-use token from an initial payment into a multi-use token
{:ok, mut} = Paysafe.create_customer_payment_handle(config, customer.id, %{
merchant_ref_num: "save-card-001",
payment_handle_token_from: handle.payment_handle_token
})
# Create a billing plan
{:ok, plan} = Paysafe.create_plan(config, %{
name: "Monthly Pro",
amount: 1999,
currency_code: "USD",
interval: "MONTHLY",
num_payments: 12
})
# Subscribe the customer using their multi-use token
{:ok, subscription} = Paysafe.create_subscription(config, %{
plan_id: plan.id,
merchant_customer_id: customer.merchant_customer_id,
payment_handle_token: mut["paymentHandleToken"],
merchant_ref_num: "sub-001"
})
Webhooks
Always verify the signature before trusting a webhook payload:
# In a Phoenix controller — make sure you capture the raw body before
# any JSON-parsing plug runs (e.g. via a custom body reader).
def webhook(conn, _params) do
signature = conn |> get_req_header("signature") |> List.first()
raw_body = conn.assigns.raw_body
hmac_key = Application.fetch_env!(:my_app, :paysafe_webhook_hmac_key)
case Paysafe.verify_webhook(raw_body, signature, hmac_key) do
{:ok, event} ->
handle_event(Paysafe.Webhooks.event_topic(event), event)
send_resp(conn, 200, "ok")
{:error, %Paysafe.Error{kind: :webhook_signature_mismatch}} ->
send_resp(conn, 401, "invalid signature")
{:error, _} ->
send_resp(conn, 400, "bad request")
end
end
defp handle_event(:payment_handle, event) do
case Paysafe.Webhooks.payment_handle_status(event) do
:payable -> # safe to call create_payment/2 now
:failed -> # notify the customer
_ -> :ok
end
end
defp handle_event(:subscription, event), do: # ...
defp handle_event(_topic, _event), do: :ok
Error handling
Every function returns {:ok, result} | {:error, %Paysafe.Error{}}:
case Paysafe.create_payment(config, params) do
{:ok, payment} ->
payment
{:error, %Paysafe.Error{code: "3022"}} ->
# insufficient funds — ask for another payment method
{:error, %Paysafe.Error{retryable?: true} = err} ->
Logger.warning("Paysafe call failed, will be retried: #{err}")
{:error, err} ->
Logger.error("Paysafe call failed: #{err}")
end
Paysafe.Error kinds: :api_error, :http_error, :rate_limited,
:timeout, :invalid_config, :invalid_params,
:webhook_signature_mismatch, :decode_error.
Telemetry
:telemetry.attach(
"paysafe-logger",
[:paysafe, :request, :stop],
fn _event, %{duration: duration}, %{operation: op, ok: ok?}, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.info("paysafe.#{op} #{if ok?, do: "ok", else: "error"} #{ms}ms")
end,
nil
)
Supported payment methods
Cards (Visa, Mastercard, Amex, Discover, Debit, Prepaid, Corporate) · Apple Pay · Google Pay · PayPal · Venmo · Skrill · Skrill 1-Tap · Neteller · PaysafeCard · PaysafeCash · ACH (US) · BACS (UK) · EFT (CA) · SEPA (EU) · iDEAL · EPS · BLIK · Interac e-Transfer · Mazooma · Pay by Bank (US) · VIP Preferred · Play+ · Openbucks · Multibanco · MB WAY · SafetyPay Express (Boleto, Pix, MACH, KHIPU) · PagoEfectivo · Rapid Transfer · Pay with Crypto.
Module overview
| Module | Covers |
|---|---|
Paysafe | Top-level facade — delegates to everything below. |
Paysafe.Config | Config struct, validation, URL builders. |
Paysafe.Payments.PaymentHandles | Tokenize payment instruments. |
Paysafe.Payments.Payments | Create / get / list / cancel payments. |
Paysafe.Payments.Settlements | Capture authorized payments. |
Paysafe.Payments.Refunds | Full & partial refunds. |
Paysafe.Payments.Payouts | Standalone & original credits. |
Paysafe.Payments.Verifications | Zero-value card verification. |
Paysafe.Payments.Customers | Customer Vault, saved instruments. |
Paysafe.Scheduler.Plans | Recurring billing plans. |
Paysafe.Scheduler.Subscriptions | Subscriptions, suspend/reactivate/cancel. |
Paysafe.Applications | Merchant onboarding. |
Paysafe.FxRates | Guaranteed FX rate quotes. |
Paysafe.CustomerIdentity | KYC/AML identity verification. |
Paysafe.BankAccountValidation | Bank account ownership verification. |
Paysafe.NetworkTokenization | (not a separate module — see card.network_token fields on PaymentHandles.create/3) |
Paysafe.AccountUpdater | (not a REST API — SFTP/back-office only; module exists only as documentation) |
Paysafe.InteracVerificationService | Interac AML Assist identity verification (Canada). |
Paysafe.Webhooks | HMAC verification & event parsing. |
Paysafe.Error | Structured, typed errors. |
Full reference docs: https://hexdocs.pm/paysafe.
Known limitations
Two onboarding-adjacent products are intentionally not implemented: the Merchant Termination Inquiry API (MATCH/Visa screening) and the PayFac Sub-merchant API (EU/UK PayFac onboarding). Both have API reference pages that render client-side and expose no concrete endpoint path, HTTP method, or JSON example through any documentation source available at the time this library was built — every other endpoint in this library was verified against a real request/response example before being implemented, and these two could not be. Rather than guess at a shape, they were left out. If you have access to Paysafe's OpenAPI/Swagger spec for either product, contributions are very welcome.
Legacy-generation APIs (Cards API, legacy Customer Vault, legacy Direct Debit, Hosted Payments API, Web Services API, Accounts API V1, Split Payouts, Balance Transfers) are also out of scope — Paysafe is steering integrators toward the modern Payments API surface this library covers, and the legacy APIs use an entirely different request/response convention.
Account Updater has no REST surface at all (SFTP + PGP + back-office
configuration only) — Paysafe.AccountUpdater exists solely as a
@moduledoc pointing you to the real process.
Development
mix deps.get
mix test
mix lint # mix format --check-formatted && mix credo --strict
mix dialyzer
mix check # lint + dialyzer + test --cover
License
MIT — see LICENSE.
This is an independent, community-maintained client and is not officially affiliated with or endorsed by Paysafe Limited.