YapilyClient
Unofficial Elixir client for the Yapily Open Banking API v12.
Connect to 2,000+ banks across the UK and Europe.
Installation
# mix.exs
{:yapily_client, "~> 1.0.0"}Configuration
# config/runtime.exs — read credentials at runtime, never hard-code
config :yapily_client,
app_key: System.fetch_env!("YAPILY_APP_KEY"),
app_secret: System.fetch_env!("YAPILY_APP_SECRET")Or build a config struct directly:
config = YapilyClient.Config.new!(
app_key: System.fetch_env!("YAPILY_APP_KEY"),
app_secret: System.fetch_env!("YAPILY_APP_SECRET")
)Quick start
# 1. List supported banks
{:ok, institutions} = YapilyClient.Institutions.list(config)
# 2. Start an account authorisation (redirect flow)
{:ok, auth} = YapilyClient.Authorisations.create_account(config, %{
institution_id: "monzo",
application_user_id: "your-internal-user-id",
callback: "https://yourapp.com/callback",
feature_scope_list: ["ACCOUNTS", "TRANSACTIONS"],
one_time_token: true
})
# Redirect the user to:
auth.authorisation_url
# 3. Exchange the token from your callback URL
{:ok, consent} = YapilyClient.Consents.exchange_one_time_token(config, token)
# 4. Read accounts
{:ok, accounts} = YapilyClient.Accounts.list(config, consent.id)
# 5. Stream transactions lazily
YapilyClient.Transactions.stream(config, consent.id, account_id)
|> Stream.filter(&(&1.currency == "GBP"))
|> Enum.take(100)Payments
Domestic
{:ok, payment} = YapilyClient.Payments.create(config, consent_token, %{
type: YapilyClient.payment_type(:domestic),
payment_idempotency_id: YapilyClient.idempotency_key("invoice-001"),
amount: 100.00,
currency: "GBP",
recipient: %{
name: "Jane Smith",
account_identifications: [
%{type: "SORT_CODE", identification: "200000"},
%{type: "ACCOUNT_NUMBER", identification: "55779911"}
]
},
reference: "Invoice-001"
})International
{:ok, _} = YapilyClient.Payments.create(config, consent_token, %{
type: YapilyClient.payment_type(:international),
payment_idempotency_id: YapilyClient.idempotency_key(),
amount: 500.00,
currency: "GBP",
recipient: %{
name: "Maria Müller",
address: %{country: "DE"},
account_identifications: [
%{type: "IBAN", identification: "DE89370400440532013000"},
%{type: "BIC", identification: "COBADEFFXXX"}
]
},
international_payment: %{
currency_of_transfer: "EUR",
charge_bearer: "DEBT", # DEBT | CRED | SHAR | FOLLOWING
priority: "NORMAL", # NORMAL | URGENT
purpose: "GDDS" # ISO 20022 purpose code
}
})Periodic (standing order)
{:ok, _} = YapilyClient.Payments.create(config, consent_token, %{
type: YapilyClient.payment_type(:domestic_periodic),
payment_idempotency_id: YapilyClient.idempotency_key(),
payment_date_time: "2025-01-01T09:00:00Z",
amount: 1_200.00,
currency: "GBP",
recipient: %{name: "Landlord", account_identifications: [...]},
periodic_payment: %{
frequency: YapilyClient.frequency(:monthly),
execution_day: 1,
interval_month: 1,
number_of_payments: 12 # or: final_payment_date_time: "2025-12-01T09:00:00Z"
}
})Variable Recurring Payments (VRP)
# 1. Authorise once
{:ok, vrp} = YapilyClient.VRP.create_sweeping_authorisation(config, %{
institution_id: "monzo",
application_user_id: "user-id",
callback: "https://yourapp.com/vrp-callback",
control_parameters: %{
currency: "GBP",
maximum_individual_amount: 500.00,
periodic_limits: [
%{maximum_amount: 2_000.00, currency: "GBP",
period_type: "Month", period_alignment: "Calendar"}
]
}
})
# Redirect user to: vrp.authorisation_url
# 2. Sweep repeatedly — no re-auth
{:ok, payment} = YapilyClient.VRP.create_payment(config, consent_token, vrp.id, %{
amount: 250.00,
currency: "GBP",
recipient: %{name: "Savings", account_identifications: [...]},
reference: "Monthly Sweep"
})Error handling
case YapilyClient.Accounts.list(config, consent_token) do
{:ok, accounts} ->
accounts
{:error, err} when YapilyClient.Error.not_found?(err) ->
handle_not_found()
{:error, err} when YapilyClient.Error.unauthorized?(err) ->
handle_unauthorized()
{:error, err} when YapilyClient.Error.rate_limited?(err) ->
handle_rate_limit()
{:error, err} when YapilyClient.Error.vop_rejected?(err) ->
handle_vop_failure()
{:error, err} when YapilyClient.Error.insufficient_funds?(err) ->
handle_insufficient_funds()
{:error, err} when YapilyClient.Error.retryable?(err) ->
retry_later()
{:error, %YapilyClient.Error.APIError{status: s, code: c, trace_id: t}} ->
Logger.error("API error #{s} #{c} (trace: #{t})")
{:error, %YapilyClient.Error.EnhancedAPIError{issues: issues, tracing_id: t}} ->
Enum.each(issues, &Logger.error("[#{&1.code}] #{&1.type}: #{&1.message}"))
Logger.error("trace: #{t}")
{:error, %YapilyClient.Error.ValidationError{field: f, message: m}} ->
{:error, "#{f}: #{m}"}
endConsent polling (Fibonacci back-off)
case YapilyClient.ConsentPoller.wait_for_authorisation(config, consent_id) do
{:ok, %{status: "AUTHORIZED"} = consent} -> proceed(consent)
{:ok, %{status: status}} -> handle_failure(status)
{:error, :timed_out} -> show_timeout()
end
Delays: 1 s → 1 s → 2 s → 3 s → 5 s → 8 s → 13 s → 21 s → 34 s (~88 s total).
Webhook verification
defmodule MyAppWeb.WebhookController do
use MyAppWeb, :controller
def handle(conn, _params) do
raw_body = conn.assigns.raw_body
signature = get_req_header(conn, "x-yapily-signature") |> List.first()
secret = System.get_env("YAPILY_WEBHOOK_SECRET")
case YapilyClient.Webhook.verify(raw_body, secret, signature) do
:ok ->
process_event(conn.body_params)
send_resp(conn, 200, "ok")
{:error, reason} ->
send_resp(conn, 401, Atom.to_string(reason))
end
end
endTesting
# config/test.exs
config :yapily_client, http_client: YapilyClient.HTTP.MockClient
# test/test_helper.exs
Mox.defmock(YapilyClient.HTTP.MockClient, for: YapilyClient.HTTP.Behaviour)
# In your test
import Mox
test "lists accounts" do
config = YapilyClient.Config.new!(app_key: "k", app_secret: "s")
expect(YapilyClient.HTTP.MockClient, :request, fn _config, :get, "/accounts", _opts ->
{:ok, %{"data" => [%{"id" => "acc-1", "type" => "CURRENT", ...}]}}
end)
assert {:ok, [acc]} = YapilyClient.Accounts.list(config, "consent-token")
assert acc.id == "acc-1"
endService reference
| Module | Methods | Description |
|---|---|---|
YapilyClient.Institutions | list/1, get/2 | Supported banks |
YapilyClient.Accounts | list/2, get/3 | Account detail |
YapilyClient.Transactions | list/4, list_all/4, stream/4, list_real_time/3 | Transaction history |
YapilyClient.Payments | create/3, get/3 | All 6 payment types |
YapilyClient.BulkPayments | create/3, get_status/3 | Batch payments |
YapilyClient.Consents | list/2, get/2, delete/3, extend/3, exchange_oauth2_code/2, exchange_one_time_token/2 | Consent lifecycle |
YapilyClient.Authorisations | 14 functions | All auth flows |
YapilyClient.FinancialData | 10 functions | Balances, statements, identity |
YapilyClient.Users | list/2, create/2, get/2, delete/2, update/3 | PSU management |
YapilyClient.VRP | 5 functions | Variable Recurring Payments |
YapilyClient.Notifications | 4 functions | Event subscriptions |
YapilyClient.DataPlus | 6 functions | Transaction enrichment |
YapilyClient.HostedPages | 11 functions | Yapily-hosted UIs |
YapilyClient.Constraints | 2 functions | Institution constraints |
YapilyClient.ApplicationManagement | 8 functions | App management |
YapilyClient.Webhooks | 5 functions | Webhook management |
YapilyClient.Beneficiaries | 11 functions | VoP flows |
YapilyClient.Validate | get_identity/2, validate_ownership/4 | Ownership verification |
YapilyClient.ConsentPoller | wait_for_authorisation/2,3 | Fibonacci polling |
YapilyClient.Webhook | verify/3, valid?/3 | Signature verification |
License
MIT