Hex.pm Docs License: MIT

Production-grade Elixir client for the Solaris Embedded Finance API.

Covers the full API surface — Onboarding, KYC, Digital Banking, SEPA Transfers, Cards, Lending, and Webhooks — with idiomatic Elixir: typed errors, auto-paginating streams, telemetry, OAuth2 token management, retries, and rate limiting.

Features

  • Full API coverage — Persons, Businesses, KYC, Accounts, SEPA, Cards, Loans, Trade Finance
  • 🔐 OAuth2 token management — Auto-refresh with proactive expiry buffer (via GenServer + :persistent_term)
  • 🔁 Retry with exponential backoff — Automatic retries on 429/5xx with jitter
  • 📊 Telemetry:telemetry events on every request and webhook; compatible with Prometheus/StatsD
  • 🌊 Streaming paginationstream/1 on all list endpoints via Solaris.Pagination
  • 🔑 Idempotency keys — Auto-generated on POST/PUT/PATCH; override per-request
  • 🪝 Webhook support — HMAC-SHA256 verification, event dispatch, Plug integration
  • 🧪 Sandbox helpers — Robot identification, card authorization simulation
  • 📝 Typed errors%Solaris.Error{} with code, status, details, and request ID

Installation

def deps do
  [
    {:solaris, "~> 1.0"}
  ]
end

Configuration

# config/runtime.exs
config :solaris,
  client_id: System.fetch_env!("SOLARIS_CLIENT_ID"),
  client_secret: System.fetch_env!("SOLARIS_CLIENT_SECRET"),
  environment: :sandbox,           # :sandbox | :production
  timeout: 30_000,                 # HTTP timeout in ms
  max_retries: 3,                  # Retry count on transient errors
  webhook_secret: System.get_env("SOLARIS_WEBHOOK_SECRET")

Quick Start

# 1. Onboard a person
{:ok, person} = Solaris.Onboarding.Persons.create(%{
  first_name: "Jane",
  last_name: "Doe",
  email: "jane@example.com",
  birth_date: "1990-01-15",
  nationality: "DE",
  address: %{
    line_1: "Unter den Linden 1",
    postal_code: "10117",
    city: "Berlin",
    country: "DE"
  }
})

# 2. Register and verify mobile number (for 2FA)
{:ok, _} = Solaris.Onboarding.Persons.create_mobile_number(person["id"], "+491701234567")
{:ok, _} = Solaris.Onboarding.Persons.authorize_mobile_number(person["id"])
{:ok, _} = Solaris.Onboarding.Persons.confirm_mobile_number(person["id"], "123456")

# 3. Start KYC
{:ok, identification} = Solaris.Onboarding.KYC.create_person_identification(person["id"], %{
  method: "idnow"
})
# redirect customer to identification["url"]

# 4. After KYC — check account
{:ok, accounts} = Solaris.Banking.Accounts.list_person_accounts(person["id"])
account = List.first(accounts)

# 5. Initiate a SEPA transfer (returns 202 + change_request_id for SCA)
{:ok, result} = Solaris.Banking.SEPA.create_person_credit_transfer(
  person["id"],
  account["id"],
  %{
    recipient_iban: "DE89370400440532013000",
    recipient_name: "Max Mustermann",
    amount: 10_000,
    currency: "EUR",
    reference: "Invoice #123"
  }
)

# 6. Complete the SCA change request via SMS OTP
change_request_id = result["change_request_id"]
{:ok, _} = Solaris.ChangeRequests.authorize_with_sms(change_request_id)
{:ok, _} = Solaris.ChangeRequests.confirm_with_otp(change_request_id, "483920")

Error Handling

All functions return {:ok, result} or {:error, %Solaris.Error{}}.

case Solaris.Banking.Accounts.get_balance(account_id) do
  {:ok, %{"balance" => %{"amount" => amount, "currency" => currency}}} ->
    IO.puts("Balance: #{amount} #{currency}")

  {:error, %Solaris.Error{code: :not_found}} ->
    Logger.warning("Account not found")

  {:error, %Solaris.Error{code: :unauthorized, request_id: req_id}} ->
    Logger.error("Auth failed, request_id: #{req_id}")

  {:error, %Solaris.Error{code: :rate_limited}} ->
    # Automatic retries handle this, but you can also catch it
    :backoff
end

Pagination & Streaming

# Fetch one page
{:ok, page} = Solaris.Onboarding.Persons.list(per_page: 50)

# Stream ALL persons across all pages (lazy)
Solaris.Onboarding.Persons.stream()
|> Stream.filter(fn p -> p["status"] == "ACTIVE" end)
|> Stream.each(fn p -> process(p) end)
|> Stream.run()

# Fetch all into memory (use with care for large datasets)
{:ok, all_persons} = Solaris.Pagination.all(fn cursor ->
  Solaris.Onboarding.Persons.list(after: cursor)
  |> case do
    {:ok, page} -> {:ok, Solaris.Pagination.from_response(page)}
    err -> err
  end
end)

