Packages

A complete, production-grade Elixir client for the Mercury Banking API (accounts, transactions, recipients, invoices, payments, treasury, webhooks, and more), with typed errors, retry with backoff, and lazy auto-paginating streams.

Current section

Files

Jump to
Raw

README.md

# Mercury
[![Hex.pm](https://img.shields.io/hexpm/v/mercury.svg)](https://hex.pm/packages/mercury)
[![Docs](https://img.shields.io/badge/docs-hexdocs-blue)](https://hexdocs.pm/mercury)
A complete, production-grade Elixir client for the
[Mercury Banking API](https://docs.mercury.com/reference) — accounts,
transactions, recipients, invoices, payments, treasury, webhooks, and more.
Ported from and kept in parity with the
[reference Go SDK](https://github.com/your-org/mercury-go), adapted to
idiomatic Elixir: `{:ok, _} | {:error, _}` tuples with `!` raising
variants, typed exception structs, lazy auto-paginating `Stream`s, and a
`Req`-based transport with automatic retry.
## Installation
```elixir
def deps do
[
{:mercury, "~> 1.0"}
]
end
```
## Quick start
```elixir
client = Mercury.Client.new("secret-token:mercury_production_...")
{:ok, account} = Mercury.Accounts.get(client, "account-id")
{:ok, %Mercury.Page{items: accounts}} = Mercury.Accounts.list(client, limit: 25)
# Lazy, auto-paginating stream across every page:
client
|> Mercury.Accounts.stream()
|> Enum.each(&IO.inspect/1)
# Every function has a `!` variant that raises instead of returning a tuple:
account = Mercury.Accounts.get!(client, "account-id")
```
## Configuration
```elixir
Mercury.Client.new("secret-token:mercury_production_...",
base_url: "https://api.mercury.com/api/v1", # override for a sandbox
timeout: 30_000, # per-request timeout, ms
retry: %Mercury.Retry{max_retries: 3}, # see Mercury.Retry
on_request: fn method, url, request_id ->
Logger.debug("mercury request #{method} #{url} #{request_id}")
end,
on_response: fn method, url, request_id, status, duration_ms ->
Logger.debug("mercury response #{method} #{url} #{status} #{duration_ms}ms")
end,
req_options: [plug: {Req.Test, MyStub}] # escape hatch for tests, proxies, etc.
)
```
## Errors
Every error is one of the typed exceptions in `Mercury.*Error`:
`Mercury.AuthenticationError`, `Mercury.NotFoundError`,
`Mercury.ValidationError`, `Mercury.ConflictError`,
`Mercury.RateLimitError`, `Mercury.ServerError`, `Mercury.NetworkError`,
`Mercury.TimeoutError`, and the catch-all `Mercury.APIError`.
```elixir
case Mercury.Accounts.get(client, id) do
{:ok, account} ->
account
{:error, error} ->
cond do
Mercury.Error.not_found_error?(error) -> :not_found
Mercury.Error.rate_limit_error?(error) -> :try_again_later
true -> reraise error, __STACKTRACE__
end
end
```
Rate limit (429) and server (5xx) errors are automatically retried with
exponential backoff and jitter — see `Mercury.Retry`.
## Resources
| Module | Covers |
|---|---|
| `Mercury.Accounts` | accounts, cards, statements, sending money |
| `Mercury.Transactions` | cross-account transaction lookup, updates, attachments |
| `Mercury.Recipients` | payment recipients, tax form attachments |
| `Mercury.Invoices` | accounts-receivable invoices |
| `Mercury.Payments` | internal transfers, send-money approval requests |
| `Mercury.Categories` | expense categories |
| `Mercury.Customers` | accounts-receivable customers |
| `Mercury.Treasury` | treasury accounts, transactions, statements |
| `Mercury.Webhooks` | webhook endpoints + inbound signature verification |
| `Mercury.Events` | audit log |
| `Mercury.Organization` | org info, partner onboarding |
| `Mercury.Users` | platform users |
| `Mercury.SafeRequests` | SAFE (equity) requests |
| `Mercury.Credit` | credit accounts |
| `Mercury.Attachments` | file attachment metadata |
| `Mercury.OAuth2` | partner OAuth2 authorization flow |
## Webhooks
```elixir
defmodule MyAppWeb.MercuryWebhookController do
use MyAppWeb, :controller
def create(conn, _params) do
raw_body = conn.assigns.raw_body # captured by a raw-body-preserving plug
signature = conn |> Plug.Conn.get_req_header("mercury-signature") |> List.first()
if Mercury.Webhooks.verify_signature(raw_body, signature, signing_secret()) do
# handle the event
send_resp(conn, 200, "")
else
send_resp(conn, 401, "invalid signature")
end
end
end
```
> Confirm the exact header name and signing scheme against the current
> [Mercury webhooks documentation](https://docs.mercury.com/reference/webhooks)
> — see the `Mercury.Webhooks.verify_signature/3` docs for details.
## Naming conventions
* Response structs use snake_case fields (`account.legal_business_name`),
converted from the API's camelCase.
* `create`/`update` functions accept snake_case keys in a map or keyword
list and camelize them automatically before sending.
* Enum-like values (payment methods, wire types, statuses) should be
given as atoms or strings that already match Mercury's wire format
exactly, e.g. `:domesticWire`, `"businessChecking"`.
* Deeply nested, provider-specific objects (routing info, merchant data,
currency exchange info) are returned as plain maps rather than fully
modeled structs — consult the
[Mercury API reference](https://docs.mercury.com/reference) for their
shape.
* `Mercury.Transaction` groups its least-frequently-used nested fields
(`glAllocations`, `attachments`, `relatedTransactions`, `categoryData`,
`merchant`, `currencyExchangeInfo`, `details`) under a single
`transaction.extra` map with their original camelCase keys, to keep the
struct under BEAM's 32-field threshold.
## Development
```sh
mix deps.get
mix test
mix credo --strict
mix dialyzer
mix docs
```
## License
MIT — see [LICENSE](LICENSE).