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 one tx_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 direction flipped.

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 in direction.
  • 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 cash code.
  • Refunds — produces compensating entries.
Plugipay — Payments that don't tax your success