A complete, production-grade Go client for the Unit embedded banking API — applications, customers, accounts, cards, payments, transactions, counterparties, repayments, recurring payments, check deposits, check payments, stop payments, chargebacks, statements, webhooks, events, tokens, institutions, authorizations, and sandbox simulation.
Zero runtime dependencies. Standard library only.
unit-go follows a domain-driven design. Each of Unit's 20 API surface areas
is its own bounded-context package under domain/, with its own entities,
value objects, and a Service interface + implementation:
unit-go/
├── unit.go # root Client facade wiring every bounded context together
├── shared/ # shared kernel: Money, Address, FullName, Phone, Tags, Relationship
├── telemetry/ # request lifecycle instrumentation hooks
├── internal/
│ ├── transport/ # HTTP client: auth, retry/backoff, idempotency, pagination
│ └── jsonapi/ # JSON:API envelope encode/decode (anti-corruption layer)
└── domain/
├── application/ # KYC/KYB onboarding applications
├── customer/ # customers, authorized users
├── account/ # deposit accounts, limits, balance history
├── card/ # debit/credit, individual/business, virtual/physical
├── payment/ # ACH, wire, book, bulk payments
├── transaction/ # the transaction ledger
├── counterparty/ # saved external bank accounts
├── repayment/ # book/ACH credit repayments
├── recurringpayment/ # scheduled recurring payments/repayments
├── checkdeposit/ # mobile check deposit
├── checkpayment/ # print-and-mail check payments
├── stoppayment/ # ACH/check stop payment orders
├── chargeback/ # card transaction disputes
├── statement/ # account statements (HTML/PDF)
├── webhook/ # webhook subscriptions + signature verification
├── event/ # the 90-day event log
├── token/ # customer/cardholder/org tokens, 2FA
├── institution/ # routing number lookup
├── authorization/ # pending card authorizations
└── sandbox/ # sandbox event simulation
Every domain Service is an interface, so any part of Client is
independently mockable in tests without touching the rest.
go get github.com/iamkanishka/unit-gopackage main
import (
"context"
"fmt"
"log"
unit "github.com/iamkanishka/unit-go"
"github.com/iamkanishka/unit-go/domain/account"
"github.com/iamkanishka/unit-go/domain/payment"
"github.com/iamkanishka/unit-go/shared"
)
func main() {
client, err := unit.New(unit.Config{
Token: "your-unit-api-token",
// BaseURL defaults to Unit's sandbox host; use "https://api.unit.co" in production.
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
acc, err := client.Accounts.CreateDeposit(ctx, account.CreateDepositParams{
CustomerID: "cus_123",
DepositProduct: "checking",
Name: "Primary Checking",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(acc.ID, acc.Balance)
payment, err := client.Payments.CreateACH(ctx, payment.CreateACHParams{
AccountID: acc.ID,
CounterpartyID: "cp_456",
Amount: shared.MoneyFromDollars(150.00),
Direction: "Credit",
Description: "Vendor payment",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(payment.ID, payment.Status)
}List calls return the first page plus a lazy *transport.Paginator[T]:
items, paginator, err := client.Transactions.List(ctx, transaction.ListQuery{AccountID: acc.ID, Limit: 100})
for paginator.HasNext() {
next, err := paginator.Next(ctx)
if err != nil {
log.Fatal(err)
}
items = append(items, next...)
}
// or, to drain everything at once:
// all, err := paginator.All(ctx)Every failed call returns *unit.Error (an alias for the transport error
type), which exposes the HTTP status, Unit's request ID, the JSON:API error
objects, and whether the failure was retryable:
_, err := client.Accounts.Get(ctx, "does-not-exist")
var apiErr *unit.Error
if errors.As(err, &apiErr) {
fmt.Println(apiErr.StatusCode, apiErr.Code(), apiErr.RequestID)
}Every request automatically retries on 429 and 5xx responses (and on bare
transport failures) with exponential backoff and jitter, honoring
Retry-After when Unit sends it. Configure or disable this:
client, err := unit.New(unit.Config{
Token: token,
RetryPolicy: &unit.RetryPolicy{MaxAttempts: 2, BaseDelay: 100 * time.Millisecond, MaxDelay: time.Second, Jitter: 0.3},
})
// or: RetryPolicy: unitPtr(unit.NoRetry())POST/PATCH requests get an auto-generated Idempotency-Key header when the
caller doesn't supply one, so accidental retries never double-submit a
payment. Supply your own for calls you want to control explicitly (e.g. a
key derived from your own transaction ID):
client.Payments.CreateACH(ctx, payment.CreateACHParams{..., IdempotencyKey: "order-8842"})Every request emits start/stop/retry/exception events through an injectable
telemetry.Handler, for wiring into your metrics or logging stack:
client, err := unit.New(unit.Config{
Token: token,
Telemetry: telemetry.HandlerFunc(func(ctx context.Context, e telemetry.Event) {
metrics.Observe(e.Type, e.Method, e.Path, e.StatusCode, e.Duration)
}),
})if !webhook.VerifySignatureSHA512(rawBody, r.Header.Get("X-Unit-Signature"), signingKey) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}VerifySignatureSHA1 is also available for webhook subscriptions still on
Unit's legacy signing scheme.
go build ./...
go vet ./...
gofmt -l . # should print nothing
go test ./... -race -coverThe test suite runs entirely against net/http/httptest fake servers — no
network access or live Unit credentials required.
Resource attribute sets here reflect Unit's well-documented, stable API
shapes as of this package's construction. Unit's OpenAPI spec is the source
of truth for exact field names and any newly added attributes; if you hit a
field this package doesn't expose yet, it's straightforward to extend the
relevant domain/<context> package's attribute struct — the JSON:API
decode plumbing in internal/jsonapi and internal/transport doesn't need
to change.
MIT