A production-grade Elixir client for the Weavr Multi embedded banking API — authentication, bulk operations, identities, managed accounts, managed cards, users, and back-office — with zero required runtime dependencies, built-in retries with backoff and jitter, telemetry instrumentation, idempotency key support, and a consistent error model across every resource.
Why zero runtime dependencies?
weavr is built entirely on Erlang/OTP's standard library (:httpc for
HTTP, :ssl/:crypto for TLS) plus a small vendored JSON codec
(Weavr.JSON). This means adding weavr to a project never introduces a
dependency conflict with whatever HTTP client or JSON library you're
already using. If your application already has Jason and/or :telemetry
loaded (almost every Phoenix app does), weavr detects and uses them
automatically - see Weavr.JSON and Weavr.Telemetry for how to configure
this explicitly.
Installation
def deps do
[
{:weavr, "~> 1.0"}
]
endHonest coverage statement
This is the important part. weavr was built against Weavr's publicly
documented API surface, gathered from Weavr's docs site and OpenAPI
reference pages. Two parts of that surface were verified end-to-end against
real, fetched documentation pages and are implemented with confidence:
| Module | Status |
|---|---|
Weavr.Auth | Fully verified. Login (password/biometrics), the Auth Token to Access Token exchange for multi-identity root users, identity listing, logout, and SCA/PSD2 step-up detection all match Weavr's documented authentication model. |
Weavr.Bulk | Fully verified. The complete bulk-process lifecycle (submit, execute, pause/resume, cancel, plus list/get/list-operations), the SUBMITTED -> RUNNING -> PAUSED/CANCELLED/COMPLETED state machine, the 10,000-operation limit, and BulkProcessorConflict handling all match Weavr's documented bulk processing behavior. |
The remaining resource modules are structurally correct, but their request/response field names are not independently verified against Weavr's current schema:
| Module | What's confirmed | What isn't |
|---|---|---|
Weavr.Identities | Corporate/Consumer concept, onboarding pattern, root-user-vs-authorised-user distinction | Exact field names for create_corporate/3 etc. |
Weavr.Accounts | Managed Account concept, IBAN upgrade concept | Exact field names, exact upgrade/statement paths |
Weavr.Cards | Prepaid vs debit mode, physical card lifecycle (lost/stolen/replace/PIN), fulfilment states, spend rules | Exact field names, some exact paths (marked individually) |
Weavr.Users | Root-user vs Authorised-User distinction (GET /users returns both; mutations only work on Authorised Users) | Exact field names, some exact paths |
Weavr.BackOffice | The distinct Bearer-token auth scheme (vs. auth_token elsewhere), nested base URL under /multi/backoffice, delegated-access token concept | Exact field names beyond bulk/auth |
Every function in every module accepts a plain map for request bodies. This is deliberate: rather than guess at field names and silently bake wrong ones into typed structs, every resource function takes whatever shape your current Weavr API reference specifies, so a schema change on Weavr's end never requires a library update to keep working - only the HTTP plumbing (auth, retries, errors, telemetry) is "baked in," because that's the part that's actually hard to get right and unlikely to change.
Each module's @moduledoc repeats this coverage note inline with specifics
for that module. Before depending on this in production, confirm field
names against your Weavr API reference (https://weavr-multi-api.redoc.ly
for your account's spec version) and adjust the maps you pass in
accordingly - no code changes to this library should be required.
If you have access to your account's full OpenAPI spec and want full, verified struct-based coverage instead of plain maps, that's a very tractable follow-up: feed the spec to whatever's maintaining this package and ask for the resource modules to be filled out against it.
Two ways to manage the per-user auth token
Weavr's Multi API requires two things on most requests: an api-key
(your account) and an auth_token (the end user). weavr supports
managing that auth_token two ways:
Stateless
config = Weavr.Config.new(api_key: System.fetch_env!("WEAVR_API_KEY"), environment: :sandbox)
{:ok, %{auth_token: token}} =
Weavr.Auth.login_with_password(config, email: "user@example.com", password: "secret")
{:ok, accounts} = Weavr.Accounts.list(config, auth_token: token)Best for web app backends, where the token belongs to an HTTP session or a database row, not to a long-lived Elixir process.
Stateful (Weavr.Client)
{:ok, client} =
Weavr.Client.start_link(api_key: System.fetch_env!("WEAVR_API_KEY"), environment: :sandbox)
:ok = Weavr.Client.login_with_password(client, email: "user@example.com", password: "secret")
{:ok, accounts} =
Weavr.Client.with_token(client, fn config, token ->
Weavr.Accounts.list(config, auth_token: token)
end)Best for long-running single-user processes (a LiveView, a background
worker acting as one Weavr user). Weavr.Client is a GenServer with a
child_spec/1, so it can be placed directly under a supervision tree.
Authentication model
Weavr's auth model has a subtlety worth understanding before you build
against it - see Weavr.Auth's moduledoc for the full explanation, but in
short:
- Auth Token - returned by
login_with_password/2. Identifies the user and auth method, but on its own unlocks limited functionality. - Access Token - obtained by exchanging an Auth Token via
Weavr.Auth.exchange_for_access_token/3, adding which identity (Corporate/Consumer) the session acts as. Required if your embedder configuration uses multi-identity root users; the field is namedauth_tokenthroughout the rest of the API either way. - Back Office uses a completely different scheme: a Bearer token in
the
Authorizationheader, not theauth_tokenheader - seeWeavr.BackOffice's moduledoc.
Bulk operations
{:ok, bulk} =
Weavr.Bulk.submit(config, "users", [%{"email" => "a@b.com"}], auth_token: token)
:ok = Weavr.Bulk.execute(config, bulk.bulk_id, auth_token: token)
{:ok, status} = Weavr.Bulk.get(config, bulk.bulk_id, auth_token: token)
# status.status will move SUBMITTED -> RUNNING -> COMPLETED / PARTIALLY_COMPLETED
case Weavr.Bulk.pause(config, bulk.bulk_id, auth_token: token) do
:ok -> :paused
{:error, error} ->
if Weavr.Bulk.processor_conflict?(error) do
# the bulk process wasn't RUNNING, so it couldn't be paused
end
endUp to 10,000 operations per submission; see Weavr.Bulk for the full
state machine.
Error handling
Every function returns {:ok, result} or {:error, Weavr.Error.t()} -
nothing in this library raises on its own, except client-side input
validation (e.g. Weavr.Config.new/1 with a missing :api_key) and the
Weavr.unwrap!/1 convenience you opt into yourself:
case Weavr.Accounts.get(config, id, auth_token: token) do
{:ok, account} -> account
{:error, %Weavr.Error.API{status: 404}} -> nil
{:error, %Weavr.Error.API{status: 401}} -> reauthenticate()
{:error, %Weavr.Error.Network{}} -> retry_later()
{:error, %Weavr.Error.Timeout{}} -> retry_later()
end
# Or, if you'd rather raise:
account = Weavr.Accounts.get(config, id, auth_token: token) |> Weavr.unwrap!()See Weavr.Error for the full struct hierarchy (API, Network,
Timeout, InvalidRequest, Decode).
Retries
GET/idempotent calls automatically retry on 429, 500, 502, 503,
and 504 with exponential backoff and jitter (configurable via
max_retries on Weavr.Config.new/1). Network failures and timeouts also
retry. Non-retryable errors (4xx other than 429) fail immediately.
Idempotency
Pass idempotency_key: to any mutating call (submit, create, etc.) to
have it sent as an Idempotency-Key header, useful for safely retrying
POSTs from your own application code:
Weavr.Bulk.submit(config, "users", operations, auth_token: token, idempotency_key: my_key)Telemetry
Every HTTP call emits [:weavr, :request, :start | :stop | :exception]
events with %{method:, url:, attempt:, resource:, operation:} metadata
(plus status: on success or error: on failure). If your app already
uses :telemetry (directly or via Phoenix/Ecto), attach handlers the
usual way:
:telemetry.attach(
"log-weavr-requests",
[:weavr, :request, :stop],
fn _event, measurements, metadata, _config ->
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info("Weavr #{metadata.method} #{metadata.url} -> #{metadata.status} (#{ms}ms)")
end,
nil
)If :telemetry isn't present at all, weavr falls back to its own
minimal compatible dispatcher (Weavr.Telemetry) automatically - no
configuration needed either way.
Testing against your own code
weavr ships with a small in-memory mock HTTP server
(Weavr.TestSupport.MockServer, only compiled in :test) used by its own
test suite. Feel free to use it the same way in your application's tests
instead of hitting Weavr's real Sandbox:
{:ok, server} = Weavr.TestSupport.MockServer.start(20_321)
Weavr.TestSupport.MockServer.set(server, {"GET", "/managed_accounts"}, {200, ~s([{"id":"acc-1"}])})Development
mix deps.get
mix test
mix format --check-formatted
mix credo --strict
mix dialyzer
License
MIT. See LICENSE.