# Weavr
[](https://hex.pm/packages/weavr)
A production-grade Elixir client for the [Weavr](https://weavr.io) 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
```elixir
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:
| 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
```elixir
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`)
```elixir
{: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
```elixir
{: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:
```elixir
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
`POST`s from your own application code:
```elixir
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:
```elixir
: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:
```elixir
{:ok, server} = Weavr.TestSupport.MockServer.start(20_321)
Weavr.TestSupport.MockServer.set(server, {"GET", "/managed_accounts"}, {200, ~s([{"id":"acc-1"}])})
```
## Development
```sh
mix deps.get
mix test
mix format --check-formatted
mix credo --strict
mix dialyzer
```
## License
MIT. See `LICENSE`.