Ledger

The ledger is Plugipay's double-entry accounting record of every money movement in your workspace: charges, refunds, platform fees, taxes, payouts. Every event posts paired debit / credit entries to typed account codes (cash, revenue, revenue_refund, platform_fee, etc.); the ledger is the source of truth your payouts.balance() figure is derived from. This page covers the plugipay.ledger namespace; for the underlying HTTP surface see API: Ledger, and for the account-code taxonomy see Concepts → Ledger.

Namespace

plugipay.ledger — every method on this namespace:

plugipay.ledger.list(params?)
plugipay.ledger.balances()

The ledger is read-only via the SDK. You can't post arbitrary entries — Plugipay derives them from charges, refunds, and payouts. To export the full ledger as CSV for accounting hand-off, use the low-level client.request against the /api/v1/reports/ledger.csv endpoint (see Reports for why it's not auto-exposed).

Methods

ledger.list

Signature. plugipay.ledger.list(params?): Promise<{ data: LedgerEntry[]; cursor: string | null; hasMore: boolean }>

Cursor-paginated list of ledger entries. Filters:

  • code — account code, e.g. 'cash', 'revenue', 'platform_fee', 'revenue_refund'
  • txId — group of entries that were posted in the same atomic transaction
  • sourceType'checkout_session', 'invoice', 'refund', 'payout'
  • sourceId — specific source object
  • order'asc' (oldest first) or 'desc' (newest first, default)
  • limit, cursor — see Pagination
import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
});

// All entries from one checkout session's settlement transaction:
const { data } = await plugipay.ledger.list({
  sourceType: 'checkout_session',
  sourceId: 'cs_01HX...',
});

for (const entry of data) {
  console.log(`${entry.postedAt} ${entry.code.padEnd(20)} ${entry.direction} ${entry.amount}`);
}

ledger.balances

Signature. plugipay.ledger.balances(): Promise<LedgerBalance[]>

Returns the running balance for every account code: total debits, total credits, and the net balance. This is the "trial balance" view.

const balances = await plugipay.ledger.balances();
for (const b of balances) {
  console.log(`${b.code}: ${b.balance} (D ${b.debits} / C ${b.credits})`);
}

In a well-formed double-entry system, the sum of all balances should be zero — every debit has a matching credit.

Types

interface LedgerEntry {
  id: string;                  // 'le_...'
  accountId: string;
  txId: string;                // 'tx_...' — groups paired entries
  code: string;                // account code, e.g. 'cash'
  direction: 'debit' | 'credit';
  amount: number;              // always positive; direction tells you the sign
  currency: string;
  sourceType: string;          // 'checkout_session' / 'invoice' / 'refund' / 'payout'
  sourceId: string;
  memo: string | null;
  postedAt: string;            // ISO-8601
}

interface LedgerBalance {
  code: string;
  debits: number;
  credits: number;
  balance: number;             // debits - credits (or vice versa depending on account nature)
}

For the full code taxonomy (which codes exist, which are debit-natured vs credit-natured), see API: Ledger.

Common patterns

Inspect a single transaction's entries

When debugging "where did this money go?", group by txId:

async function entriesByTx(txId: string) {
  const { data } = await plugipay.ledger.list({ txId, order: 'asc', limit: 100 });
  return data;
}

// Pretty-print a transaction:
const entries = await entriesByTx('tx_01HX...');
console.table(entries.map((e) => ({
  code: e.code,
  direction: e.direction,
  amount: e.amount,
  source: `${e.sourceType}/${e.sourceId}`,
})));

Cross-check payouts.balance() against the ledger

Your available balance should equal cash balance minus payout_in_transit (locked):

const balances = await plugipay.ledger.balances();
const cash = balances.find((b) => b.code === 'cash')?.balance ?? 0;
const locked = balances.find((b) => b.code === 'payout_in_transit')?.balance ?? 0;

const apiBalance = await plugipay.payouts.balance();

console.log('Ledger says:', cash - locked);
console.log('API says:   ', apiBalance.available);

If these diverge significantly, something has gone wrong — reach out to support with the figures.

Reconcile revenue to your accounting system

Pull every revenue credit since your last reconciliation cursor:

async function* newRevenueEntries(sinceIso: string) {
  let cursor: string | undefined;
  while (true) {
    const page = await plugipay.ledger.list({
      code: 'revenue',
      order: 'asc',
      limit: 100,
      cursor,
    });
    for (const e of page.data) {
      if (e.postedAt > sinceIso) yield e;
    }
    if (!page.hasMore) break;
    cursor = page.cursor!;
  }
}

for await (const e of newRevenueEntries('2026-11-01T00:00:00Z')) {
  await pushToAccounting(e);
}

Trace refund impact

A refund posts a revenue_refund debit and a cash credit. To see the full impact of one refund:

async function refundImpact(refundId: string) {
  const { data } = await plugipay.ledger.list({
    sourceType: 'refund',
    sourceId: refundId,
  });
  return data.reduce(
    (acc, e) => {
      acc[e.code] = (acc[e.code] ?? 0) + (e.direction === 'credit' ? e.amount : -e.amount);
      return acc;
    },
    {} as Record<string, number>,
  );
}

Period P&L from raw entries

For a custom P&L outside what reports.pnl gives:

async function pnl(from: string, to: string) {
  const totals = { revenue: 0, refunds: 0, platformFees: 0, tax: 0 };
  let cursor: string | undefined;
  while (true) {
    const page = await plugipay.ledger.list({ order: 'asc', limit: 100, cursor });
    for (const e of page.data) {
      if (e.postedAt < from || e.postedAt >= to) continue;
      const signed = e.direction === 'credit' ? e.amount : -e.amount;
      if (e.code === 'revenue') totals.revenue += signed;
      if (e.code === 'revenue_refund') totals.refunds -= signed;
      if (e.code === 'platform_fee') totals.platformFees -= signed;
      if (e.code.startsWith('tax_')) totals.tax -= signed;
    }
    if (!page.hasMore) break;
    cursor = page.cursor!;
  }
  return totals;
}

For most workspaces the canned client.reports.pnl({ from, to }) call is faster — it runs the same aggregation server-side.

Errors

Code Status Cause
validation_error 400 Unknown code, bad sourceType, malformed txId.
forbidden 403 Key lacks plugipay:ledger:read scope.

The ledger is read-only; there's no class of state_conflict you can hit here.

Next

  • Payouts — the balance figure derived from cash.
  • Refunds — the source of revenue_refund entries.
  • Events — high-level event log (vs the ledger's accounting log).
  • API: Ledger — HTTP reference and full account-code taxonomy.
Plugipay — Payments that don't tax your success