Production-ready Elixir client for the GoCardless API. Complete coverage of all 139 endpoints across 46 resource modules — payments, mandates, billing requests, subscriptions, outbound payments, webhooks, OAuth2, and more.
| Capability | Detail |
|---|---|
| Complete API coverage | All 139 GoCardless API endpoints across 46 resource modules |
| Billing Requests | Full Open Banking flow — mandate setup, instant payments, fallback to DD |
| Outbound Payments | Send money with ECDSA P-256 / RSA request signing |
| OAuth2 | Partner platform auth URL, token exchange, lookup, disconnect |
| Resilience | Exponential backoff + full jitter, honours Retry-After header |
| Pagination | Lazy Stream and eager collect_all — zero memory pressure on large datasets |
| Webhooks | HMAC-SHA256 constant-time verification, Phoenix Plug middleware, IP allowlist |
| Telemetry | [:gocardless, :request, :start/stop/exception] events |
| Rate limits | X-RateLimit-* header tracking, accessible at runtime |
| Config | NimbleOptions-validated schema — catches misconfiguration at startup |
| OTP | Finch connection pools, supervised under GoCardlessClient.Supervisor |
# mix.exs
def deps do
[{:gocardless_client, "~> 2.0"}]
end# config/config.exs
config :gocardless_client,
access_token: System.get_env("GOCARDLESS_ACCESS_TOKEN"),
environment: :sandbox, # or :live
timeout: 30_000,
max_retries: 3# Build a client at runtime (overrides application config)
client = GoCardlessClient.client!(access_token: token, environment: :live)alias GoCardlessClient.Resources.{
CustomerBankAccounts,
Customers,
Mandates,
Payments
}
client = GoCardlessClient.client!()
# 1. Create a customer
{:ok, customer} = Customers.create(client, %{
email: "alice@example.com",
given_name: "Alice",
family_name: "Smith",
country_code: "GB"
})
# 2. Add their bank account
{:ok, bank_account} = CustomerBankAccounts.create(client, %{
account_holder_name: "Alice Smith",
account_number: "55779911",
branch_code: "200000",
country_code: "GB",
links: %{customer: customer["id"]}
})
# 3. Create a mandate
{:ok, mandate} = Mandates.create(client, %{
scheme: "bacs",
links: %{customer_bank_account: bank_account["id"]}
})
# 4. Charge the customer
{:ok, payment} = Payments.create(client, %{
amount: 1500,
currency: "GBP",
description: "Monthly subscription",
links: %{mandate: mandate["id"]}
}, idempotency_key: GoCardlessClient.new_idempotency_key())The modern flow for collecting both mandates and instant bank payments, with built-in Open Banking support and fallback to Direct Debit.
alias GoCardlessClient.Resources.{BillingRequestFlows, BillingRequests}
# Mandate + optional instant payment in one flow
{:ok, br} = BillingRequests.create(client, %{
mandate_request: %{currency: "GBP", scheme: "bacs"},
payment_request: %{amount: 5000, currency: "GBP", description: "Setup fee"}
})
{:ok, flow} = BillingRequestFlows.create(client, %{
redirect_uri: "https://myapp.com/complete",
exit_uri: "https://myapp.com/cancel",
links: %{billing_request: br["id"]}
})
# Redirect customer to flow["authorisation_url"]alias GoCardlessClient.Resources.BillingRequestWithActions
{:ok, result} = BillingRequestWithActions.create(client, %{
mandate_request: %{currency: "GBP"},
actions: [
%{
type: "collect_customer_details",
collect_customer_details: %{
customer: %{given_name: "Alice", family_name: "Smith", email: "alice@example.com"},
customer_billing_detail: %{address_line1: "1 Example St", city: "London",
postal_code: "EC1A 1BB", country_code: "GB"}
}
},
%{
type: "collect_bank_account",
collect_bank_account: %{
account_holder_name: "Alice Smith",
account_number: "55779911",
branch_code: "200000",
country_code: "GB"
}
},
%{type: "confirm_payer_details", confirm_payer_details: %{}},
%{type: "fulfil", fulfil: %{}}
]
})alias GoCardlessClient.Resources.Subscriptions
{:ok, sub} = Subscriptions.create(client, %{
amount: 2500,
currency: "GBP",
name: "Premium Monthly",
interval_unit: "monthly",
interval: 1,
day_of_month: 1,
links: %{mandate: mandate_id}
})
{:ok, _} = Subscriptions.pause(client, sub["id"], %{pause_cycles: 2})
{:ok, _} = Subscriptions.resume(client, sub["id"])
{:ok, _} = Subscriptions.cancel(client, sub["id"])alias GoCardlessClient.Resources.InstalmentSchedules
# Explicit dates
{:ok, schedule} = InstalmentSchedules.create_with_dates(client, %{
name: "3-month plan",
currency: "GBP",
instalments: [
%{charge_date: "2025-02-01", amount: 5000},
%{charge_date: "2025-03-01", amount: 5000},
%{charge_date: "2025-04-01", amount: 5000}
],
links: %{mandate: mandate_id}
})
# Interval-based
{:ok, schedule} = InstalmentSchedules.create_with_schedule(client, %{
name: "6-month plan",
currency: "GBP",
amount: 3000,
start_date: "2025-02-01",
count: 6,
interval_unit: "monthly",
interval: 1,
links: %{mandate: mandate_id}
})Requires an ECDSA P-256 private key registered in your GoCardless dashboard.
alias GoCardlessClient.Resources.OutboundPayments
alias GoCardlessClient.Signing
signer = Signing.new!(
key_id: System.get_env("GC_SIGNING_KEY_ID"),
pem: File.read!("private_key.pem"),
algorithm: :ecdsa
)
# Send to a recipient
{:ok, payment} = OutboundPayments.create(client, %{
amount: 50_000,
currency: "GBP",
description: "Supplier invoice #1234",
links: %{payment_account: "PA123", creditor: "CR456"},
recipient_bank_account: %{
account_holder_name: "Acme Ltd",
account_number: "12345678",
branch_code: "204514",
country_code: "GB"
}
}, signer: signer, idempotency_key: GoCardlessClient.new_idempotency_key())
# Withdraw funds to your own bank account
{:ok, _} = OutboundPayments.withdrawal(client, %{
amount: 100_000,
currency: "GBP",
links: %{payment_account: "PA123", creditor_bank_account: "BA456"}
}, signer: signer, idempotency_key: GoCardlessClient.new_idempotency_key())
# Check available funds first
{:ok, avail} = GoCardlessClient.Resources.FundsAvailabilities.check(client, %{
amount: 50_000, currency: "GBP"
})All list endpoints support lazy streaming and eager collection:
alias GoCardlessClient.Resources.Payments
# Lazy stream — fetches pages on demand, no memory pressure
Payments.stream(client, %{status: "paid_out"})
|> Stream.filter(&(&1["amount"] > 1000))
|> Stream.each(&reconcile/1)
|> Stream.run()
# Collect all pages eagerly
{:ok, all_payments} = Payments.collect_all(client, %{mandate: mandate_id})
# Single page with cursor
{:ok, %{items: payments, meta: meta}} = Payments.list(client, %{limit: 50, after: cursor})
next_cursor = get_in(meta, ["cursors", "after"])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, &Logger.error("#{&1.field}: #{&1.message}"))
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("Not found")
GoCardlessClient.APIError.server_error?(err) ->
Logger.error("GoCardless internal error — request_id: #{err.request_id}")
end
{:error, %GoCardlessClient.Error{reason: :timeout}} ->
Logger.error("Request timed out")
endAdd to endpoint.ex before Plug.Parsers:
plug Plug.Parsers,
parsers: [:json],
json_decoder: Jason,
body_reader: {GoCardlessClient.Webhooks.Plug, :read_body, []}Add to router.ex:
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
endIn your controller:
def handle(conn, _params) do
conn.private[:gocardless_events]
|> Enum.each(&dispatch/1)
send_resp(conn, 200, "")
end
defp dispatch(%{"resource_type" => "payments", "action" => "paid_out"} = event),
do: Reconciler.payment_paid_out(event)
defp dispatch(%{"resource_type" => "mandates", "action" => "active"} = event),
do: MandateHandler.activated(event)
defp dispatch(%{"resource_type" => "billing_requests", "action" => "fulfilled"} = event),
do: OnboardingFlow.complete(event)
defp dispatch(_event), do: :oksecret = System.get_env("GOCARDLESS_WEBHOOK_SECRET")
case GoCardlessClient.Webhooks.parse(raw_body, signature_header, secret) do
{:ok, events} -> Enum.each(events, &dispatch/1)
{:error, :invalid_signature} -> Logger.warning("Forged webhook rejected")
{:error, :payload_too_large} -> Logger.warning("Oversized payload rejected")
{:error, :invalid_json} -> Logger.warning("Malformed payload")
endalias GoCardlessClient.Webhooks
Webhooks.payment_event?(event) # resource_type == "payments"
Webhooks.mandate_event?(event) # resource_type == "mandates"
Webhooks.subscription_event?(event) # resource_type == "subscriptions"
Webhooks.billing_request_event?(event) # resource_type == "billing_requests"
Webhooks.payout_event?(event) # resource_type == "payouts"
Webhooks.refund_event?(event) # resource_type == "refunds"
Webhooks.outbound_payment_event?(event) # resource_type == "outbound_payments"
Webhooks.instalment_schedule_event?(event) # resource_type == "instalment_schedules"
Webhooks.creditor_event?(event) # resource_type == "creditors"
Webhooks.customer_event?(event) # resource_type == "customers"
Webhooks.export_event?(event) # resource_type == "exports"
Webhooks.scheme_identifier_event?(event) # resource_type == "scheme_identifiers"
Webhooks.payment_account_transaction_event?(event)# resource_type == "payment_account_transactions"
Webhooks.action?(event, "paid_out") # action == "paid_out"alias GoCardlessClient.OAuth
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
}
# 1. Redirect merchant to GoCardless
auth_url = OAuth.authorise_url(config, scope: "read_write", state: csrf_token)
# 2. Exchange code for token
{:ok, token} = OAuth.exchange_code(config, params["code"])
# 3. Build a merchant-scoped client
merchant_client = GoCardlessClient.client!(
access_token: token["access_token"],
environment: :live
)
# Lookup organisation details
{:ok, info} = OAuth.lookup_token(config, token["access_token"])
# Revoke
:ok = OAuth.disconnect(config, token["access_token"])Bulk-import mandates from another payment provider:
alias GoCardlessClient.Resources.{MandateImportEntries, MandateImports}
{:ok, import} = MandateImports.create(client, %{scheme: "bacs"})
{:ok, _} = MandateImportEntries.add(client, %{
record_identifier: "CUST-001",
amendment: %{
original_creditor_id: "OLD-CR-001",
original_creditor_name: "Old Provider Ltd",
original_mandate_reference: "OLD-REF-001"
},
customer: %{given_name: "Alice", family_name: "Smith", email: "alice@example.com",
address_line1: "1 Example St", city: "London",
postal_code: "EC1A 1BB", country_code: "GB"},
bank_account: %{account_holder_name: "Alice Smith",
sort_code: "200000", account_number: "55779911"},
links: %{mandate_import: import["id"]}
})
{:ok, _} = MandateImports.submit(client, import["id"])alias GoCardlessClient.Resources.{Events, PayoutItems, Payouts}
{:ok, %{items: payouts}} = Payouts.list(client, %{status: "paid"})
# Get line items for a specific payout
{:ok, %{items: items}} = PayoutItems.list(client, %{payout: "PO123"})
fees = Enum.filter(items, &(&1["type"] == "gocardless_fee"))
payments = Enum.filter(items, &(&1["type"] == "payment_paid_out"))
chargebacks = Enum.filter(items, &(&1["type"] == "payment_charged_back"))
# Get the event log for the payout
{:ok, %{items: events}} = Events.list(client, %{payout: "PO123"})Trigger payment lifecycle events without real bank interactions:
alias GoCardlessClient.Resources.ScenarioSimulators
# Payment scenarios
{:ok, _} = ScenarioSimulators.run(client, "payment_paid_out", %{links: %{payment: "PM123"}})
{:ok, _} = ScenarioSimulators.run(client, "payment_failed", %{links: %{payment: "PM123"}})
{:ok, _} = ScenarioSimulators.run(client, "payment_charged_back", %{links: %{payment: "PM123"}})
# Mandate scenarios
{:ok, _} = ScenarioSimulators.run(client, "mandate_activated", %{links: %{mandate: "MD456"}})
{:ok, _} = ScenarioSimulators.run(client, "mandate_failed", %{links: %{mandate: "MD456"}})
# Billing request scenarios
{:ok, _} = ScenarioSimulators.run(client, "billing_request_fulfilled",
%{links: %{billing_request: "BRQ789"}})
# List all available scenario types
ScenarioSimulators.valid_scenarios():telemetry.attach_many("gocardless-metrics", [
[:gocardless, :request, :start],
[:gocardless, :request, :stop],
[:gocardless, :request, :exception]
], &MyApp.Telemetry.handle_event/4, nil)
# Metadata on :stop event:
# %{method: :post, url: "https://api.gocardless.com/payments",
# attempt: 1, status: 201, duration: 143}state = GoCardlessClient.rate_limit_state(client)
# => %{limit: 1000, remaining: 950, reset_at: ~U[2025-01-15 10:30:00Z]}| Module | Endpoints | Key operations |
|---|---|---|
BankAuthorisations |
2 | create, get |
BillingRequests |
14 | create, get, list, update + 9 actions |
BillingRequestFlows |
2 | create, initialise |
BillingRequestTemplates |
4 | create, get, list, update |
BillingRequestWithActions |
1 | create |
Institutions |
2 | list, list_for_billing_request |
Balances |
1 | list |
BankAccountDetails |
1 | get |
BankAccountHolderVerifications |
2 | create, get |
BankDetailsLookups |
1 | lookup |
Blocks |
6 | create, get, list, disable, enable, block_by_reference |
Creditors |
4 | create, get, list, update |
CreditorBankAccounts |
5 | create, get, list, update, disable |
CurrencyExchangeRates |
1 | list |
Customers |
5 | create, get, list, update, remove (GDPR) |
CustomerBankAccounts |
5 | create, get, list, update, disable |
CustomerNotifications |
1 | handle |
Events |
2 | get, list |
Exports |
2 | get, list |
FundsAvailabilities |
1 | check |
InstalmentSchedules |
6 | create_with_dates, create_with_schedule, get, list, update, cancel |
Logos |
1 | create |
Mandates |
6 | create, get, list, update, cancel, reinstate |
MandateImports |
4 | create, get, submit, cancel |
MandateImportEntries |
2 | add, list |
MandatePDFs |
1 | create |
NegativeBalanceLimits |
1 | list |
OutboundPayments |
8 | create, withdrawal, get, list, update, cancel, approve, statistics |
OutboundPaymentImports |
3 | create, get, list |
OutboundPaymentImportEntries |
1 | list |
PayerAuthorisations |
6 | create, get, list, update, submit, confirm |
PayerThemes |
1 | create |
Payments |
6 | create, get, list, update, cancel, retry |
PaymentAccounts |
2 | get, list |
PaymentAccountTransactions |
2 | get, list |
Payouts |
3 | get, list, update |
PayoutItems |
1 | list |
RedirectFlows |
4 | create, get, list, complete |
Refunds |
4 | create, get, list, update |
ScenarioSimulators |
1 | run (19 scenario types) |
SchemeIdentifiers |
4 | create, get, list, update |
Subscriptions |
7 | create, get, list, update, pause, resume, cancel |
TaxRates |
2 | get, list |
TransferredMandates |
1 | get |
VerificationDetails |
3 | create, get, list |
Webhooks (resource) |
3 | get, list, retry |
All list endpoints also expose stream/3 (lazy Stream) and collect_all/3 (eager list).
mix deps.get
mix test
mix test --cover
mix credo --strict
mix dialyzerMIT — see LICENSE.