Transactions & Ledger
Transactions & Ledger
A financial activity in Penny can be represented in several ways depending on whether you need its current state, lifecycle history, or balance impact. There’s no separate object for “funding” or “transfer”: every movement of money, from a card purchase to an account-to-account transfer, is a Transaction, distinguished by its type, channel, and counterparty.
The diagram below follows a transfer, which produces a Transaction, Transaction Event, and Ledger Entry chain on each of its two accounts. Other activity, such as a card purchase, has a single chain on one account.
Transactions
A Transaction represents the current financial and business state of an activity.
What it’s for: it’s the single record to read when you need to know what an activity represents right now (a card purchase, a transfer, funds received from a linked account) without reconstructing it from lower-level events or postings.
How it behaves:
typedistinguishespayment(a card purchase, or funds received from a linked account) fromtransfer(a movement between two of your Capital Accounts).directionisinboundoroutboundrelative to the account named on the transaction. A transfer between two of a Business Entity’s own accounts produces two transactions, oneoutboundon the source and oneinboundon the destination, not one transaction shared between them.channelrecords how it happened:card(with the card, cardholder, and network processing details),internal(a transfer between your Capital Accounts), orbank(funds received from a linked account, identified bylinked_account_id).counterpartyrecords who’s on the other side:card(a merchant and acquirer),transfer(the account on the other side, byaccount_id), orbusiness(another business, for transactions that cross business boundaries).- Transactions can be associated with contextual resources including Capital Accounts, Control Accounts, Cards, Cardholders, merchants, and Spend Controls. Not every transaction type uses every resource.
See Transactions for the full field reference.
Example: a transfer between two accounts
A transfer isn’t one record shared between both accounts. Each side gets its own Transaction, its own Transaction Events, and its own Ledger Entries, tied to the account it affected:
Account A sees an outbound Transaction, whose event posts a debit Ledger Entry; Account B sees an inbound Transaction, whose event posts a credit Ledger Entry. POST /accounts/capital/transfer returns both, as source_transaction and destination_transaction. Reading either account’s history on its own gives you the complete picture of what happened to its balance; you don’t need the other side’s records to reconcile one account.
Transaction Events
Transaction Events represent changes to a transaction over time, advancing it through its lifecycle. Each one carries an identifier, which is one of:
Penny’s own authorization decision (transaction.request.approved/transaction.request.declined) and the final approval decided by the card network or issuer (transaction.approved/transaction.declined) are separate phases. A transaction can go through both, or go straight to final approval when there’s no separate request phase. Either approval event puts the transaction in approved status.
What it’s for: processing lifecycle changes or building event-driven integrations, where you need to react as a transaction moves from one state to the next rather than only its current snapshot.
How it behaves: the transaction’s stage increases by one each time an event advances the transaction through its lifecycle. Use stage to detect out-of-order webhook delivery: if an event arrives with a stage lower than one you’ve already processed, it’s safe to ignore. GET /transactions/{transaction_id}/events lists a transaction’s events, and GET /transactions/{transaction_id}/history returns the full transaction snapshot at each stage.
Do not treat a transaction’s current state and its event history as interchangeable: one represents the current projection, while the other describes changes over time.
How events combine into a status
The transaction’s overall status isn’t one of the event identifiers above. It’s derived from the combination of events applied so far, checked in this priority order:
For example, cancelled and expired are statuses that result from applying a transaction.cancelled or transaction.expired event to an already-approved transaction.
Ledger Entries
Ledger Entries represent the financial postings that affect balances: the resulting record once a Transaction Event posts, not the event itself.
What it’s for: reconciliation and balance analysis, where you need the raw, append-only record of exactly what moved and when, rather than inferring financial postings from the current Transaction object.
How it behaves: each Transaction Event posts exactly one Ledger Entry, against the one account its Transaction belongs to. Most activity, such as a card purchase, has a single Transaction on a single account. A transfer is the exception: it produces two independent Transactions, one per account, so their separate Transaction Events each post their own Ledger Entry. The debit on the source account and the credit on the destination account are paired only in that they trace back to the same transfer; one event never splits into two entries. Ledger Entries are read per account (GET /accounts/capital/{account_id}/ledger, and the equivalent under /accounts/control/{account_id}) or across your business with GET /ledger/.
Transaction state and ledger impact are related but serve different purposes. Reconciliation should use the financial records intended for that purpose.
Resource context
A Transaction may reference additional resources that explain the activity, including the account, card, cardholder, merchant, or authorization controls involved. GET /transactions/ accepts at most one scope filter per request (account_id, card_id, cardholder_id, control_account_id, or program_id), and control_account_id can’t be combined with stage_time filters. You can also read transactions scoped directly under an account, card, or cardholder, or find them by your own reference with GET /transactions/by-client-reference.
These relationships provide context; they do not mean every transaction type uses every resource.