Ledger
The ledger is Plugipay's double-entry record of every money movement in your workspace — payments in, refunds out, fees deducted, payouts settled. Every state-changing operation produces ledger entries grouped under a transaction id (txId); balances roll up by account code. The Python SDK exposes two read-only methods behind plugipay.ledger. There is no create — the ledger is derived from primary resources (sessions, invoices, payouts, etc.). For the HTTP shape and the full chart of accounts, see API → Ledger.
Namespace
plug.ledger # _Ledger
Methods
list
plug.ledger.list(
*,
limit: int | None = None,
cursor: str | None = None,
order: str | None = None,
tx_id: str | None = None,
code: str | None = None,
source_type: str | None = None,
source_id: str | None = None,
) -> PageResult[LedgerEntry]
Cursor-paginated list of ledger entries.
tx_id— fetches every leg of one transaction (a payment + its fees + revenue allocation all share onetx_id).code— filters by chart-of-accounts code (e.g.cash,revenue,processor_fees,refunds_payable).source_type+source_id— pivots to "every entry produced by this payment / invoice / payout."order—"asc"(oldest first) or"desc"(newest first, default).
# All legs of one payment's transaction
page = plug.ledger.list(tx_id="tx_01HX...")
for entry in page.data:
sign = "+" if entry["direction"] == "credit" else "-"
print(f"{entry['code']:<24} {sign}{entry['amount']:>15} {entry['currency']}")
# All processor-fee entries in May
cursor = None
fees_total = 0
while True:
page = plug.ledger.list(limit=100, code="processor_fees", cursor=cursor)
for entry in page.data:
if entry["occurredAt"] >= "2026-05-01" and entry["occurredAt"] < "2026-06-01":
fees_total += entry["amount"] if entry["direction"] == "debit" else -entry["amount"]
if not page.has_more:
break
cursor = page.cursor
balances
plug.ledger.balances() -> list[LedgerBalance]
Returns the current rolled-up balance per account code per currency. No pagination — the chart of accounts is small (dozens of codes, not millions).
for bal in plug.ledger.balances():
print(bal["code"], bal["currency"], bal["amount"])
A typical response shape:
[
{"code": "cash", "currency": "IDR", "amount": 1_500_000_00},
{"code": "revenue", "currency": "IDR", "amount": 2_100_000_00},
{"code": "processor_fees", "currency": "IDR", "amount": 30_000_00},
{"code": "refunds_payable", "currency": "IDR", "amount": 0 },
# ...
]
The ledger is append-only. Plugipay never edits or deletes entries — corrections post compensating entries (e.g. a refund posts the inverse of the original payment's revenue allocation). To "undo" a balance, expect to see new entries with
directionflipped.
Types
from plugipay import LedgerEntry, LedgerBalance, PageResult
LedgerEntry likely-read fields:
entry["id"]—lge_+ ULID.entry["txId"]—tx_+ ULID; groups legs of one transaction.entry["code"]— chart-of-accounts code.entry["direction"]—"debit"or"credit".entry["amount"]— integer minor units. Always positive; the sign is indirection.entry["currency"].entry["sourceType"],entry["sourceId"]— what produced this entry.entry["occurredAt"]— ISO 8601 UTC.
LedgerBalance is the rolled-up per-(code, currency) total. Full reference at API → Ledger.
Common patterns
Tx-id drill-down for support tickets. When a buyer asks "where did my money go?", reconstruct the full leg set:
def show_transaction(plug, tx_id: str) -> None:
page = plug.ledger.list(tx_id=tx_id, order="asc")
for e in page.data:
sign = "+" if e["direction"] == "credit" else "-"
print(f"{e['occurredAt']} {e['code']:<22} {sign}{e['amount']:>15} {e['currency']}")
Daily P&L using balances. Cache yesterday's balances, compute deltas today:
import json
from pathlib import Path
today_path = Path(f"/var/data/ledger-balances-{today}.json")
yesterday_path = Path(f"/var/data/ledger-balances-{yesterday}.json")
today_bals = [bal.raw for bal in plug.ledger.balances()]
today_path.write_text(json.dumps(today_bals))
if yesterday_path.exists():
yesterday_bals = json.loads(yesterday_path.read_text())
by_key = {(b["code"], b["currency"]): b["amount"] for b in yesterday_bals}
for b in today_bals:
delta = b["amount"] - by_key.get((b["code"], b["currency"]), 0)
print(f"{b['code']:<22} {b['currency']} delta={delta}")
Reconcile cash against bank. The cash code's balance per currency should match what's settled to your bank account (modulo in-transit payouts):
cash = [b for b in plug.ledger.balances() if b["code"] == "cash" and b["currency"] == "IDR"]
expected_cash = cash[0]["amount"] if cash else 0
actual_from_bank = fetch_bank_balance("IDR")
if abs(expected_cash - actual_from_bank) > 100_00:
alert("Cash reconciliation drift > IDR 100")
Don't sum entries manually for live balances. balances() is the canonical answer — it's computed server-side from the full entry stream. Summing entries client-side is fine for time-bounded reports but error-prone for current state (you'd miss in-flight entries during pagination).
Use source_type to audit a specific resource.
# Every ledger leg produced by one invoice
page = plug.ledger.list(source_type="invoice", source_id="inv_01HX...")
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Bad code, malformed tx_id/source_id. |
403 |
insufficient_scope |
Key lacks plugipay:ledger:read. |
The ledger is read-only — no state-transition errors. Network and timeout failures surface as the usual PlugipayNetworkError / PlugipayTimeoutError. See Errors.
Next
- API → Ledger — chart of accounts and per-resource leg schemas.
- Reports — pre-aggregated P&L and cash-flow built on ledger entries.
- Payouts — the operation that drains the
cashcode. - Refunds — produces compensating entries.