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
| Environment | Address |
|---|---|
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 network | the service's own name on port 8080 |
| Kubernetes | a 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
| Credential | An OIDC client-credentials token for a registered consumer. Not an sk_ API key, and not a wallet user token |
| Rate limits | None. 429 rate_limited and 503 bulkhead_full are the public gateway's, and this service is not behind it |
| Errors | RFC 7807, same shape, but the schema is ProblemDetails and the codes are the ledger's own (limit_exceeded, precondition_failed, event_feed_blocked, …) |
| Idempotency | Required 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 |
| Amounts | Integer 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.