Packages

Unofficial Elixir client for the Yapily Open Banking API v12 — payments, data, VRP, hosted pages, enrichment, and more.

Current section

Files

Jump to
yapily_client lib yapily_client.ex
Raw

lib/yapily_client.ex

defmodule YapilyClient do
@moduledoc """
Official Elixir client for the Yapily Open Banking API v12.
Connect to 2,000+ banks across the UK and Europe through a single integration.
## Installation
```elixir
# mix.exs
{:yapily_client, "~> 1.0"}
```
Set credentials at runtime (never hard-code them):
```elixir
# config/runtime.exs
config :yapily_client,
app_key: System.fetch_env!("YAPILY_APP_KEY"),
app_secret: System.fetch_env!("YAPILY_APP_SECRET")
```
## Quick start
config = YapilyClient.Config.new!(
app_key: System.fetch_env!("YAPILY_APP_KEY"),
app_secret: System.fetch_env!("YAPILY_APP_SECRET")
)
# List banks
{:ok, institutions} = YapilyClient.Institutions.list(config)
# Start account authorisation
{:ok, auth} = YapilyClient.Authorisations.create_account(config, %{
institution_id: "monzo",
application_user_id: "user-123",
callback: "https://myapp.com/callback",
feature_scope_list: ["ACCOUNTS", "TRANSACTIONS"],
one_time_token: true
})
# Redirect user to: auth.authorisation_url
# Exchange token after callback
{:ok, consent} = YapilyClient.Consents.exchange_one_time_token(config, token)
# Read accounts
{:ok, accounts} = YapilyClient.Accounts.list(config, consent.id)
# Stream transactions
YapilyClient.Transactions.stream(config, consent.id, account_id)
|> Stream.filter(&(&1.currency == "GBP"))
|> Enum.take(100)
# Make a payment
{:ok, payment} = YapilyClient.Payments.create(config, consent_token, %{
type: YapilyClient.payment_type(:domestic),
payment_idempotency_id: YapilyClient.idempotency_key(),
amount: 100.00,
currency: "GBP",
recipient: %{
name: "Jane Smith",
account_identifications: [
%{type: "SORT_CODE", identification: "200000"},
%{type: "ACCOUNT_NUMBER", identification: "55779911"}
]
},
reference: "Invoice-001"
})
## Services
| Module | Description |
|--------|-------------|
| `YapilyClient.Institutions` | List and inspect supported banks |
| `YapilyClient.Accounts` | Account list and detail |
| `YapilyClient.Transactions` | History — `list/4`, `list_all/4`, `stream/4` |
| `YapilyClient.Payments` | Domestic, scheduled, periodic, international |
| `YapilyClient.BulkPayments` | Batch payment initiation |
| `YapilyClient.Consents` | Lifecycle — exchange, extend, delete |
| `YapilyClient.Authorisations` | 14 flows — redirect, embedded, pre-auth |
| `YapilyClient.FinancialData` | Balances, direct debits, statements, identity |
| `YapilyClient.Users` | PSU management |
| `YapilyClient.VRP` | Variable Recurring Payments |
| `YapilyClient.Notifications` | Event subscriptions |
| `YapilyClient.DataPlus` | Transaction enrichment |
| `YapilyClient.HostedPages` | Yapily-hosted UIs |
| `YapilyClient.Constraints` | Per-institution rules |
| `YapilyClient.ApplicationManagement` | App and sub-app management |
| `YapilyClient.Webhooks` | Registration and secret rotation |
| `YapilyClient.Beneficiaries` | App + user beneficiaries (VoP) |
| `YapilyClient.Validate` | Account ownership verification |
| `YapilyClient.ConsentPoller` | Fibonacci back-off polling |
| `YapilyClient.Webhook` | HMAC-SHA256 signature verification |
## Error handling
case YapilyClient.Accounts.list(config, token) do
{:ok, accounts} ->
accounts
{:error, err} when YapilyClient.Error.not_found?(err) ->
handle_not_found()
{:error, err} when YapilyClient.Error.vop_rejected?(err) ->
handle_vop_failure()
{:error, %YapilyClient.Error.APIError{status: s, trace_id: t}} ->
Logger.error("API \#{s} trace=\#{t}")
{:error, %YapilyClient.Error.EnhancedAPIError{issues: issues}} ->
Enum.each(issues, &Logger.error("[code \#{&1.code}] \#{&1.message}"))
{:error, %YapilyClient.Error.ValidationError{field: f, message: m}} ->
{:error, "\#{f}: \#{m}"}
end
## Consent polling
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
## Webhook verification
case YapilyClient.Webhook.verify(raw_body, secret, signature_header) do
:ok -> process_event(event)
{:error, :invalid_signature} -> send_resp(conn, 401, "unauthorized")
{:error, :missing_signature} -> send_resp(conn, 400, "missing signature")
end
"""
@version "1.0.0"
@doc "Returns the SDK version."
@spec version() :: String.t()
def version, do: @version
@doc "Returns the Yapily API version this SDK targets."
@spec api_version() :: String.t()
def api_version, do: "12.0.0"
@doc """
Returns `key` if non-empty, otherwise generates a unique idempotency key.
Always provide an idempotency key when creating payments to prevent duplicate
submissions on network retry.
## Examples
YapilyClient.idempotency_key("order-123") # => "order-123"
YapilyClient.idempotency_key() # => "idem-<monotonic-int>"
YapilyClient.idempotency_key("") # => "idem-<monotonic-int>"
"""
@spec idempotency_key(String.t()) :: String.t()
def idempotency_key(key \\ "") do
if key != "" and String.trim(key) != "",
do: key,
else: "idem-#{System.unique_integer([:positive, :monotonic])}"
end
@doc """
Returns the Yapily API payment type string for the given atom.
| Atom | String |
|------|--------|
| `:domestic` | `"DOMESTIC_PAYMENT"` |
| `:domestic_scheduled` | `"DOMESTIC_SCHEDULED_PAYMENT"` |
| `:domestic_periodic` | `"DOMESTIC_PERIODIC_PAYMENT"` |
| `:international` | `"INTERNATIONAL_PAYMENT"` |
| `:international_scheduled` | `"INTERNATIONAL_SCHEDULED_PAYMENT"` |
| `:international_periodic` | `"INTERNATIONAL_PERIODIC_PAYMENT"` |
"""
@spec payment_type(
:domestic
| :domestic_scheduled
| :domestic_periodic
| :international
| :international_scheduled
| :international_periodic
) ::
String.t()
def payment_type(:domestic), do: "DOMESTIC_PAYMENT"
def payment_type(:domestic_scheduled), do: "DOMESTIC_SCHEDULED_PAYMENT"
def payment_type(:domestic_periodic), do: "DOMESTIC_PERIODIC_PAYMENT"
def payment_type(:international), do: "INTERNATIONAL_PAYMENT"
def payment_type(:international_scheduled), do: "INTERNATIONAL_SCHEDULED_PAYMENT"
def payment_type(:international_periodic), do: "INTERNATIONAL_PERIODIC_PAYMENT"
@doc """
Returns the Yapily frequency string for a periodic payment.
| Atom | String |
|------|--------|
| `:daily` | `"DAILY"` |
| `:every_working_day` | `"EVERY_WORKING_DAY"` |
| `:calendar_day` | `"CALENDAR_DAY"` |
| `:weekly` | `"WEEKLY"` |
| `:every_two_weeks` | `"EVERY_TWO_WEEKS"` |
| `:monthly` | `"MONTHLY"` |
| `:every_two_months` | `"EVERY_TWO_MONTHS"` |
| `:quarterly` | `"QUARTERLY"` |
| `:semi_annual` | `"SEMIANNUAL"` |
| `:annual` | `"ANNUAL"` |
"""
@spec frequency(
:daily
| :every_working_day
| :calendar_day
| :weekly
| :every_two_weeks
| :monthly
| :every_two_months
| :quarterly
| :semi_annual
| :annual
) ::
String.t()
def frequency(:daily), do: "DAILY"
def frequency(:every_working_day), do: "EVERY_WORKING_DAY"
def frequency(:calendar_day), do: "CALENDAR_DAY"
def frequency(:weekly), do: "WEEKLY"
def frequency(:every_two_weeks), do: "EVERY_TWO_WEEKS"
def frequency(:monthly), do: "MONTHLY"
def frequency(:every_two_months), do: "EVERY_TWO_MONTHS"
def frequency(:quarterly), do: "QUARTERLY"
def frequency(:semi_annual), do: "SEMIANNUAL"
def frequency(:annual), do: "ANNUAL"
@doc "Returns the Yapily consent status string for the given atom."
@spec consent_status(:awaiting | :authorized | :rejected | :revoked | :failed | :expired) ::
String.t()
def consent_status(:awaiting), do: "AWAITING_AUTHORIZATION"
def consent_status(:authorized), do: "AUTHORIZED"
def consent_status(:rejected), do: "REJECTED"
def consent_status(:revoked), do: "REVOKED"
def consent_status(:failed), do: "FAILED"
def consent_status(:expired), do: "EXPIRED"
end