← Building a solo company with AI agents

Part 29 of 40 · 5 min read

Payment events, accounting entries and the meaning of “paid”

Experimental · Work in progress · Internal R&D

Provider signatures, business deduplication, partial settlement, ledger posting and reconciliation.

Arunank Sharan · Kuzushi Labs ·

In this article

A payment provider says a charge succeeded. The customer says the invoice is paid. The bank shows a smaller settlement after fees. All three statements can describe the same transaction without meaning the same thing.

The operating platform needs to preserve the relationships between the commercial obligation, payment, allocation, provider fee and bank movement. Otherwise the collections agent may chase a settled invoice or the delivery agent may release work after only a deposit. This chapter focuses on the integration mechanics; accounting policy still belongs to the business and its finance reviewer.

Start with the obligation

Give the invoice a stable identity, currency, total and amendment history. Give each allocation a separate identity linking a payment to that invoice. A payment can cover several invoices, and an invoice can receive several payments. A one-to-one assumption will eventually break.

Use integer minor units for the fixture currency and an explicit currency code. A general money library also needs each currency's exponent and rounding rules; “multiply every amount by 100” is not universal. The examples here use SGD with two decimal places.

from company_lab.business import outstanding, Review

receipts = [
    ("allocation-017", 200000, "SGD"),
    ("allocation-017", 200000, "SGD"),
]
assert outstanding(600000, "SGD", receipts) == 400000
try:
    outstanding(600000, "SGD", [("allocation-018", 200000, "USD")])
    raise AssertionError("currency mismatch must be reviewed")
except Review:
    pass

The repeated allocation is applied once. A conflicting amount under the same identity is an error to investigate, not another payment. The reference function rejects overpayment for review; a complete ledger would represent unapplied cash or customer credit explicitly.

Authenticate before interpreting

Read the raw webhook body, validate the provider signature with its official library or documented scheme, then parse and validate the event. Reconstructing JSON before signature checking can change the bytes. Keep the signing secret behind the connector and separate test and live environments.

Stripe documents duplicate deliveries and event-ordering considerations, so the consumer must not depend on one delivery in one sequence. Acknowledge after durable ingestion and process downstream work asynchronously. Stripe webhooks.

Verified provider event
  → durable inbox identity and payload digest
  → normalised payment fact
  → matched business obligation
  → allocation transaction
  → accounting posting claim
  → business event for collections and delivery

An unmatched payment belongs in reconciliation. Do not let an LLM guess the invoice from a similar customer name and post the result without a reviewable match rule.

Deduplicate at two boundaries

Provider event identity prevents replaying the same delivery. Business-effect identity prevents two different notifications about the same payment from creating two allocations. Those are separate keys.

For example, a successful payment notification and a later object update may both refer to the same payment. The posting identity could bind the payment, invoice and allocation revision. Persist the claim and the resulting ledger entry in one transaction. Inserting the claim first and crashing before the posting would otherwise make the retry look complete.

A verified provider event is matched to an obligation, allocated once and reconciled separately from provider fees and bank settlement.

Keep gross settlement and provider fees separate

If the customer pays S$2,000 and the provider remits S$1,940 after S$60 in fees, the customer's obligation may have decreased by S$2,000. The fee is a separate expense or settlement component under the chosen accounting policy. Reducing the invoice by only the net deposit would incorrectly leave S$60 to collect.

A teaching journal can show the distinction without prescribing an organisation's chart of accounts:

from company_lab.business import balanced

lines = [
    {"account": "settlement_cash", "debit": 194000, "credit": 0, "currency": "SGD"},
    {"account": "provider_fee", "debit": 6000, "credit": 0, "currency": "SGD"},
    {"account": "receivable", "debit": 0, "credit": 200000, "currency": "SGD"},
]
assert balanced(lines)

Balance is necessary but not sufficient. A perfectly balanced entry can use the wrong accounts, period or invoice. Validate business identity, permitted posting rules and supporting evidence separately.

Writes need a stable operation key

For a provider write, derive the operation identity from the reviewed business action and preserve its request digest. Reuse that identity only for the same operation. A revised amount is a new reviewed action, not permission to reuse an old key with different parameters.

Stripe's idempotency mechanism associates a key with a request result and checks parameter reuse. Its retention and response semantics are provider-specific; persist your own operation record and do not treat the provider key as a permanent business ledger. Stripe idempotent requests.

This public lab does not initiate payments. It demonstrates allocation, receipts and reconciliation using local fixtures. A live payment adapter would add account authorisation, beneficiary checks, current approvals and provider-specific recovery before any transfer capability becomes available.

Reconcile rather than hide discrepancies

Run separate checks for provider events versus payment objects, allocations versus invoices, postings versus allocations and bank settlements versus provider batches. Each comparison answers a different question. Store the discrepancy, age, owner and next action.

A reversal should create a linked compensating entry and update the business balance through a controlled transition. Do not delete the original payment and pretend it never happened. The collections loop then receives the current obligation rather than inferring it from an old “paid” email.

The acceptance fixture contains a duplicated deposit, a fee, an out-of-order provider update and a later reversal. The system should retain one original allocation, separate the fee, converge after delayed information and preserve the reversal history. That is a much stronger payment integration than a green webhook response.