# YapilyClient
[](https://hex.pm/packages/yapily_client)
[](https://hexdocs.pm/yapily_client)
[](https://docs.yapily.com)
Unofficial Elixir client for the [Yapily Open Banking API v12](https://docs.yapily.com).
Connect to **2,000+ banks** across the UK and Europe.
## Installation
```elixir
# mix.exs
{:yapily_client, "~> 1.0.0"}
```
## Configuration
```elixir
# 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:
```elixir
config = YapilyClient.Config.new!(
app_key: System.fetch_env!("YAPILY_APP_KEY"),
app_secret: System.fetch_env!("YAPILY_APP_SECRET")
)
```
## Quick start
```elixir
# 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
```elixir
{: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
```elixir
{: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)
```elixir
{: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)
```elixir
# 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
```elixir
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}"}
end
```
## Consent polling (Fibonacci back-off)
```elixir
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
```elixir
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
end
```
## Testing
```elixir
# 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"
end
```
## Service 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