Skip to main content

Ledger — reconcile against your own books

If you keep your own accounts, this is the chapter that lets you prove ours agree with them.

The invariant

Every transaction writes a balanced set of ledger entries — debits equal credits, always. Entries are append-only: a correction is a new compensating entry, never an edit. So the ledger is replayable, and any balance is derivable from it.

GET /ledger/health returns that invariant as a boolean over your tenant's books. It is cheap. Call it nightly; alert if it is ever false.

Reading entries

GET /ledger/entries?page=0&size=100

Each entry carries its wallet, fund, direction (DEBIT/CREDIT), amount, the transaction that produced it, and its timestamp.

Reconciling by your own reference

The endpoint built for exactly this:

GET /transactions/by-reference?referenceId=trip-88213

It returns a page, not a single row — deliberately. One reference legitimately spans several transactions: a posting and its reversal, a top-up and the credit it produced. Code for the list.

A nightly job that works:

  1. For each of yesterday's business events, GET /transactions/by-reference with your id.
  2. Assert exactly one non-reversed transaction, COMPLETED, for the amount you expect.
  3. Fetch GET /transactions/{id}/detail for anything that disagrees — it includes the gateway block for top-ups and withdrawals, which is usually where the answer is.
  4. Finish with GET /ledger/health.

Anything unmatched is a real discrepancy worth a human.

GET /transactions behaves differently depending on the token

With your machine token it returns transactions across your whole tenant.

With a wallet-user token — the same endpoint, called from your app — the server rewrites the query to that user's own activity. The clamp is applied on our side; you cannot widen it by passing different parameters.

Same URL, two scopes. If you proxy this endpoint from your app, know which token you are forwarding.

What is true afterwards

What each movement records about its authorisation

Alongside the amounts, every transaction and every withdrawal stores how it came to be authorised: which credential asked, through which surface, and what we could prove about the customer at that moment.

That last part is the one to understand, because it decides who answers for a disputed movement. A payment your backend instructs on a machine credential records that no end user was identified. A movement made with a delegated token records that you asserted the user rather than that we authenticated them. A customer who logged in to us directly records that we did.

The distinction is stamped when the row is written and never recalculated, because the question a dispute asks is what was known at the time, not what today's rules would say.

These fields are not returned by the read APIs yet. They are described here because they affect how you should think about delegated sessions — not because you can query them today.

You can produce, for any business event you recorded, the GoHubPay transaction id and the balanced ledger pair behind it — and ledger/health agrees. That is the state an auditor will ask you to demonstrate.

Next: Top-up.