# TreasuryPrime
An unofficial, complete Elixir client for the [Treasury Prime](https://www.treasuryprime.com)
banking API (the Ledger product): bank accounts, ACH, wires, book
transfers, FedNow, debit cards, account opening / KYC, check deposit
(RDC), check issuing, Green Dot cash loads, webhooks, and sandbox
simulations.
- **Zero required HTTP dependency.** Ships with a default transport built
entirely on Erlang's own `:httpc`/`:ssl` — no Req, Finch, or Hackney
required. Bring your own by implementing one callback
(`TreasuryPrime.HTTPClient`) if you'd rather use what your app already
depends on.
- **Every call comes in a raising and non-raising flavor**, e.g.
`TreasuryPrime.Account.get/2` / `TreasuryPrime.Account.get!/2`.
- **Lazy pagination.** List endpoints return a `TreasuryPrime.Page`;
`TreasuryPrime.Page.stream/1` gives you a lazy `Stream` across every
page, fetching only what you actually consume.
- **~55 resource modules** covering the full Treasury Prime Ledger API
surface — accounts, payments, cards, account opening, and utilities.
- Idempotency key helpers, automatic retry with backoff on `429`/`5xx`,
`:telemetry` instrumentation, and webhook signature verification built in.
This is a community-built client and is not affiliated with or endorsed by
Treasury Prime, Inc. Always cross-check field names and behavior against
the [official API reference](https://docs.treasuryprime.com/reference/introduction)
for your integration — Treasury Prime's API surface evolves, and some
lesser-used endpoints in this library (noted in their `@moduledoc`s) were
implemented from documentation rather than against a live account.
## Installation
```elixir
def deps do
[
{:treasury_prime, "~> 1.0.0"}
]
end
```
## Quick start
```elixir
client =
TreasuryPrime.new(
api_key_id: System.fetch_env!("TREASURY_PRIME_KEY_ID"),
api_key_value: System.fetch_env!("TREASURY_PRIME_KEY_VALUE"),
environment: :sandbox # or :production
)
{:ok, page} = TreasuryPrime.Account.list(client)
Enum.each(page.data, &IO.inspect/1)
{:ok, account} = TreasuryPrime.Account.get(client, "acct_123456")
{:ok, ach} =
TreasuryPrime.Ach.create(
client,
%{
account_id: "acct_123456",
counterparty_id: "cp_098765",
amount: "100.00",
direction: "credit",
sec_code: "ccd"
},
idempotency_key: TreasuryPrime.Idempotency.generate_key()
)
```
Prefer not to thread a `client` through every call? Configure one in
`config.exs` / `runtime.exs` and pull it from `TreasuryPrime.Config`
instead:
```elixir
# config/runtime.exs
config :treasury_prime,
api_key_id: System.fetch_env!("TREASURY_PRIME_KEY_ID"),
api_key_value: System.fetch_env!("TREASURY_PRIME_KEY_VALUE"),
environment: :sandbox
```
```elixir
client = TreasuryPrime.Config.client()
TreasuryPrime.Account.list(client)
```
## Pagination
```elixir
{:ok, page} = TreasuryPrime.Ach.list(client, %{status: "pending"})
page.data #=> [%TreasuryPrime.Ach{}, ...]
# One page at a time:
{:ok, next_page} = TreasuryPrime.Page.next(page)
# Or lazily over everything:
client
|> TreasuryPrime.Ach.list!(%{status: "pending"})
|> TreasuryPrime.Page.stream()
|> Stream.filter(&(&1.amount == "100.00"))
|> Enum.take(10)
```
## Errors
Every function returns `{:ok, result} | {:error, %TreasuryPrime.Error{}}`,
or has a `!` variant that raises the same `TreasuryPrime.Error` struct
(which implements the `Exception` behaviour) instead.
```elixir
case TreasuryPrime.Ach.create(client, %{}) do
{:ok, ach} ->
ach
{:error, %TreasuryPrime.Error{type: :api_error, status: 400, body: body}} ->
Logger.error("ACH creation failed: #{inspect(body)}")
{:error, %TreasuryPrime.Error{type: :network_error}} = error ->
Logger.error(Exception.message(elem(error, 1)))
end
```
## Webhooks
```elixir
{:ok, webhook} =
TreasuryPrime.Webhook.create(client, %{
event: "ach.update",
url: "https://example.application.com/notify",
basic_user: "myapp",
basic_secret: System.fetch_env!("TREASURY_PRIME_WEBHOOK_SECRET")
})
```
In your webhook controller:
```elixir
def create(conn, params) do
header = conn |> Plug.Conn.get_req_header("authorization") |> List.first()
if TreasuryPrime.WebhookSignature.valid?(header, "myapp", webhook_secret()) do
event = TreasuryPrime.WebhookEvent.parse!(params)
{:ok, fresh_object} = TreasuryPrime.WebhookEvent.fetch(event, client)
MyApp.Webhooks.handle(event.event, fresh_object)
send_resp(conn, 200, "")
else
send_resp(conn, 401, "")
end
end
```
## Sandbox testing
```elixir
{:ok, _} = TreasuryPrime.Testing.Simulation.ach_status(client, ach.id, "settled")
{:ok, _} = TreasuryPrime.Testing.Simulation.card_auth_request(client, card.id, %{amount: "25.00"})
```
## Using a different HTTP transport
```elixir
defmodule MyApp.ReqHTTPClient do
@behaviour TreasuryPrime.HTTPClient
@impl true
def request(method, url, headers, body, opts) do
case Req.request(method: method, url: url, headers: headers, body: body, retry: false) do
{:ok, resp} -> {:ok, %{status: resp.status, headers: resp.headers, body: resp.body}}
{:error, reason} -> {:error, reason}
end
end
end
client = TreasuryPrime.new(api_key_id: "...", api_key_value: "...", http_client: MyApp.ReqHTTPClient)
```
## Resource coverage
| Area | Modules |
|---|---|
| Account opening | `AccountApplication`, `BusinessApplication`, `PersonApplication`, `AdditionalPersonApplication`, `Deposit`, `Kyc`, `KycProduct`, `AccountProduct`, `AccountNumberReservation` |
| Accounts & parties | `Account`, `Business`, `Person`, `AccountLock`, `AverageBalance`, `DailyBalance`, `Transaction`, `ReserveAccount`, `StatementConfig`, `TaxDocument` |
| Payments | `Ach`, `Wire`, `Book`, `NetworkTransfer`, `FedNow`, `Check`, `CheckDeposit`, `Counterparty`, `IncomingAch`, `IncomingWire`, `InvoiceAccountNumber`, `ManualHold`, `Greendot`, `DepositSweep` |
| Cards | `Card`, `CardProduct`, `CardEvent`, `CardAuthLoopEndpoint`, `DigitalWalletToken`, `Marqeta.JS`, `Marqeta.UXToolkit` |
| Utilities | `Document`, `File`, `RoutingNumber`, `Webhook`, `WebhookEvent`, `WebhookSignature` |
| Testing | `Testing.Simulation` |
## Development
```sh
mix deps.get
mix test
mix format
mix credo
mix dialyzer
```
## License
MIT — see [LICENSE](LICENSE).