YapilyClient

Hex.pmHex DocsAPI v12

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}"}
end

Consent 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
end

Testing

# 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.Institutionslist/1, get/2 Supported banks
YapilyClient.Accountslist/2, get/3 Account detail
YapilyClient.Transactionslist/4, list_all/4, stream/4, list_real_time/3 Transaction history
YapilyClient.Paymentscreate/3, get/3 All 6 payment types
YapilyClient.BulkPaymentscreate/3, get_status/3 Batch payments
YapilyClient.Consentslist/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.Userslist/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.Validateget_identity/2, validate_ownership/4 Ownership verification
YapilyClient.ConsentPollerwait_for_authorisation/2,3 Fibonacci polling
YapilyClient.Webhookverify/3, valid?/3 Signature verification

License

MIT