Skip to main content

README.md

# Paysafe

[![Hex.pm](https://img.shields.io/hexpm/v/paysafe.svg)](https://hex.pm/packages/paysafe)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/paysafe)
[![License](https://img.shields.io/hexpm/l/paysafe.svg)](https://github.com/your-org/paysafe/blob/main/LICENSE)

A production-grade Elixir client for the [Paysafe](https://developer.paysafe.com)
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** — `:telemetry` spans 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 a
  `kind`, `code`, and `retryable?` flag.
- ✅ **Battle-tested** — config validated via `NimbleOptions`; full unit +
  HTTP integration test suite using `Bypass`.

## Installation

Add `paysafe` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:paysafe, "~> 1.0.0"}
  ]
end
```

## Configuration

Build a config struct from your Paysafe Business Portal credentials:

```elixir
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`):

```elixir
config :paysafe,
  username: System.get_env("PAYSAFE_USERNAME"),
  password: System.get_env("PAYSAFE_PASSWORD"),
  environment: :test,
  account_id: System.get_env("PAYSAFE_ACCOUNT_ID")
```

```elixir
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

```elixir
# 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

```elixir
# 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:

```elixir
# 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{}}`:

```elixir
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

```elixir
: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](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

```bash
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](LICENSE).

This is an independent, community-maintained client and is not officially
affiliated with or endorsed by Paysafe Limited.