Skip to main content
Version: 2.x

Go API

Install the versioned module:

go get github.com/Zonastery/bursar/golang/v2

The Go SDK mirrors Bursar's Python and TypeScript contracts with context.Context-first operations and typed (T, error) results.

:::important Python owns the CLI The Go module deliberately has no CLI, migration command, or tenant administration command. Use the Python bursar CLI to apply the shared SQL baseline and administer tenants, then use Go for application runtime behavior. See the quickstart. :::

Package map​

AreaPublic Go surface
Facade and runtimebursar.New, Bursar, lightweight NewBursarRuntime, and storage/runtime.NewBursarRuntime
Catalog and pricingCatalogService, PricingEngine, UsageMetrics
Credits and accountingCreditsService, exact Amount, priced usage, leases, plans, quotas, teams, ledger, and analytics
Billing and commerceBillingService, normalized lifecycle events, customer/subscription/invoice/payment state, checkout, auto-recharge, and CommerceService
Payment providersproviders/stripe, providers/dodo, providers/mock, and ProviderRegistry
Google ADK (optional)Separate golang/integrations/googleadk/v2 module with replay-safe model-call admission, settlement, and durable session recovery
Storage and outboxPostgresStorageRepository, OutboxWorker, projection handlers, maintenance, storage/clickhouse, and storage/s3
Telemetry and resilienceInstrumentation, telemetry/opentelemetry, and RetryBursarOperation

The complete exported signatures are available on pkg.go.dev.

Google ADK users install github.com/Zonastery/bursar/golang/integrations/googleadk/v2 separately and register its plugin through ADK's runner.PluginConfig. That module follows ADK's Go 1.26.5 requirement; the core SDK retains its Go 1.25 floor. See Meter Google ADK model calls for the complete registration and financial-lifecycle example.

Construct the facade and runtime​

For a complete process runtime, use the storage composition root. It accepts database URLs or caller-owned pools and keeps the tenant-bound primary and unscoped operator connections distinct:

package main

import (
"context"
"log"
"os"

bursar "github.com/Zonastery/bursar/golang/v2"
storageRuntime "github.com/Zonastery/bursar/golang/v2/storage/runtime"
)

func main() {
ctx := context.Background()
runtime, err := storageRuntime.NewBursarRuntime(ctx, storageRuntime.Options{
TenantID: os.Getenv("BURSAR_TENANT_ID"), // UUID
TenantSlug: os.Getenv("BURSAR_TENANT_SLUG"),
ProviderEnvironment: bursar.ProviderEnvironmentTest,
DatabaseURL: os.Getenv("DATABASE_URL"),
OperatorDatabaseURL: os.Getenv("BURSAR_OPERATOR_DATABASE_URL"),
})
if err != nil {
log.Fatal(err)
}
if err := runtime.Start(ctx); err != nil { // Verifies identity and retries catalog loading.
log.Fatal(err)
}
defer runtime.Close(context.Background())

if !runtime.Health(ctx).Ready {
log.Fatal("Bursar runtime is not ready")
}
}

storage/runtime.NewBursarRuntime validates the tenant UUID and optional slug, constructs the facade, recovery and maintenance handles, optional ClickHouse/S3 projections, and the outbox worker. Its Start, Health, State, CheckDependencies, Flush, and Close methods own that lifecycle. Health and State are race-safe local snapshots and never perform network or database I/O. Use CheckDependencies when a probe should actively verify PostgreSQL, the loaded catalog revision, bounded outbox status, dead letters, and configured component health. The operator connection is deliberately unscoped and separate from the tenant-bound primary connection.

The root bursar.NewBursarRuntime is the lightweight alternative. It accepts an already constructed *bursar.Bursar plus optional RuntimeComponent instances and coordinates catalog loading, component lifecycle, readiness, dependency checks, flush, and close; it does not create stores, projections, or workers. Both runtimes leave migrations and CLI administration to Python.

Bursar exposes Credits, Catalog, Accounts, and optional Billing and Commerce capabilities.

Exact and metric-priced credits​

All credits, measures, and prices use bursar.Amount, an alias of the battle-tested shopspring/decimal type. Parse runtime input with NewAmount; use MustAmount only for static constants and tests. QuantizeMoney applies the shared six-decimal, half-up accounting rule.

