# GoCardlessClient Elixir SDK (`gocardless_client`)
[](https://hex.pm/packages/gocardless_client)
[](https://hexdocs.pm/gocardless_client)
[](https://github.com/iamkanishka/gocardless_client/actions)
[](LICENSE)
Production-ready Elixir client for the [GoCardlessClient API](https://developer.gocardless.com/api-reference/). Full coverage of all 44 resource endpoints — payments, mandates, billing requests, subscriptions, webhooks, OAuth2, outbound payments, and more.
---
## Features
| Capability | Detail |
|---|---|
| **Complete API** | All 44 GoCardlessClient resource services |
| **Open Banking** | Billing Requests, Bank Authorisations, Institutions |
| **Outbound Payments** | Send money with ECDSA/RSA request signing |
| **OAuth2** | Partner platform auth-URL, token exchange, lookup, disconnect |
| **Resilience** | Exponential backoff + full jitter, respects `Retry-After` |
| **Pagination** | Lazy `Stream` — zero memory pressure on large datasets |
| **Webhooks** | HMAC-SHA256 verification, Phoenix Plug middleware, IP allowlist |
| **Telemetry** | `[:gocardless, :request, :start/stop/exception]` events |
| **Rate limits** | ETS-backed `X-RateLimit-*` tracking, accessible at runtime |
| **Config** | NimbleOptions-validated schema — catches misconfiguration at startup |
| **OTP** | Finch connection pools, supervised under `GoCardlessClient.Supervisor` |
---
## Installation
Add to `mix.exs`:
```elixir
def deps do
[{:gocardless_client, "~> 1.0"}]
end
```
---
## Configuration
```elixir
# config/config.exs
config :gocardless_client,
access_token: System.get_env("GOCARDLESS_ACCESS_TOKEN"),
environment: :sandbox, # or :live
timeout: 30_000,
max_retries: 3
```
### Runtime / per-request configuration
```elixir
# Build a client at runtime
client = GoCardlessClient.Client.new!(access_token: token, environment: :live)
# Override token for a single request (OAuth partner apps)
client = GoCardlessClient.Client.with_token(client, merchant_token)
```
---
## Quick Start
```elixir
client = GoCardlessClient.client!()
# Create a customer
{:ok, customer} = GoCardlessClient.Resources.Customers.create(client, %{
email: "alice@example.com",
given_name: "Alice",
family_name: "Smith",
country_code: "GB"
})
# Create a customer bank account
{:ok, bank_account} = GoCardlessClient.Resources.CustomerBankAccounts.create(client, %{
account_holder_name: "Alice Smith",
account_number: "55779911",
branch_code: "200000",
country_code: "GB",
links: %{customer: customer["id"]}
})
# Create a mandate
{:ok, mandate} = GoCardlessClient.Resources.Mandates.create(client, %{
scheme: "bacs",
links: %{customer_bank_account: bank_account["id"]}
})
# Create a payment
{:ok, payment} = GoCardlessClient.Resources.Payments.create(client, %{
amount: 1500,
currency: "GBP",
description: "Monthly subscription",
links: %{mandate: mandate["id"]}
}, idempotency_key: GoCardlessClient.new_idempotency_key())
```
---
## Pagination
All list endpoints support a lazy `Stream` that transparently fetches pages:
```elixir
# Stream — memory-efficient, fetches as consumed
GoCardlessClient.Resources.Payments.stream(client, %{status: "paid_out"})
|> Stream.filter(&(&1["amount"] > 1000))
|> Stream.each(&reconcile_payment/1)
|> Stream.run()
# Collect all into a list
{:ok, all_customers} = GoCardlessClient.Resources.Customers.collect_all(client)
# Single page with cursor
{:ok, %{items: payments, meta: meta}} =
GoCardlessClient.Resources.Payments.list(client, %{limit: 50, after: cursor})
next_cursor = get_in(meta, ["cursors", "after"])
```
---
## Error Handling
```elixir
case GoCardlessClient.Resources.Payments.create(client, params) do
{:ok, payment} ->
process(payment)
{:error, %GoCardlessClient.APIError{} = err} ->
cond do
GoCardlessClient.APIError.validation_failed?(err) ->
Enum.each(err.errors, fn fe ->
Logger.error("field=#{fe.field} message=#{fe.message}")
end)
GoCardlessClient.APIError.rate_limited?(err) ->
Logger.warning("Rate limited. request_id=#{err.request_id}")
GoCardlessClient.APIError.invalid_state?(err) ->
Logger.warning("Invalid state: #{err.message}")
GoCardlessClient.APIError.not_found?(err) ->
Logger.warning("Resource not found")
end
{:error, %GoCardlessClient.Error{reason: :timeout}} ->
Logger.error("Request timed out")
end
```
---
## Idempotency
```elixir
key = GoCardlessClient.new_idempotency_key()
{:ok, payment} = GoCardlessClient.Resources.Payments.create(client, params,
idempotency_key: key
)
```
---
## Subscriptions
```elixir
{:ok, sub} = GoCardlessClient.Resources.Subscriptions.create(client, %{
amount: 2500,
currency: "GBP",
name: "Premium Monthly",
interval_unit: "monthly",
interval: 1,
day_of_month: 1,
links: %{mandate: mandate_id}
})
# Pause for 2 billing cycles
{:ok, _} = GoCardlessClient.Resources.Subscriptions.pause(client, sub["id"], %{pause_cycles: 2})
# Resume
{:ok, _} = GoCardlessClient.Resources.Subscriptions.resume(client, sub["id"])
# Cancel
{:ok, _} = GoCardlessClient.Resources.Subscriptions.cancel(client, sub["id"])
```
---
## Billing Requests (Open Banking / Pay by Bank)
```elixir
# One-off instant bank payment
{:ok, br} = GoCardlessClient.Resources.BillingRequests.create(client, %{
payment_request: %{
amount: 5000,
currency: "GBP",
description: "Order #1234"
}
})
{:ok, flow} = GoCardlessClient.Resources.BillingRequestFlows.create(client, %{
redirect_uri: "https://example.com/payment-complete",
links: %{billing_request: br["id"]}
})
# Redirect customer to flow["authorisation_url"]
```
---
## Redirect Flows (Hosted Mandate Setup)
```elixir
session_token = GoCardlessClient.new_idempotency_key()
{:ok, flow} = GoCardlessClient.Resources.RedirectFlows.create(client, %{
description: "Set up your Direct Debit",
session_token: session_token,
success_redirect_url: "https://example.com/mandate-confirmed",
scheme: "bacs"
})
# Redirect customer to flow["redirect_url"]
# On return:
{:ok, completed} = GoCardlessClient.Resources.RedirectFlows.complete(client,
flow["id"],
session_token
)
mandate_id = get_in(completed, ["links", "mandate"])
```
---
## Webhooks
### Verification
```elixir
secret = System.get_env("GOCARDLESS_WEBHOOK_SECRET")
case GoCardlessClient.Webhooks.parse(raw_body, signature, secret) do
{:ok, events} ->
Enum.each(events, &handle_event/1)
{:error, :invalid_signature} ->
Logger.warning("Invalid webhook signature")
{:error, :empty_payload} ->
Logger.warning("Empty webhook payload")
end
```
### Phoenix Plug (recommended)
In `endpoint.ex`:
```elixir
plug Plug.Parsers,
parsers: [:json],
json_decoder: Jason,
body_reader: {GoCardlessClient.Webhooks.Plug, :read_body, []}
```
In `router.ex`:
```elixir
pipeline :gocardless_webhooks do
plug GoCardlessClient.Webhooks.Plug, secret: System.get_env("GOCARDLESS_WEBHOOK_SECRET")
end
scope "/webhooks" do
pipe_through :gocardless_webhooks
post "/gocardless", MyApp.WebhookController, :handle
end
```
In your controller:
```elixir
def handle(conn, _params) do
events = conn.private[:gocardless_events]
Enum.each(events, fn event ->
case {event["resource_type"], event["action"]} do
{"payments", "paid_out"} -> handle_payment_paid_out(event)
{"mandates", "active"} -> handle_mandate_active(event)
{"billing_requests", "fulfilled"} -> handle_br_fulfilled(event)
{"subscriptions", "cancelled"} -> handle_sub_cancelled(event)
_ -> :ok
end
end)
send_resp(conn, 200, "")
end
```
---
## OAuth2 (Partner Platforms)
```elixir
config = %{
client_id: System.get_env("GC_CLIENT_ID"),
client_secret: System.get_env("GC_CLIENT_SECRET"),
redirect_uri: "https://yourapp.com/oauth/callback",
environment: :live
}
# Step 1: redirect merchant
auth_url = GoCardlessClient.OAuth.authorise_url(config, scope: "read_write", state: csrf)
redirect(conn, external: auth_url)
# Step 2: exchange code
{:ok, token} = GoCardlessClient.OAuth.exchange_code(config, params["code"])
# Step 3: use merchant token
client = GoCardlessClient.Client.with_token(client, token["access_token"])
# Lookup organisation
{:ok, info} = GoCardlessClient.OAuth.lookup_token(config, token["access_token"])
# Revoke
:ok = GoCardlessClient.OAuth.disconnect(config, token["access_token"])
```
---
## Outbound Payments (Request Signing)
```elixir
signer = GoCardlessClient.Signing.new!(
key_id: System.get_env("GC_SIGNING_KEY_ID"),
pem: File.read!("private_key.pem"),
algorithm: :ecdsa
)
{:ok, payment} = GoCardlessClient.Resources.OutboundPayments.create(client, %{
amount: 50000,
currency: "GBP",
description: "Supplier payment",
links: %{creditor: creditor_id},
recipient_bank_account: %{
account_holder_name: "Acme Ltd",
account_number: "12345678",
branch_code: "204514"
}
}, signer: signer, idempotency_key: GoCardlessClient.new_idempotency_key())
```
---
## Scenario Simulators (Sandbox Only)
```elixir
# Trigger events for testing
{:ok, _} = GoCardlessClient.Resources.ScenarioSimulators.run(client, "payment_paid_out", "PM123")
{:ok, _} = GoCardlessClient.Resources.ScenarioSimulators.run(client, "mandate_activated", "MD456")
{:ok, _} = GoCardlessClient.Resources.ScenarioSimulators.run(client, "billing_request_fulfilled", "BRQ789")
```
---
## Rate Limit State
```elixir
state = GoCardlessClient.rate_limit_state(client)
# => %{limit: 1000, remaining: 950, reset_at: ~U[2024-01-15 10:30:00Z]}
```
---
## Telemetry
The HTTP client emits Telemetry events you can attach to for metrics and tracing:
```elixir
:telemetry.attach_many("gocardless-metrics", [
[:gocardless, :request, :start],
[:gocardless, :request, :stop],
[:gocardless, :request, :exception]
], &MyApp.Telemetry.handle_event/4, nil)
```
Each event carries `%{method: method, url: url, attempt: n}` in its metadata,
and `:stop` adds `%{status: status_code}`.
---
## Running Tests
```bash
mix deps.get
mix test
mix test --cover
mix credo --strict
mix dialyzer
```
---
## License
MIT — see [LICENSE](LICENSE).