# Solaris
[](https://hex.pm/packages/solaris)
[](https://hexdocs.pm/solaris)
[](LICENSE)
Production-grade Elixir client for the [Solaris Embedded Finance API](https://docs.solarisgroup.com/api-reference).
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 pagination** โ `stream/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
```elixir
def deps do
[
{:solaris, "~> 1.0"}
]
end
```
## Configuration
```elixir
# 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
```elixir
# 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{}}`.
```elixir
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
```elixir
# 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
```elixir
# 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")
```
```elixir
# 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
```elixir
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
```elixir
# 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
```elixir
# 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
```elixir
# 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](LICENSE).