api

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package api defines the go-ledger HTTP API using huma, which generates the OpenAPI 3.1 spec directly from the typed operation handlers. The spec is a byproduct of the same handlers that serve traffic, so the published docs and the API cannot drift. huma runs on a chi router via the humachi adapter, so chi owns routing and middleware while huma owns operations, validation, and problem+json errors.

Index

Constants

View Source
const APIVersion = "0.2.0"

APIVersion is the OpenAPI info.version. It is a deliberately stable semantic version, never a build SHA or timestamp, so the committed api/openapi.yaml snapshot stays reproducible across machines and CI.

View Source
const MaxRequestBodyBytes int64 = 64 * 1024

MaxRequestBodyBytes bounds every request body, so one request can no longer exhaust memory before validation runs (see ADR-012, "Input hardening"). cmd/server applies the same limit as a router-level maxBodyBytes middleware that wraps huma, so it runs first and enforces the hard cutoff. The write operations below also set MaxBodyBytes as their own huma option, so huma's own accounting returns a clean 413 for the same limit when it is the one reading the body, such as in tests that exercise the huma router directly without the wrapping middleware.

Variables

This section is empty.

Functions

func New

func New(router chi.Router, deps Deps) huma.API

New builds the huma API on the given chi router, registers every operation, and returns the API. huma also serves /openapi.json, /openapi.yaml, and /schemas/ from the same router. The interactive playground is registered separately by RegisterPlayground.

func RegisterPlayground

func RegisterPlayground(router chi.Router)

RegisterPlayground serves the interactive Scalar playground at /playground and its embedded JS asset. The spec it renders comes from huma at /openapi.json, so the playground always reflects the live API.

func SpecYAML

func SpecYAML() ([]byte, error)

SpecYAML builds the API on a throwaway router and serializes its OpenAPI spec to YAML. It is the single source for both the generator (cmd/genopenapi) and the drift test, so the committed snapshot and the runtime spec come from the same code path.

Types

type AccountAuditInput