Webhooks

Phoenix Integration

# endpoint.ex — preserve raw body for signature verification
plug Plug.Parsers,
  parsers: [:json],
  json_decoder: Jason,
  body_reader: {Solaris.Webhooks.Plug.BodyReader, :read_body, []}

# router.ex
post "/webhooks/solaris", Solaris.Webhooks.Plug,
  handler: MyApp.SolarisHandler,
  secret: System.get_env("SOLARIS_WEBHOOK_SECRET")
# my_app/solaris_handler.ex
defmodule MyApp.SolarisHandler do
  @behaviour Solaris.Webhooks.Handler

  @impl true
  def handle_event("BOOKING", %{"booking" => booking}, _event) do
    MyApp.Ledger.record(booking)
    :ok
  end

  def handle_event("IDENTIFICATION", payload, _event) do
    case payload["identification"]["status"] do
      "successful" -> MyApp.KYC.complete(payload["person_id"])
      "failed"     -> MyApp.KYC.reject(payload["person_id"])
      _ -> :ok
    end
  end

  # Always add a catch-all for forward compatibility
  def handle_event(_type, _payload, _event), do: :ok
end

Manual Verification

raw_body = get_raw_body(conn)
signature = get_req_header(conn, "solaris-webhook-signature")

case Solaris.Webhooks.verify_and_parse(raw_body, signature, webhook_secret) do
  {:ok, event} ->
    Solaris.Webhooks.dispatch(event, MyApp.SolarisHandler)
    send_resp(conn, 200, "ok")

  {:error, :invalid_signature} ->
    send_resp(conn, 401, "unauthorized")
end

Telemetry

# Attach the default logger (for development)
Solaris.Telemetry.attach_default_logger(:debug)

# Use with telemetry_metrics
defmodule MyApp.Metrics do
  def metrics do
    Solaris.Telemetry.metrics()
    # Returns distribution/counter metrics for all Solaris events
  end
end

Events emitted:

  • [:solaris, :request, :start]
  • [:solaris, :request, :stop] — includes method, path, status, duration
  • [:solaris, :webhook, :received] — includes event_type, delivery_id
  • [:solaris, :rate_limit, :hit]

Consumer Loan Flow

# 1. Create application
{:ok, app} = Solaris.Lending.ConsumerLoans.create_application(person_id, %{
  amount: 10_000_00, currency: "EUR", term_months: 36, purpose: "CONSUMER_GOODS"
})

# 2. Wait for CONSUMER_LOAN_APPLICATION webhook with status "OFFERED"

# 3. Download SECCI (legally required before signing)
{:ok, pdf} = Solaris.Lending.ConsumerLoans.get_secci(person_id, app["id"], offer_id)
present_to_customer(pdf)

# 4. Get final contract
{:ok, contract_pdf} = Solaris.Lending.ConsumerLoans.get_contract(person_id, app["id"], offer_id)

# 5. Create loan (after customer signs)
{:ok, loan} = Solaris.Lending.ConsumerLoans.create_loan(person_id, app["id"], %{
  offer_id: offer_id, signing_id: signing_id
})

Sandbox Testing

# Simulate card authorization (3DS flow)
Solaris.Cards.sandbox_simulate_authorization(card_id, %{amount: 5000, currency: "EUR"})

# Robot-based KYC identification
Solaris.Onboarding.KYC.sandbox_identify_with_robot("AUTOTEST-APPROVED")

# Simulate expired ID document
Solaris.Onboarding.Persons.simulate_id_document_expiry(person_id)

# Set cash operation status
Solaris.Banking.Transactions.sandbox_set_cash_operation_status(account_id, op_id, "PAID")

Module Reference

ModuleDescription
Solaris.Onboarding.PersonsPerson CRUD, mobile numbers, tax IDs, documents
Solaris.Onboarding.BusinessesBusiness CRUD, legal reps, beneficial owners
Solaris.Onboarding.KYCIdentification sessions, signings, screener hits
Solaris.Banking.AccountsAccounts, balances, bookings, savings, IBANs
Solaris.Banking.SEPASCT, Instant SCT, SDD mandates
Solaris.Banking.TransactionsCash ops, top-ups, remittances, payouts
Solaris.CardsIssuance, lifecycle, tokenization, 3DS
Solaris.Lending.LoansLoan servicing, repayment, dunning
Solaris.Lending.ConsumerLoansLoan applications, offers, SECCI
Solaris.Lending.OverdraftOverdraft facilities
Solaris.Lending.SplitpayBNPL / installment plans
Solaris.Lending.TradeFinanceBusiness trade credit lines
Solaris.ChangeRequestsSCA/2FA completion flow
Solaris.WebhooksSignature verification, dispatch
Solaris.Webhooks.PlugPhoenix/Plug webhook endpoint
Solaris.PaginationCursor pagination, streaming
Solaris.TelemetryTelemetry events and metrics

License

MIT — see LICENSE.