metrics := bursar.UsageMetrics{
Operation: "completion",
Measures: map[string]bursar.Amount{
"input_tokens": bursar.MustAmount("1200"),
"output_tokens": bursar.MustAmount("300"),
},
Dimensions: map[string]any{"model": "gpt-5"},
}

charged, err := sdk.Credits.DeductUsage(ctx, userID, metrics, bursar.PricedUsageOptions{
IdempotencyKey: "request:job-42",
})

DeductUsage, CanAffordUsage, RecordUsageMetrics, and DeductTeamUsage price UsageMetrics with the subject's effective catalog and rate card. ReserveUsage records the pricing context on a durable lease; SettleUsage prices final metrics against that immutable lease revision, so a catalog activation cannot change an in-flight charge.

For work whose final metrics are only known after execution, CreditsService.RunBilledUsage combines a metric-priced reserve, work callback, and settlement:

result, err := sdk.Credits.RunBilledUsage(ctx, userID, bursar.RunBilledUsageOptions{
Estimate: estimatedMetrics,
OperationKey: "request:job-42",
DoWork: func(ctx context.Context) (any, bursar.UsageMetrics, error) {
value, metrics, err := runModel(ctx)
return value, metrics, err
},
})

The operation key is reused for replay-safe settlement. A work failure releases the lease; retryable settlement reuses the same idempotency context. CreditsServiceOptions.CatalogCacheTTL controls TTL-aware lazy catalog refresh (five minutes by default; zero disables automatic refresh), and LazyExpiry enables expiry handling during normal credit reads. LowBalanceConfig adds edge-triggered CreditEventHandler notifications via its Thresholds, OnTrigger, and bounded MaxTrackedUsers fields. The concise LowBalance thresholds remain available without a callback.

The lower-level exact-amount methods remain available for administrative or already-priced operations: AddCredits, Deduct, Reserve, Settle, Release, Renew, refunds, grants, plans, entitlements, quotas, teams, ledger pages, usage receipts, and spend aggregates.

Billing management and provider events​

BillingService owns durable event claim/complete/fail processing and the default provider-neutral PostgreSQL lifecycle. Its management methods cover checkout intents, customer and subscription state, preferences, invoices, subscription changes, offer/top-up resolution, grace expiry, and pseudonymization. When the store supports auto-recharge, the facade also attaches Billing.AutoRecharge for profile, quote, enable/disable, retry, and post-deduction processing. CommerceService adds checkout, verified webhook, subscription commands, plan changes, portals, invoice links, and account overview operations.

Register post-completion billing notifications with BillingServiceOptions.EventHandlers (or BillingService.OnEvent). These BillingEventCallback functions are failure-isolated and cannot change a completed event. Use BillingServiceOptions.Handlers, On, or SetDefaultHandler only when deliberately replacing a normalized lifecycle route. CommerceService.GetAccountOverview(ctx, accountID) returns the combined AccountCommerceOverview projection, including durable credit, entitlement, subscription, invoice, transaction, usage, payment-method, and availability sections.

Provider-specific accounting inputs can be captured through ProviderReceiptSource, whose Begin/Finish pair returns a *ProviderReceipt containing exact UsageMetrics and financial metadata. ProviderReceipt.Validate checks that the metrics can be priced. This keeps provider response parsing at the integration boundary while settlement continues through the normal credits API.

Provider adapters verify raw webhook signatures before returning a normalized BillingEvent. Only then should the application ingest the event:

webhook, err := bursar.WebhookRequestFromHTTP(request, 1<<20)
if err != nil {
return err
}
verified, err := provider.HandleWebhook(request.Context(), webhook)
if err != nil {
return err
}
if verified.Event != nil {
_, err = sdk.IngestBillingEvent(request.Context(), *verified.Event)
}
return err

The maintained adapters are:

Stripe and Dodo implement the portable checkout and webhook contract plus their supported optional capabilities, including checkout status, customer and payment-method portals, customer creation, subscriptions, invoices, saved-payment charges, and plan changes. ProviderRegistry lazily constructs configured providers once. When a registry is attached through CommerceOptions, the facade rejects a live, test, or sandbox environment that does not match the tenant-bound store.

