Hex.pm

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

Honest 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:

ModuleStatus
Weavr.AuthFully 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.BulkFully 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:

ModuleWhat's confirmedWhat isn't
Weavr.IdentitiesCorporate/Consumer concept, onboarding pattern, root-user-vs-authorised-user distinctionExact field names for create_corporate/3 etc.
Weavr.AccountsManaged Account concept, IBAN upgrade conceptExact field names, exact upgrade/statement paths
Weavr.CardsPrepaid vs debit mode, physical card lifecycle (lost/stolen/replace/PIN), fulfilment states, spend rulesExact field names, some exact paths (marked individually)
Weavr.UsersRoot-user vs Authorised-User distinction (GET /users returns both; mutations only work on Authorised Users)Exact field names, some exact paths
Weavr.BackOfficeThe distinct Bearer-token auth scheme (vs. auth_token elsewhere), nested base URL under /multi/backoffice, delegated-access token conceptExact 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:

  1. Auth Token - returned by login_with_password/2. Identifies the user and auth method, but on its own unlocks limited functionality.
  2. 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 named auth_token throughout the rest of the API either way.
  3. Back Office uses a completely different scheme: a Bearer token in the Authorization header, not the auth_token header - see Weavr.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
end

Up 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.