WalletD developers

Reaching the ledger

ledgerd is not on the internet. Who this section is for, what credential it takes, and what the placeholder host in the contract means.

ledgerd is the standalone double-entry ledger that every WalletD money path posts into. It is a separate service with its own database, its own contract and its own credentials, and it is not exposed at the public API gateway.

There is no public host for this API. The servers entry in the published contract is the placeholder https://ledger.internal.invalid, and it is a placeholder on purpose: publishing the real internal address would be handing out a target. Substitute your own deployment's internal address.

Who this section is for

Partners running the stack. You operate ledgerd yourself, inside your own deployment. You need this to provision consumers, close periods, read a trial balance, and answer an auditor.

Teams consuming the ledger directly. A service of yours, inside the same cluster, that wants the books rather than the wallet abstraction over them.

Not a partner integrating against the wallet API over the internet. If you hold a sk_live_ or sk_sandbox_ key, everything you need is in the Wallet API, and the ledger moves underneath it without you calling it. GET /v1/ledger/summary and GET /v1/transactions on the wallet API are the ledger views that are reachable from outside.

Why it is not exposed

The cluster's public routes are three surfaces: the API gateway, Keycloak, and the admin portal. walletd, authsvc and ledgerd have no public route at all — not an authenticated one, not a restricted one. Nothing outside the cluster can address the ledger, whatever credential it holds.

That is the deliberate shape: the ledger of record is the one component where a forged or replayed call cannot be undone by a compensating API call, only by a correcting posting that stays in the history forever. Keeping it off the internet removes an entire class of question.

Getting a credential

Consumers are OIDC service clients. A consumer is created by an operator with database access, never through the API, because creating a consumer is the authorization to call the service:

ledgerd consumer create --name walletd --subject <the OIDC subject its tokens carry>

The --subject is the sub claim the identity provider puts in that client's access tokens, and it is what ledgerd matches on. Issue the client credentials in your IdP first, read the subject off a token it issues, then register it here. ledgerd consumer ensure is the same call, safe to re-run, which is what a bootstrap job uses; ledgerd consumer list shows what is registered.

Calls then carry an ordinary bearer token from that client:

Authorization: Bearer <client-credentials access token>

A consumer only ever sees its own ledgers. A ledger id that belongs to another consumer answers 404, never 403, for the same reason the wallet API does: an id you do not own must be indistinguishable from one that does not exist.

Where it listens

EnvironmentAddress
Compose (make up in ledgerd)http://127.0.0.1:8095 — published on loopback only, so it cannot be reached from another machine on your network
Inside the compose networkthe service's own name on port 8080
Kubernetesa ClusterIP Service in the wallet namespace, no HTTPRoute, no LoadBalancer

GET /healthz and GET /version are unauthenticated: they are the container probes and the deployment check. Everything else requires a consumer token.

What differs from the wallet API

CredentialAn OIDC client-credentials token for a registered consumer. Not an sk_ API key, and not a wallet user token
Rate limitsNone. 429 rate_limited and 503 bulkhead_full are the public gateway's, and this service is not behind it
ErrorsRFC 7807, same shape, but the schema is ProblemDetails and the codes are the ledger's own (limit_exceeded, precondition_failed, event_feed_blocked, …)
IdempotencyRequired on every mutation that moves money, same Idempotency-Key header. GET /v1/ledgers/{ledgerId}/idempotency/{key} returns the stored result of a key, which is how a caller finishes a repair after a refusal it did not see the outcome of
AmountsInteger minor units of the ledger's own commodity, which is not necessarily the wallet tenant's currency

Two-phase postings, in one paragraph

Money moves as a balanced posting or not at all. A single-phase movement is POST /v1/ledgers/{ledgerId}/postings: legs that sum to zero, committed atomically. A two-phase movement is POST …/pendings to reserve, then post or void it — which is what an authorization hold on a payment is underneath. Nothing is ever edited or deleted: a reversal is a contra posting, so the original and its reversal both stand in the history. The money model page explains why that matters to a caller.

On this page