Storage projections, outbox, and maintenance​

PostgreSQL remains authoritative for accounting and the transactional outbox. NewPostgresStorageRepositoryFromStore exposes leased claim renewal, completion, retry/dead-letter recovery, stats, canonical usage exports, and billing-payload exports.

repository, err := bursar.NewPostgresStorageRepositoryFromStore(store)
if err != nil {
return err
}
usageHandler, err := bursar.NewUsageChargeOutboxHandler(repository, usageSink)
if err != nil {
return err
}
archiveHandler, err := bursar.NewBillingPayloadOutboxHandler(repository, billingArchive)
if err != nil {
return err
}
worker, err := bursar.NewOutboxWorker(
repository,
[]bursar.OutboxHandler{usageHandler, archiveHandler},
bursar.OutboxWorkerOptions{Concurrency: 4},
)

OutboxWorker is a bounded, concurrent RuntimeComponent with claim heartbeats, exponential retry delays, single-flight flushes, claim-loss reporting, and dead-letter limits. Snapshot exposes local lifecycle, the last manual run, and the last sanitized worker error without touching the store. Stop idempotently stops polling while leaving the caller-owned store open; Close provides the final RuntimeComponent shutdown alias. The two supplied handlers project only canonical records reloaded from PostgreSQL when necessary.

UsageChargeOutboxHandler detects the optional BatchUsageEventSink capability. With a batch sink, concurrent deliveries coalesce over a small fixed window into one WriteUsageBatch call; every handler waits for the shared result. storage/clickhouse.ClickHouseUsageStore implements this interface and preserves each UsageExportEntry.OutboxEventID, so retries and replays remain idempotent. Sinks that implement only UsageEventSink retain the direct one-event WriteUsage path, and cancellation still reaches the underlying context.

Optional projection packages keep PostgreSQL as the financial authority:

  • storage/clickhouse.NewUsageStore uses the official clickhouse-go/v2 connection for idempotent usage projection, schema compatibility checks, receipt history, and spend analytics.
  • storage/s3.NewBillingArchive uses the official AWS SDK for Go v2 to archive canonical billing envelopes under deterministic tenant-scoped keys. It supports the AWS credential chain, S3-compatible endpoints, server-side encryption, and checksums.

NewBursarMaintenanceFromStore runs bounded tenant-scoped lease, credit, catalog-change, and optional billing-grace work when invoked by the host. NewBursarOperatorMaintenance is restricted to an unscoped bursar_operator PostgreSQL client for storage-retention and partition work. Neither helper schedules itself.

Telemetry and retry​

Core instrumentation is vendor-neutral and no-op by default. To use the official telemetry/opentelemetry Go API adapter, configure the host's SDK/exporters and enable it before constructing stores and services:

import bursarotel "github.com/Zonastery/bursar/golang/v2/telemetry/opentelemetry"

restore, err := bursarotel.Enable(bursarotel.Options{})
if err != nil {
return err
}
defer restore()

The adapter emits operation spans, counts, and durations through host-provided tracer and meter providers. Bursar allowlists and sanitizes low-cardinality attributes; raw SQL, provider payloads, secrets, emails, and full identifiers are excluded.

RetryBursarOperation uses bounded, context-aware exponential backoff from cenkalti/backoff/v5. By default it makes at most three attempts and retries only typed Bursar errors marked retryable. Retry mutations only when they are replay-safe and always reuse the exact idempotency key.

result, err := bursar.RetryBursarOperation(ctx, func(ctx context.Context) (bursar.BalanceResult, error) {
return sdk.Credits.GetBalance(ctx, userID)
})

Errors​

SDK failures are typed *bursar.BursarError values. Use Go's normal errors.As/errors.Is flow or bursar.AsBursarError; stable codes, categories, retryability, and the indeterminate-write flag let transport adapters make safe decisions without parsing messages.

if classified, ok := bursar.AsBursarError(err); ok && classified.Retryable {
// Retry only a read or a replay-safe mutation with its original key.
}

The Go package contains no router integration or CLI. Applications retain their own net/http, Chi, Gin, Fiber, gRPC, worker, and scheduler composition.