type AccountAuditInput struct {
	ID     string `path:"id" format:"uuid" doc:"Account id"`
	Limit  int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max entries per page"`
	Cursor string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

AccountAuditInput is the account audit request: a path id plus keyset paging.

type AccountAuditOutput

type AccountAuditOutput struct {
	Body struct {
		Entries    []AuditEntryBody `json:"entries"`
		NextCursor *string          `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

AccountAuditOutput is one page of an account's audit log.

type AccountBalanceBody

type AccountBalanceBody struct {
	AccountID string `json:"account_id"`
	Name      string `json:"name"`
	Type      string `json:"type" doc:"One of: asset, liability, equity, income, expense"`
	Currency  string `json:"currency"`
	IsSystem  bool   `` /* 159-byte string literal not displayed */
	Balance   int64  `json:"balance" doc:"Signed derived balance in minor units"`
	// RolledUpBalance is Balance summed over this account and every
	// descendant in its hierarchy (ADR-023). It is additive display only:
	// the balance proof (the currency net/imbalance fields above) is
	// computed from own balances alone, since a rollup double-counts a
	// parent and its descendants by design.
	RolledUpBalance int64 `json:"rolled_up_balance" doc:"Balance including all descendants, in minor units"`
}

AccountBalanceBody is one account's identity plus its derived balance in the trial balance report. IsSystem accounts (FX clearing accounts, ADR-014) are included, clearly marked, since they hold the FX position and are part of the balance proof.

type AccountBody

type AccountBody struct {
	ID       string `json:"id" doc:"Account id (UUID)"`
	Name     string `json:"name"`
	Type     string `json:"type" doc:"One of: asset, liability, equity, income, expense"`
	Currency string `json:"currency" doc:"ISO 4217 code, e.g. USD"`
	// Status is the account's lifecycle gate (Task 5.5, audit A1.5): active,
	// frozen, or closed. See POST /v1/accounts/{id}/status to change it.
	Status string `json:"status" doc:"One of: active, frozen, closed"`
	// MinBalance is the optional floor enforced on this account's derived
	// balance, in minor units (Task 5.5, audit A1.5). null means no floor.
	MinBalance *int64 `json:"min_balance,omitempty" doc:"Optional minimum balance floor, in minor units. Omitted when unset."`
	// PartyReference and PartyType are optional linkage metadata for an
	// external KYC/party system (Task 6.1, audit A9.1). Both are omitted
	// when unset; this package does not validate them beyond a length cap,
	// the actual party/KYC system is external.
	PartyReference *string `` /* 134-byte string literal not displayed */
	PartyType      *string `json:"party_type,omitempty" doc:"Optional free-text party classification, e.g. individual or business. Omitted when unset."`
	// ParentID is this account's parent in the hierarchy (ADR-023), null for a root.
	ParentID *string `json:"parent_id" nullable:"true" doc:"Parent account id, null for a root"`
}

AccountBody is the JSON shape of an account in responses.

type AccountOutput

type AccountOutput struct {
	Body AccountBody
}

AccountOutput wraps an account in a response.

type AccountTreeNode added in v1.0.0

type AccountTreeNode struct {
	AccountBody
	Depth           int   `json:"depth"`
	OwnBalance      int64 `json:"own_balance" doc:"This account's own balance, in minor units"`
	RolledUpBalance int64 `json:"rolled_up_balance" doc:"Balance including all descendants, in minor units"`
}

AccountTreeNode is one row of the account hierarchy: the account, its depth from a root, its own balance, and the balance rolled up over its whole subtree (ADR-023).

type AccountTreeOutput added in v1.0.0

type AccountTreeOutput struct {
	Body struct {
		Accounts []AccountTreeNode `json:"accounts"`
	}
}

AccountTreeOutput is the list-account-tree response (ADR-023).

type AccountsOutput

type AccountsOutput struct {
	Body struct {
		Accounts []AccountBody `json:"accounts"`
	}
}

AccountsOutput is the list-accounts response.

type ApprovePendingOutput added in v1.0.0

type ApprovePendingOutput struct {
	Body TransactionBody
}

ApprovePendingOutput is the approve response: the transaction the approval posted. Approving an already-approved pending is idempotent (ApprovalService. Approve's own doc comment) and returns the SAME transaction rather than posting a second one, still with status 200.

type AuditAnchorBody

type AuditAnchorBody struct {
	ChainSeq  int64     `json:"chain_seq"`
	RowHash   string    `json:"row_hash"`
	CreatedAt time.Time `json:"created_at"`
}

AuditAnchorBody is the tenant's most recently recorded off-box anchor (Task 5.3, migration 0025): the chain_seq and row_hash the anchor job read and logged at CreatedAt.

type AuditEntryBody

type AuditEntryBody struct {
	ID            string    `json:"id"`
	Action        string    `json:"action"`
	TransactionID string    `json:"transaction_id"`
	Actor         string    `json:"actor"`
	Before        any       `json:"before,omitempty"`
	After         any       `json:"after"`
	CreatedAt     time.Time `json:"created_at"`
}

AuditEntryBody is one audit row in a response. Before and After are raw JSON (Before is omitted for a create). Amounts inside After are minor units.

type AuditHeadBody

type AuditHeadBody struct {
	ChainSeq int64  `json:"chain_seq"`
	RowHash  string `json:"row_hash"`
}

AuditHeadBody is the tenant's current chain head (Task 5.3): the chain_seq and row_hash of its latest audit_log row.

type AuditListOutput

type AuditListOutput struct {
	Body struct {
		Entries []AuditEntryBody `json:"entries"`
	}
}

AuditListOutput is a list of audit entries.

type AuditLogInput added in v1.0.0

type AuditLogInput struct {
	Limit  int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max entries per page"`
	Cursor string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

AuditLogInput is the tenant-wide audit log request: keyset paging only.

type AuditLogOutput added in v1.0.0

type AuditLogOutput struct {
	Body struct {
		Entries    []AuditEntryBody `json:"entries"`
		NextCursor *string          `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

AuditLogOutput is one page of the tenant's whole audit log.

type BalanceOutput

type BalanceOutput struct {
	Body struct {
		AccountID string `json:"account_id"`
		Amount    int64  `json:"amount" doc:"Signed balance in minor units (e.g. cents)"`
		Currency  string `json:"currency"`
	}
}

BalanceOutput is the account balance response.

type ConvertInput

type ConvertInput struct {
	// See CreateTransactionInput's IdempotencyKey field for why this is not
	// required:"true": a missing header should fail our explicit 400 check
	// below with a clear message, not huma's generic 422.
	IdempotencyKey string `` /* 405-byte string literal not displayed */
	Body           struct {
		FromAccount  string `json:"from_account" format:"uuid" doc:"Account to debit, in its own currency"`
		ToAccount    string `json:"to_account" format:"uuid" doc:"Account to credit; its currency is the conversion's destination currency"`
		SourceAmount int64  `json:"source_amount" doc:"Amount to convert, in minor units of the from account's currency; must be positive"`
	}
}

ConvertInput is the convert request body: the account to debit, the account to credit, and the amount to convert, in the from account's own currency. The destination currency always comes from the to account (ADR-014 decision 3): a client cannot supply a rate or target currency directly, since a client-controlled rate would be a money-theft vector.

type ConvertOutput

type ConvertOutput struct {
	Status   int
	Replayed bool `header:"Idempotent-Replayed"`
	Body     ConvertResponseBody
}

ConvertOutput is the convert response. Replayed mirrors CreateTransactionOutput: true when an Idempotency-Key matched an earlier convert request and the original conversion was returned unchanged. Status is 201 (converted) or 202 (ADR-025: held for approval); see CreateTransactionOutput's Status doc comment for the mechanism.

type ConvertResponseBody

type ConvertResponseBody struct {
	Transaction TransactionBody `json:"transaction"`
	FX          FXDetailBody    `json:"fx"`
	Pending     *PendingBody    `` /* 180-byte string literal not displayed */
}

ConvertResponseBody is the JSON shape of a successful convert: the posted transaction (source account, source clearing, destination clearing, destination account) plus the FX rate detail actually applied. Pending is set instead (ADR-025) when the conversion exceeded the approval threshold and was held rather than posted; Transaction and FX are left zero in that case.

type CreateAccountInput

type CreateAccountInput struct {
	Body struct {
		Name       string `json:"name" minLength:"1" maxLength:"200" doc:"Human-readable account name"`
		Type       string `json:"type" enum:"asset,liability,equity,income,expense" doc:"Fundamental account class"`
		Currency   string `` /* 134-byte string literal not displayed */
		MinBalance *int64 `json:"min_balance,omitempty" doc:"Optional minimum balance floor, in minor units. Omit for no floor."`
		// PartyReference and PartyType (Task 6.1, audit A9.1) are optional
		// linkage metadata for an external KYC/party system: an id and a
		// free-text classification (e.g. "individual", "business"). Neither
		// is validated beyond a length cap; the party/KYC system itself is
		// external and out of scope for this service.
		PartyReference *string `` /* 153-byte string literal not displayed */
		PartyType      *string `json:"party_type,omitempty" maxLength:"256" doc:"Optional free-text party classification, e.g. individual or business."`
		// ParentID (ADR-023) optionally nests the new account under an
		// existing one at creation time. The parent must be the same tenant
		// and currency; an unknown parent or a hierarchy violation is
		// rejected with 422 (see toHumaErr).
		ParentID *string `json:"parent_id,omitempty" format:"uuid" doc:"Optional parent account id. The parent must be the same tenant and currency."`
	}
}

CreateAccountInput is the create-account request body. Currency is optional (omitempty): when the caller omits it, the server stamps the deployment's configured DEFAULT_CURRENCY instead (ADR-014). MinBalance is also optional (Task 5.5, audit A1.5): every account is created active, so there is no status field here (see POST /v1/accounts/{id}/status).

type CreateFXRateInput added in v1.0.0

type CreateFXRateInput struct {
	Body struct {
		TenantID  string `json:"tenant_id,omitempty" doc:"Tenant to scope this rate to. Omit or empty for the global default."`
		Base      string `json:"base" pattern:"^[A-Z]{3}$" doc:"Base currency, 3 uppercase letters"`
		Quote     string `json:"quote" pattern:"^[A-Z]{3}$" doc:"Quote currency, 3 uppercase letters"`
		MidRateE8 int64  `json:"mid_rate_e8" minimum:"1" doc:"Mid rate, quote units per base unit, scaled by 1e8"`
		SpreadBps *int32 `` /* 145-byte string literal not displayed */
	}
}

CreateFXRateInput is the create-fx-rate request body.

type CreateTenantInput

type CreateTenantInput struct {
	Body struct {
		Name string `json:"name" minLength:"1" maxLength:"200" doc:"Human-readable tenant name"`
	}
}

CreateTenantInput is the create-tenant request body.

type CreateTransactionInput

type CreateTransactionInput struct {
	// The schema field is left optional (not required:"true") so a missing
	// header fails our explicit 400 check below with a clear message,
	// rather than huma's generic schema-validation 422.
	IdempotencyKey string `` /* 407-byte string literal not displayed */
	Body           struct {
		Currency string         `json:"currency" pattern:"^[A-Z]{3}$" doc:"ISO 4217 code shared by every posting"`
		Postings []PostingInput `json:"postings" minItems:"2" maxItems:"100" doc:"Two or more legs that must sum to zero"`
		// Reference is an optional external id for reconciliation (Task 4.3,
		// audit A1.3), for example an upstream payment processor's charge id
		// or a bank statement line. Must be unique for this tenant when
		// supplied: reusing one already in use returns 409 Conflict.
		Reference *string `` /* 155-byte string literal not displayed */
		// EffectiveAt is an optional value date (Task 4.3, audit A1.3),
		// distinct from when the row is actually written. Omitted, it
		// defaults to the post time.
		EffectiveAt *time.Time `` /* 143-byte string literal not displayed */
	}
}

CreateTransactionInput is the post-transaction request body.

type CreateTransactionOutput

type CreateTransactionOutput struct {
	Status   int
	Replayed bool `header:"Idempotent-Replayed"`
	Body     struct {
		TransactionBody
		Pending *PendingBody `` /* 226-byte string literal not displayed */
	}
}

CreateTransactionOutput is the post-transaction response. Replayed is surfaced as the Idempotent-Replayed header: true when an Idempotency-Key matched an earlier request and the original transaction was returned unchanged.

Status overrides the operation's DefaultStatus (201) at runtime: huma reads a field literally named Status on an output struct and uses it as the actual response code instead (see huma's processOutputType), so every return path through create-transaction sets it explicitly, either 201 (the transaction posted) or 202 (ADR-025: postings exceeded the tenant's approval threshold and were held as a pending instead; Body.Pending is set in that case and the TransactionBody fields are left zero).

type CreateWebhookSubscriptionInput

type CreateWebhookSubscriptionInput struct {
	Body struct {
		TenantID   string   `json:"tenant_id" format:"uuid" doc:"Tenant this subscription belongs to"`
		URL        string   `json:"url" doc:"Callback URL; must be an absolute http or https URL"`
		EventTypes []string `` /* 149-byte string literal not displayed */
	}
}

CreateWebhookSubscriptionInput is the create-subscription request body.

type CreateWebhookSubscriptionOutput

type CreateWebhookSubscriptionOutput struct {
	Body IssuedWebhookSubscriptionBody
}

CreateWebhookSubscriptionOutput wraps a newly created subscription.

type CurrencyBalanceBody

type CurrencyBalanceBody struct {
	Currency  string `json:"currency"`
	Net       int64  `` /* 136-byte string literal not displayed */
	Imbalance bool   `json:"imbalance" doc:"true if net is nonzero, which should never happen"`
}

CurrencyBalanceBody is one currency's net posted total in the trial balance report (Task 6.3, audit A9.2): the double-entry balance proof. In a correct ledger Imbalance is always false; a true value names a genuine invariant violation.

type Deps

type Deps struct {
	Accounts     *ledger.AccountService
	Transactions *ledger.TransactionService
	Audit        *ledger.AuditService
	// Admin backs the /v1/admin operations (Task 2.2b): onboarding a tenant
	// and issuing/rotating/revoking its api keys. Every /v1/admin/ path
	// already requires domain.ScopeAdmin via auth.HumaMiddleware
	// (RequiredHTTPScope), so the operations themselves add no further auth.
	Admin *admin.Service
	// Reports backs GET /v1/reports/trial-balance (Task 6.3, audit A9.2).
	Reports *ledger.ReportService
	// Disputes backs the /v1/disputes operations (Task 6.3, audit A9.2): a
	// dispute/chargeback data model built on the reversal primitive.
	Disputes *ledger.DisputeService
	// Approvals backs the /v1/pending operations (Task 9, ADR-025): listing,
	// reading, approving, rejecting, and cancelling a held (over-threshold)
	// transaction. Optional like the other services; a zero Deps leaves it
	// nil, and the approve/reject/cancel/list/get operations are still
	// registered (they are always part of the spec), but call a nil
	// *ledger.ApprovalService if ever invoked without one configured, the same
	// tradeoff every other optional Deps field already makes.
	Approvals *ledger.ApprovalService
	// FX backs the /v1/admin/fx operations (ADR-020): live rate and markup
	// config. Optional like the other services; a zero Deps leaves it nil.
	FX *fx.AdminService
	// DemoMode, when true, makes create-tenant prefill the new tenant with the
	// demo's starter FX rates and 1 percent markup (fx.PrefillDemoRates). It is
	// demo-only convenience; a zero Deps (production) leaves it false and no
	// prefill happens.
	DemoMode bool
	Auth     *auth.Resolver
	// RateLimiter, if set, is registered immediately after the auth middleware
	// (see New). It is optional: a zero Deps (spec generation, and tests that
	// only exercise unauthenticated routes) leaves it nil and no rate-limit
	// middleware is registered at all, rather than one that always fails open.
	RateLimiter *auth.Limiter
	// NegativeThrottle, if set, is passed straight through to
	// auth.HumaMiddleware (Task 5.2, audit A2.5/A6.4): it caps failed-auth
	// attempts per client IP BEFORE the resolver's database lookup runs, so a
	// garbage-API-key flood cannot exhaust the connection pool. Optional for
	// the same reason RateLimiter is: a nil value skips the gate entirely
	// rather than failing closed.
	NegativeThrottle *auth.NegativeThrottle
	// Revision is the running binary's build revision (git short SHA; "dev"
	// outside a real build), surfaced additively on GET /healthz (Task
	// 5.6a). A zero value (spec generation, and tests that never care) just
	// serializes as an empty string; it never affects the "ok" status the
	// deploy health check depends on.
	Revision string
}

Deps are the services the operations call, plus the auth resolver every /v1 request goes through to derive its tenant, and the per-key rate limiter applied after auth. The tenant is never a request field: HumaMiddleware resolves it from the bearer key and handlers read it back with auth.TenantFromContext. Spec generation passes a zero Deps because the handler bodies (and the middleware, which only runs on a served request) are never invoked while serializing the schema.

type DisputeBody

type DisputeBody struct {
	ID                      string     `json:"id"`
	TransactionID           string     `json:"transaction_id"`
	Status                  string     `json:"status" doc:"One of: open, resolved_reversed, resolved_rejected"`
	Reason                  string     `json:"reason"`
	ResolutionTransactionID *string    `` /* 134-byte string literal not displayed */
	CreatedAt               time.Time  `json:"created_at"`
	ResolvedAt              *time.Time `json:"resolved_at,omitempty" doc:"When the dispute was resolved. Unset while status is open."`
}

DisputeBody is the JSON shape of a dispute in responses.

type DisputeOutput

type DisputeOutput struct {
	Body DisputeBody
}

DisputeOutput wraps a dispute in a response.

type EmptyOutput

type EmptyOutput struct{}

EmptyOutput is the huma output for admin operations that report success with no response body, just a 204 No Content status.

type FXDetailBody

type FXDetailBody struct {
	SourceAmount    int64     `json:"source_amount" doc:"Amount converted, in minor units of the source account's currency"`
	ConvertedAmount int64     `json:"converted_amount" doc:"Amount credited, in minor units of the destination account's currency"`
	MidRateE8       int64     `json:"mid_rate_e8" doc:"Mid rate (destination units per source unit), scaled by 1e8, before the spread"`
	AppliedE8       int64     `json:"applied_e8" doc:"Informational, spread-adjusted rate actually reflected in converted_amount"`
	SpreadBps       int32     `json:"spread_bps" doc:"Spread widened against the customer, in basis points"`
	RateSource      string    `json:"rate_source" doc:"Where the mid rate came from"`
	EffectiveAt     time.Time `json:"effective_at" doc:"When the applied fx_rates row went live"`
	RateID          int64     `json:"rate_id" doc:"The fx_rates row id the mid rate and spread were read from"`
}

FXDetailBody is the applied-rate detail for a cross-currency convert: the requested and converted amounts, the mid rate and spread applied, and where that rate came from (ADR-014 decision 7). It only appears on the convert response: GET /v1/transactions/{id} reports per-posting currency only, not the fx_* snapshot (ADR-014, "Out of scope for v1").

type FXMarkupBody added in v1.0.0

type FXMarkupBody struct {
	DefaultSpreadBps *int32    `` /* 137-byte string literal not displayed */
	EffectiveAt      time.Time `json:"effective_at"`
}

FXMarkupBody is the JSON shape of a markup default in the set-fx-markup and get-fx-markup responses. DefaultSpreadBps is null when the row is a CLEAR (see SetFXMarkupInput).

type FXRateBody added in v1.0.0

type FXRateBody struct {
	TenantID           string    `json:"tenant_id" doc:"Tenant id, empty for the global default"`
	Base               string    `json:"base"`
	Quote              string    `json:"quote"`
	MidRateE8          int64     `json:"mid_rate_e8"`
	SpreadBps          *int32    `json:"spread_bps" doc:"Per-pair override, null when the row uses the markup default"`
	EffectiveSpreadBps int32     `json:"effective_spread_bps" doc:"Spread a conversion would actually apply: override, else default, else 0"`
	Source             string    `json:"source"`
	EffectiveAt        time.Time `json:"effective_at"`
}

FXRateBody is the JSON shape of an FX rate in the create-fx-rate and list-fx-rates responses.

type FXRateOutput added in v1.0.0

type FXRateOutput struct{ Body FXRateBody }

FXRateOutput wraps a rate in a create-fx-rate response.

type GetFXMarkupInput added in v1.0.0

type GetFXMarkupInput struct {
	TenantID string `query:"tenant_id" doc:"Tenant whose override to return alongside the global default. Omit for global only."`
}

GetFXMarkupInput is the get-fx-markup request: an optional tenant filter.

type GetFXMarkupOutput added in v1.0.0

type GetFXMarkupOutput struct {
	Body struct {
		Global nullableFXMarkupBody `json:"global"`
		Tenant nullableFXMarkupBody `json:"tenant"`
	}
}

GetFXMarkupOutput is the get-fx-markup response.

type HealthOutput

type HealthOutput struct {
	Body struct {
		Status   string `json:"status" example:"ok" doc:"Liveness status, \"ok\" while the service is serving"`
		Revision string `` /* 134-byte string literal not displayed */
	}
}

HealthOutput is the response for the liveness check. huma derives the response schema from the Body field's type. Revision is additive (Task 5.6a): status stays exactly "ok" so the deploy health check's grep for it keeps working unchanged; revision only adds a field callers can read to see which build is actually serving.

type IssueKeyInput

type IssueKeyInput struct {
	Body struct {
		TenantID  string     `json:"tenant_id" format:"uuid" doc:"Tenant this key acts as"`
		Name      string     `json:"name" minLength:"1" maxLength:"200" doc:"Human-readable label for this key"`
		Scopes    []string   `json:"scopes" minItems:"1" doc:"One or more of: read, post, admin"`
		ExpiresAt *time.Time `json:"expires_at,omitempty" doc:"Optional expiry. Omit for a key that never expires."`
	}
}

IssueKeyInput is the issue-key request body.

type IssueKeyOutput

type IssueKeyOutput struct {
	Body IssuedKeyBody
}

IssueKeyOutput wraps a newly issued or rotated key.

type IssuedKeyBody

type IssuedKeyBody struct {
	ID        string     `json:"id"`
	TenantID  string     `json:"tenant_id"`
	Name      string     `json:"name"`
	Scopes    []string   `json:"scopes"`
	ExpiresAt *time.Time `json:"expires_at" doc:"Null if the key never expires"`
	Plaintext string     `` /* 145-byte string literal not displayed */
}

IssuedKeyBody is the JSON shape of a just-issued or just-rotated key: its metadata plus the plaintext credential itself, shown exactly once. Store it now: only its hash is ever persisted, so it cannot be recovered again.

type IssuedWebhookSubscriptionBody

type IssuedWebhookSubscriptionBody struct {
	ID         string   `json:"id"`
	TenantID   string   `json:"tenant_id"`
	URL        string   `json:"url"`
	EventTypes []string `json:"event_types"`
	Secret     string   `` /* 198-byte string literal not displayed */
}

IssuedWebhookSubscriptionBody is the JSON shape of a just-created webhook subscription: its metadata plus the signing secret, shown exactly once. Store it now: only the secret's own value is ever persisted (Task 4.1, unlike an api key it is not hashed, since the delivery worker must read it back to sign every outbound payload), but it is still never returned again by any read after this one response.

type KeyBody

type KeyBody struct {
	ID         string     `json:"id" doc:"Key id (UUID)"`
	TenantID   string     `json:"tenant_id"`
	Name       string     `json:"name"`
	Scopes     []string   `json:"scopes"`
	ExpiresAt  *time.Time `json:"expires_at" doc:"Null if the key never expires"`
	LastUsedAt *time.Time `json:"last_used_at" doc:"Null if the key has never been used"`
	RevokedAt  *time.Time `json:"revoked_at" doc:"Null if the key is still live"`
	CreatedAt  time.Time  `json:"created_at"`
}

KeyBody is the JSON shape of an api key in the list-keys response. It never carries the plaintext: that is never stored anywhere to return, only shown once, at issue or rotate time, in IssuedKeyBody below.

type ListAccountsInput

type ListAccountsInput struct {
	Limit int `query:"limit" default:"100" minimum:"1" maximum:"500" doc:"Max accounts to return"`
}

ListAccountsInput is the list-accounts request: a capped limit, no cursor.

type ListDisputesInput

type ListDisputesInput struct {
	Status string `query:"status" doc:"Optional filter: one of open, resolved_reversed, resolved_rejected. Omitted returns every status."`
	Limit  int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max disputes per page"`
	Cursor string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

ListDisputesInput is the list-disputes request: an optional status filter plus keyset paging (Task 6.3, audit A9.2). Status is validated by hand in the handler (an empty string means "no filter"), the same convention TransactionListInput's Reference field uses, rather than an enum schema tag, which cannot express "valid value OR empty".

type ListDisputesOutput

type ListDisputesOutput struct {
	Body struct {
		Disputes   []DisputeBody `json:"disputes"`
		NextCursor *string       `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

ListDisputesOutput is one page of a tenant's disputes.

type ListFXRatesInput added in v1.0.0

type ListFXRatesInput struct {
	TenantID string `query:"tenant_id" doc:"Tenant whose overrides to include alongside the global defaults. Omit for globals only."`
}

ListFXRatesInput is the list-fx-rates request: an optional tenant filter.

type ListFXRatesOutput added in v1.0.0

type ListFXRatesOutput struct {
	Body struct {
		Rates []FXRateBody `json:"rates"`
	}
}

ListFXRatesOutput is the list-fx-rates response.

type ListKeysInput

type ListKeysInput struct {
	TenantID string `query:"tenant_id" format:"uuid" required:"true" doc:"Tenant to list keys for"`
}

ListKeysInput is the list-keys request: a required tenant filter.

type ListKeysOutput

type ListKeysOutput struct {
	Body struct {
		Keys []KeyBody `json:"keys"`
	}
}

ListKeysOutput is the list-keys response.

type ListTenantsOutput

type ListTenantsOutput struct {
	Body struct {
		Tenants []TenantBody `json:"tenants"`
	}
}

ListTenantsOutput is the list-tenants response.

type ListWebhookSubscriptionsInput

type ListWebhookSubscriptionsInput struct {
	TenantID string `query:"tenant_id" format:"uuid" required:"true" doc:"Tenant to list webhook subscriptions for"`
}

ListWebhookSubscriptionsInput is the list-webhook-subscriptions request: a required tenant filter.

type ListWebhookSubscriptionsOutput

type ListWebhookSubscriptionsOutput struct {
	Body struct {
		Subscriptions []WebhookSubscriptionBody `json:"subscriptions"`
	}
}

ListWebhookSubscriptionsOutput is the list-webhook-subscriptions response.

type OpenDisputeInput

type OpenDisputeInput struct {
	Body struct {
		TransactionID string `json:"transaction_id" format:"uuid" doc:"The transaction being disputed; must belong to the caller's tenant"`
		Reason        string `json:"reason" minLength:"1" maxLength:"256" doc:"Free-text reason for the dispute"`
	}
}

OpenDisputeInput is the open-dispute request body (Task 6.3, audit A9.2).

type PendingBody added in v1.0.0

type PendingBody struct {
	ID   string `json:"id"`
	Kind string `json:"kind" doc:"Which write path this pending will replay on approval: post, convert, or reverse"`
	// Status is the pending's own lifecycle state, not to be confused with
	// the output struct's Status field elsewhere in this package (which sets
	// the HTTP response code): this is a JSON body field.
	Status            string     `json:"status" doc:"One of: pending, approved, rejected, cancelled, expired"`
	ThresholdCurrency string     `json:"threshold_currency" doc:"The currency whose configured threshold this transaction exceeded"`
	ThresholdAmount   int64      `json:"threshold_amount" doc:"The configured threshold amount, in minor units, this transaction exceeded"`
	CreatedBy         string     `json:"created_by"`
	CreatedAt         time.Time  `json:"created_at"`
	DecidedBy         *string    `json:"decided_by,omitempty" doc:"Set once a decision (approve, reject, cancel, or the TTL sweep) has been made"`
	DecidedAt         *time.Time `json:"decided_at,omitempty"`
	Reason            *string    `json:"reason,omitempty" doc:"Set when status is rejected"`
	TransactionID     *string    `json:"transaction_id,omitempty" doc:"Set when status is approved: the transaction the approval posted"`
}

PendingBody is the JSON shape of a pending (over-threshold, held) transaction in responses (ADR-025). Payload itself is never exposed: it is an internal replay format (postPayloadBody/convertPayloadBody/ reversePayloadBody in internal/ledger), not a stable public contract.

type PendingListInput added in v1.0.0

type PendingListInput struct {
	Status string `query:"status" doc:"Optional filter: one of pending, approved, rejected, cancelled, expired. Omitted returns every status."`
	Limit  int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max pending transactions per page"`
	Cursor string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

PendingListInput is the list-pending request: an optional status filter, validated by hand the same way ListDisputesInput's Status is (an empty string means "no filter", which an enum schema tag cannot express), plus keyset paging.

type PendingListOutput added in v1.0.0

type PendingListOutput struct {
	Body struct {
		Pending    []PendingBody `json:"pending"`
		NextCursor *string       `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

PendingListOutput is one page of the tenant's pending transactions.

type PendingOutput added in v1.0.0

type PendingOutput struct {
	Body PendingBody
}

PendingOutput wraps a single pending transaction in a response.

type PostingBody

type PostingBody struct {
	AccountID   string `json:"account_id"`
	Amount      int64  `json:"amount"`
	Currency    string `json:"currency"`
	Description string `json:"description"`
}

PostingBody is one leg of a transaction in responses. Currency is the posting's own currency (ADR-014): a cross-currency (FX) transaction spans two currencies, so each leg is labeled with its own rather than inheriting one shared transaction-level currency.

type PostingInput

type PostingInput struct {
	AccountID   string `json:"account_id" format:"uuid" doc:"Account this leg posts to"`
	Amount      int64  `json:"amount" doc:"Signed amount in minor units: positive debit, negative credit"`
	Description string `json:"description,omitempty" maxLength:"256" doc:"Optional narration for this line"`
}

PostingInput is one leg of a transaction in the request body.

type RejectPendingInput added in v1.0.0

type RejectPendingInput struct {
	ID   string `path:"id" format:"uuid" doc:"Pending transaction id"`
	Body struct {
		Reason *string `json:"reason,omitempty" maxLength:"256" doc:"Optional free-text reason recorded on the pending"`
	}
}

RejectPendingInput is the reject request: a path id plus an optional free-text reason recorded on the pending.

type ResolveDisputeInput

type ResolveDisputeInput struct {
	ID   string `path:"id" format:"uuid" doc:"Dispute id"`
	Body struct {
		Action string `json:"action" enum:"reverse,reject" doc:"reverse: post a real reversal of the disputed transaction. reject: no money moves."`
	}
}

ResolveDisputeInput is the resolve-dispute request: a path id plus the action to take.

type ReverseTransactionOutput

type ReverseTransactionOutput struct {
	Status          int
	AlreadyReversed bool `header:"Already-Reversed"`
	Body            struct {
		TransactionBody
		Pending *PendingBody `` /* 186-byte string literal not displayed */
	}
}

ReverseTransactionOutput is the reverse-transaction response. AlreadyReversed mirrors CreateTransactionOutput's Replayed / ConvertOutput's Replayed: true when the original already had a reversal and the existing one was returned unchanged instead of a new one being posted. The response status is 201 whenever a reversal exists (new or already-reversed), or 202 (ADR-025) when the reversal itself exceeded the approval threshold and was held instead; see CreateTransactionOutput's Status doc comment for the mechanism.

type SetAccountParentInput added in v1.0.0

type SetAccountParentInput struct {
	ID   string `path:"id" format:"uuid" doc:"Account id"`
	Body struct {
		ParentID *string `json:"parent_id,omitempty" format:"uuid" doc:"New parent account id. Omit or send null to clear (make it a root)."`
	}
}

SetAccountParentInput is the set-account-parent request: a path id plus the new (or cleared) parent id (ADR-023).

type SetAccountStatusInput

type SetAccountStatusInput struct {
	ID   string `path:"id" format:"uuid" doc:"Account id"`
	Body struct {
		Status string `json:"status" enum:"active,frozen,closed" doc:"New account status"`
	}
}

SetAccountStatusInput is the set-account-status request: a path id plus the new status (Task 5.5, audit A1.5).

type SetFXMarkupInput added in v1.0.0

type SetFXMarkupInput struct {
	Body struct {
		TenantID string `json:"tenant_id,omitempty" doc:"Tenant to scope this default to. Omit or empty for the global default."`
		// DefaultSpreadBps is optional: omit it, or send it explicitly as
		// null, to append a CLEARED row. For a tenant scope, a cleared row
		// means the tenant drops its own override and follows the global
		// default again; for the global scope it means no markup at all.
		DefaultSpreadBps *int32 `` /* 234-byte string literal not displayed */
	}
}

SetFXMarkupInput is the set-fx-markup request body.

type SetFXMarkupOutput added in v1.0.0

type SetFXMarkupOutput struct{ Body FXMarkupBody }

SetFXMarkupOutput wraps a markup default in a set-fx-markup response.

type SetTenantPolicyInput

type SetTenantPolicyInput struct {
	ID   string `path:"id" format:"uuid" doc:"Tenant id"`
	Body struct {
		MaxTransactionAmount int64    `` /* 148-byte string literal not displayed */
		DailyVolumeLimit     int64    `` /* 150-byte string literal not displayed */
		AllowedCurrencies    []string `` /* 129-byte string literal not displayed */
	}
}

SetTenantPolicyInput is the set-tenant-policy request (Task 2.4b, audit A3.4): a path id plus the full policy in the body. Every field is optional; an omitted field is 0/empty, meaning "no limit" for that guardrail (see domain.TenantPolicy). This call always writes the FULL policy the body describes, not a partial merge: omitting a field clears (unlimits) that guardrail rather than leaving a previous value in place.

type SetTenantStatusInput

type SetTenantStatusInput struct {
	ID   string `path:"id" format:"uuid" doc:"Tenant id"`
	Body struct {
		Status string `json:"status" enum:"active,suspended,closed" doc:"New tenant status"`
	}
}

SetTenantStatusInput is the set-tenant-status request: a path id plus the new status in the body.

type ShredTenantPIIInput

type ShredTenantPIIInput struct {
	ID   string `path:"id" format:"uuid" doc:"Tenant id"`
	Body struct {
		Confirm bool `` /* 209-byte string literal not displayed */
	}
}

ShredTenantPIIInput is the shred-pii request: a path id plus a required confirmation flag (Task 6.2, audit A9.3). Confirm must be exactly true: omitting it (the zero value, false) fails closed with a 400 rather than silently proceeding, since this call is irreversible (see admin.Service.ShredTenantPII's own doc comment). There is deliberately no way to pass this as a query parameter or header instead: a body field forces a caller to construct the request deliberately, not accidentally via a copy-pasted URL.

type StatementEntryBody

type StatementEntryBody struct {
	ID             string    `json:"id" doc:"Posting id"`
	TransactionID  string    `json:"transaction_id"`
	Amount         int64     `json:"amount" doc:"Signed posting amount in minor units"`
	RunningBalance int64     `json:"running_balance" doc:"Account balance as of this posting, in minor units"`
	Description    string    `json:"description"`
	CreatedAt      time.Time `json:"created_at"`
}

StatementEntryBody is one line of a statement: a posting affecting the account, with the running balance as of that posting.

type StatementExportInput

type StatementExportInput struct {
	ID     string `path:"id" format:"uuid" doc:"Account id"`
	From   string `query:"from" doc:"RFC3339 timestamp. Only postings created at or after this time."`
	To     string `query:"to" doc:"RFC3339 timestamp. Only postings created strictly before this time."`
	Format string `` /* 142-byte string literal not displayed */
}

StatementExportInput is the export-account-statement request: a path id, an optional from/to created_at window (RFC3339, half-open like TransactionExportInput's), plus format (Task 6.3, audit A9.2). Unlike GET /v1/accounts/{id}/statement this is not keyset paged: it returns every matching entry up to ledger.MaxExportRows in a single response, the same bounded-export shape GET /v1/transactions/export uses.

type StatementExportOutput

type StatementExportOutput struct {
	ContentType        string `header:"Content-Type"`
	ContentDisposition string `header:"Content-Disposition" doc:"Set to attachment for csv; unset for json"`
	Truncated          bool   `` /* 160-byte string literal not displayed */
	Body               []byte
}

StatementExportOutput is a raw csv or json export body, the same shape TransactionExportOutput uses (Task 6.3, audit A9.2): Body is a []byte, which huma writes out verbatim instead of JSON-encoding it.

type StatementInput

type StatementInput struct {
	ID     string `path:"id" format:"uuid" doc:"Account id"`
	Limit  int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max entries per page"`
	Cursor string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

StatementInput is the account statement request: a path id plus keyset paging.

type StatementOutput

type StatementOutput struct {
	Body struct {
		AccountID  string               `json:"account_id"`
		Currency   string               `json:"currency"`
		Entries    []StatementEntryBody `json:"entries"`
		NextCursor *string              `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

StatementOutput is one page of an account statement.

type TenantBody

type TenantBody struct {
	ID     string `json:"id" doc:"Tenant id (UUID)"`
	Name   string `json:"name"`
	Status string `json:"status" doc:"One of: active, suspended, closed"`
}

TenantBody is the JSON shape of a tenant in admin responses.

type TenantOutput

type TenantOutput struct {
	Body TenantBody
}

TenantOutput wraps a tenant in a response.

type TransactionBody

type TransactionBody struct {
	ID       string        `json:"id"`
	Postings []PostingBody `json:"postings"`
	// ReversesTransactionID is set only when this transaction is itself a
	// reversal (Task 4.2, audit A1.2): the id of the transaction it reverses.
	// Omitted for an ordinary post or convert.
	ReversesTransactionID *string `` /* 130-byte string literal not displayed */
	// Reference is the caller's optional external id for reconciliation
	// (Task 4.3, audit A1.3), unique per tenant when present. Omitted when
	// the transaction was posted without one.
	Reference *string `json:"reference,omitempty" doc:"Optional client-supplied external reference, unique per tenant"`
	// EffectiveAt is the value date (Task 4.3, audit A1.3): when the
	// transaction is considered to have happened economically. Always
	// present in a response, even when the caller supplied none: it then
	// falls back to when the transaction was actually posted.
	EffectiveAt time.Time `json:"effective_at" doc:"Value date; defaults to when the transaction was posted if not supplied"`
}

TransactionBody is the JSON shape of a transaction in responses. There is deliberately no transaction-level currency field (ADR-014 decision 1): a convert transaction spans two currencies, so a single top-level currency could never label every posting correctly. Each PostingBody carries its own currency instead.

type TransactionExportInput

type TransactionExportInput struct {
	From          string `query:"from" doc:"RFC3339 timestamp. Only transactions created at or after this time."`
	To            string `query:"to" doc:"RFC3339 timestamp. Only transactions created strictly before this time."`
	EffectiveFrom string `` /* 162-byte string literal not displayed */
	EffectiveTo   string `` /* 164-byte string literal not displayed */
	Reference     string `query:"reference" doc:"Exact match on the transaction's reference."`
	Format        string `` /* 181-byte string literal not displayed */
}

TransactionExportInput is the export-transactions request: the same from/to/reference filters as TransactionListInput, plus format (Task 4.4, audit A7.2). Unlike the list endpoint this is not paged: it returns every matching transaction up to ledger.MaxExportRows in a single response.

type TransactionExportOutput

type TransactionExportOutput struct {
	ContentType        string `header:"Content-Type"`
	ContentDisposition string `header:"Content-Disposition" doc:"Set to attachment for csv; unset for json"`
	Truncated          bool   `` /* 151-byte string literal not displayed */
	Body               []byte
}

TransactionExportOutput is a raw csv or json export body (Task 4.4, audit A7.2). Body is a []byte, which huma writes out verbatim instead of JSON-encoding it (see huma's ContentTypeFilter / raw-body handling), so ContentType and, for csv, ContentDisposition are set explicitly here rather than inferred from a struct tag.

type TransactionListInput

type TransactionListInput struct {
	From          string `query:"from" doc:"RFC3339 timestamp. Only transactions created at or after this time."`
	To            string `query:"to" doc:"RFC3339 timestamp. Only transactions created strictly before this time."`
	EffectiveFrom string `` /* 162-byte string literal not displayed */
	EffectiveTo   string `` /* 164-byte string literal not displayed */
	Reference     string `query:"reference" doc:"Exact match on the transaction's reference."`
	Limit         int    `query:"limit" default:"50" minimum:"1" maximum:"200" doc:"Max transactions per page"`
	Cursor        string `query:"cursor" doc:"Opaque cursor from a previous page's next_cursor"`
}

TransactionListInput is the list-transactions request: optional date-range and reference filters, plus keyset paging (Task 4.4, audit A7.2). From and To follow the same half-open convention as domain.TransactionFilter: From is inclusive, To is exclusive.

type TransactionListOutput

type TransactionListOutput struct {
	Body struct {
		Transactions []TransactionBody `json:"transactions"`
		NextCursor   *string           `json:"next_cursor" doc:"Cursor for the next page, or null if this is the last page"`
	}
}

TransactionListOutput is one page of a tenant's transactions.

type TransactionOutput

type TransactionOutput struct {
	Body TransactionBody
}

TransactionOutput wraps a transaction in a response.

type TrialBalanceOutput

type TrialBalanceOutput struct {
	Body struct {
		Currencies []CurrencyBalanceBody `json:"currencies"`
		Accounts   []AccountBalanceBody  `json:"accounts"`
	}
}

TrialBalanceOutput is the GET /v1/reports/trial-balance response.

type VerifyAuditInput

type VerifyAuditInput struct {
	Fast bool `` /* 375-byte string literal not displayed */
}

VerifyAuditInput is the verify-audit-chain request: no path parameters, and one optional query flag choosing which of AuditService's two walks runs.

type VerifyAuditOutput

type VerifyAuditOutput struct {
	Body struct {
		Valid        bool             `json:"valid"`
		Checked      int              `json:"checked"`
		FirstBreakID *string          `json:"first_break_id" doc:"Id of the first audit row that failed to verify, or null if the chain is intact"`
		Pending      int              `` /* 147-byte string literal not displayed */
		Head         *AuditHeadBody   `json:"head" doc:"The tenant's current chain head (chain_seq + row_hash), or null for an empty chain"`
		Anchor       *AuditAnchorBody `json:"anchor" doc:"The tenant's most recently recorded off-box anchor, or null if none has been recorded yet"`
	}
}

VerifyAuditOutput is the result of walking the caller's tenant audit chain (ADR-012, "A per-tenant, tamper-evident audit chain"). FirstBreakID is null when Valid is true. Pending is the number of posted events not yet reflected in the chain (ADR-017): the chain is built asynchronously by a single background chainer, so a just-posted transaction can briefly show up here before its audit-chain link exists.

Head and Anchor (Task 5.3, audit A2.4) are independent of which walk (fast=true or false) actually ran: Head is always the tenant's current live chain head, and Anchor is always the latest off-box-logged checkpoint, or null if the tenant has none yet. A caller (or an automated external monitor comparing Head against the same anchor line its own log shipper captured, per Task 5.6) can tell a rewrite of already-anchored history apart from ordinary chain growth: if Head.ChainSeq equals Anchor.ChainSeq but Head.RowHash does not equal Anchor.RowHash, the anchored row itself was altered after being anchored, something a self-consistent in-database rewrite would not otherwise reveal (see migration 0025's own doc comment).

type WebhookSubscriptionBody

type WebhookSubscriptionBody struct {
	ID         string    `json:"id" doc:"Subscription id (UUID)"`
	TenantID   string    `json:"tenant_id"`
	URL        string    `json:"url"`
	EventTypes []string  `json:"event_types" doc:"Actions this subscription receives; empty means every action"`
	Active     bool      `json:"active"`
	CreatedAt  time.Time `json:"created_at"`
}

WebhookSubscriptionBody is the JSON shape of a webhook subscription in the list-webhook-subscriptions response (Task 4.1, audit A7.1). It never carries the signing secret: that is shown once, at creation time, in IssuedWebhookSubscriptionBody below.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL