Skip to content

Repository files navigation

treasuryprime-go

An unofficial, complete Go 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 dependencies. Built entirely on the Go standard library (net/http, encoding/json, crypto/subtle, log/slog) — nothing to go get beyond this module itself.
  • Hexagonal / DDD architecture. domain/* packages declare each resource's data and behavior as a Repository interface (the port); infrastructure/treasuryprimeapi implements every one of them against Treasury Prime's REST API (the adapter); application/* contains business workflows that orchestrate several repositories at once (opening an account end to end, issuing-and-displaying a card). See Architecture below.
  • ~39 resource repositories, each with a typed entity struct and typed CreateParams/UpdateParams.
  • Idiomatic Go throughout: every method takes context.Context first; errors implement Unwrap for errors.Is/errors.As; pagination is a lazy iterator in the bufio.Scanner style; configuration is functional options; constant-time webhook signature comparison via crypto/subtle.
  • Pluggable HTTP transport via a one-method Doer interface that *http.Client already satisfies — bring your own instrumented client, or use the default.

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 — some lesser-used endpoints (noted in their doc comments) were implemented from documentation rather than against a live account.

Installation

go get github.com/iamkanishka/treasuryprime-go

Requires Go 1.25 (uses generics extensively).

Quick start

package main

import (
	"context"
	"log"
	"os"

	"github.com/iamkanishka/treasuryprime-go"
	"github.com/iamkanishka/treasuryprime-go/domain/ach"
	"github.com/iamkanishka/treasuryprime-go/domain/shared"
)

func main() {
	client, err := treasuryprime.New(
		treasuryprime.WithAPIKey(os.Getenv("TREASURY_PRIME_KEY_ID"), os.Getenv("TREASURY_PRIME_KEY_VALUE")),
		treasuryprime.WithEnvironment(treasuryprime.EnvironmentSandbox),
	)
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	page, err := client.Account.List(ctx, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, account := range page.Items {
		log.Printf("%s: %s", account.ID, account.Status)
	}

	created, err := client.Ach.Create(ctx, ach.CreateParams{
		AccountID:      "acct_123456",
		CounterpartyID: "cp_098765",
		Amount:         "100.00",
		Direction:      ach.DirectionCredit,
		SecCode:        ach.SECCodeCCD,
	}, shared.WithIdempotencyKey(shared.NewIdempotencyKey()))
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("created ACH transfer %s", created.ID)
}

See examples/ for runnable programs covering basic usage, webhooks, and pagination.

Architecture

domain/                  — the ports: entities + Repository interfaces, zero I/O
  account/, ach/, wire/, card/, webhook/, shared/, ... (one package per resource)
application/             — business workflows spanning multiple repositories
  accountopening/        — submit person/business + account application + deposit, in sequence
  cardprovisioning/      — issue a card, then prep it for client-side display via Marqeta
infrastructure/          — the adapters: REST implementations of every domain Repository
  transport/             — auth, retries, JSON, generic Resource[T] CRUD executor
  treasuryprimeapi/      — one file per resource, implementing its domain.Repository
  marqeta/                — direct Marqeta API calls (JS / UX Toolkit access tokens)
  simulation/             — sandbox-only simulation endpoint
client.go, options.go    — composition root: wires every adapter into one Client

Why this matters in practice, not just in theory:

  • Testability without HTTP. application/accountopening is tested entirely with in-memory fakes implementing the Repository interfaces — no httptest.Server, no network, just plain structs (see application/accountopening/service_test.go).
  • One place to swap transports. Everything routes through transport.Doer; provide your own and every resource picks it up.
  • Resources are mechanical, not magical. Adding the 40th resource means writing a domain package (entity + interface) and an infrastructure package (a thin wrapper around the generic transport.Resource[T]) — no reflection, no code generation.

Pagination

first, err := client.Ach.List(ctx, url.Values{"status": {"pending"}})

it := client.Ach.Iterator(ctx, first)
for it.Next() {
	a := it.Item()
	fmt.Println(a.ID, a.Amount)
}
if err := it.Err(); err != nil {
	log.Fatal(err)
}

Pages are fetched lazily: stopping the loop early (a break, or running out of for) never triggers an HTTP call for a page you didn't end up needing.

Errors

Every method returns a *shared.Error (or wraps one) on failure. It implements Unwrap, so it composes with errors.Is/errors.As:

created, err := client.Ach.Create(ctx, ach.CreateParams{})
if err != nil {
	var apiErr *shared.Error
	if errors.As(err, &apiErr) && apiErr.Type == shared.ErrorTypeAPI {
		log.Printf("treasury prime rejected the request: %d %v", apiErr.StatusCode, apiErr.Body)
	}
}

shared.IsType(err, shared.ErrorTypeAPI) is a convenience wrapper around the same errors.As check.

Webhooks

sig := webhook.Signature{BasicUser: "myapp", BasicSecret: webhookSecret}

func handler(w http.ResponseWriter, r *http.Request) {
	if !sig.Valid(r.Header.Get("Authorization")) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var payload map[string]any
	json.NewDecoder(r.Body).Decode(&payload)
	event, _ := webhook.ParseEvent(payload)

	// Re-fetch via the matching typed repository, or via client.FetchRaw
	// for a generic map[string]any if you'd rather not switch on EventType.
}

Sandbox testing

_, err := client.Simulation.AchStatus(ctx, ach.ID, "settled")
_, err = client.Simulation.CardAuthRequest(ctx, card.ID, map[string]any{"amount": "25.00"})

Using a different HTTP transport

type myDoer struct{ inner *http.Client }

func (d *myDoer) Do(req *http.Request) (*http.Response, error) {
	// add tracing, custom retry policy, etc.
	return d.inner.Do(req)
}

client, err := treasuryprime.New(
	treasuryprime.WithAPIKey(id, secret),
	treasuryprime.WithDoer(&myDoer{inner: http.DefaultClient}),
)

Resource coverage

Area Repositories
Account opening AccountApplication, BusinessApplication, PersonApplication, AdditionalPersonApplication, Deposit, Kyc, KycProduct, AccountProduct, AccountNumberReservation
Accounts & parties Account, Business, Person, AccountLock, ReserveAccount, StatementConfig, Transaction (AverageBalance/DailyBalance/TaxDocument via Account)
Payments Ach, Wire, Book, NetworkTransfer, FedNow, Check, CheckDeposit, Counterparty, IncomingAch, IncomingWire, InvoiceAccountNumber, ManualHold, Greendot, DepositSweep
Cards Card, CardProduct, CardEvent, CardAuthLoopEndpoint, DigitalWalletToken, infrastructure/marqeta
Utilities Document, File, RoutingNumber, Webhook
Testing Simulation (sandbox only)

Development

go build ./...
go vet ./...
go test ./... -cover
gofmt -l .          # should print nothing
staticcheck ./...   # if installed

License

MIT — see LICENSE.

About

An unofficial, complete Go 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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages