An unofficial, complete Elixir client for the Treasury Prime 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/1gives you a lazyStreamacross 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,:telemetryinstrumentation, 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
for your integration — Treasury Prime's API surface evolves, and some
lesser-used endpoints in this library (noted in their @moduledocs) were
implemented from documentation rather than against a live account.
Installation
def deps do
[
{:treasury_prime, "~> 1.0.0"}
]
endQuick start
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:
# 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: :sandboxclient = TreasuryPrime.Config.client()
TreasuryPrime.Account.list(client)Pagination
{: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.
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)))
endWebhooks
{: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:
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
endSandbox testing
{: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
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
mix deps.get
mix test
mix format
mix credo
mix dialyzer
License
MIT — see LICENSE.