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 —
:telemetryevents on every request and webhook; compatible with Prometheus/StatsD - 🌊 Streaming pagination —
stream/1on all list endpoints viaSolaris.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"}
]
endConfiguration
# 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
endPagination & 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
endManual 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")
endTelemetry
# 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
endEvents emitted:
[:solaris, :request, :start][:solaris, :request, :stop]— includesmethod,path,status,duration[:solaris, :webhook, :received]— includesevent_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
| Module | Description |
|---|---|
Solaris.Onboarding.Persons | Person CRUD, mobile numbers, tax IDs, documents |
Solaris.Onboarding.Businesses | Business CRUD, legal reps, beneficial owners |
Solaris.Onboarding.KYC | Identification sessions, signings, screener hits |
Solaris.Banking.Accounts | Accounts, balances, bookings, savings, IBANs |
Solaris.Banking.SEPA | SCT, Instant SCT, SDD mandates |
Solaris.Banking.Transactions | Cash ops, top-ups, remittances, payouts |
Solaris.Cards | Issuance, lifecycle, tokenization, 3DS |
Solaris.Lending.Loans | Loan servicing, repayment, dunning |
Solaris.Lending.ConsumerLoans | Loan applications, offers, SECCI |
Solaris.Lending.Overdraft | Overdraft facilities |
Solaris.Lending.Splitpay | BNPL / installment plans |
Solaris.Lending.TradeFinance | Business trade credit lines |
Solaris.ChangeRequests | SCA/2FA completion flow |
Solaris.Webhooks | Signature verification, dispatch |
Solaris.Webhooks.Plug | Phoenix/Plug webhook endpoint |
Solaris.Pagination | Cursor pagination, streaming |
Solaris.Telemetry | Telemetry events and metrics |
License
MIT — see LICENSE.