Manually copying orders from your store into your accounting software is slow, error-prone, and unnecessary. But automating it badly is worse. Here's how to do it properly.
- The store thinks in orders; the ledger thinks in invoices, tax codes, and payments. The integration is a set of deliberate translation decisions, not a pipe.
- Reconciliation is the part that matters and the part cheap integrations skip — payouts are batched and net of fees, so sales, payments, and bank deposits must be tied together explicitly.
- Make every sync idempotent and fail loudly. Duplicate sales quietly inflate revenue; a silent stall is discovered at month-end.
Every online store owner eventually hits the same wall. Orders come in through the storefront, and somehow they have to end up in the accounting system — as sales, with the right tax, matched to the right payments, ready for the books to balance. In the early days someone does this by hand: reads the orders, types them into the accounts, reconciles the payments. It works, until volume grows and it becomes a daily grind that is both tedious and quietly error-prone.
So people automate it. And this is where it goes one of two ways. Done well, the integration disappears into the background and your books simply stay correct. Done badly, it creates a new class of problem — duplicated orders, mismatched totals, tax applied wrong, a month-end reconciliation that no longer adds up and nobody can explain. The gap between those outcomes is entirely in the details, and the details are where I want to spend this.
Two systems that model the world differently
People imagine this integration as a simple pipe: an order appears in the store, the same order appears in the accounts. If it were that simple it would rarely go wrong. It is not, because the two systems think about the world differently, and the real work is translating between them.
A store thinks in orders, customers, products, and the moment of purchase. An accounting system thinks in invoices, tax codes, ledger accounts, payments, and reconciliation. Getting an order from one to the other means answering a series of questions with no universal right answer.
STORE TRANSLATION LAYER LEDGER ┌──────────┐ order ┌───────────────────┐ invoice ┌──────────┐ │ order │ ──────────▶ │ • when = a sale? │ ───────▶ │ invoice │ │ line │ │ • account mapping │ │ tax code │ │ items │ │ • tax treatment │ │ ledger │ │ discount │ │ • fees & payouts │ │ payment │ └──────────┘ └───────────────────┘ └──────────┘
- When does an order become a sale in the books — at purchase, at payment, at dispatch?
- How does each product map to the right account and the right tax treatment?
- What happens to discounts, shipping, and gift cards, each of which the accounting side treats differently?
- How do refunds and partial refunds flow back through?
- How do the payments your provider actually deposits — often batched, often net of fees — get matched against individual sales?
None of these is exotic, but every one is a decision. A good integration is really a set of these decisions made deliberately and consistently. A bad one is these decisions made by accident, differently each time, until the numbers stop agreeing.
Get the tax and the accounts right first
Before automating anything, the mapping has to be correct, because automation faithfully repeats whatever you tell it — including your mistakes, at speed. The right shape is an explicit table from what the store sells to how the ledger records it: account, tax code, and any special handling.
// A deliberate, reviewable mapping — the heart of a correct integration.
type LedgerLine = { account: string; taxCode: string };
const productMapping: Record<string, LedgerLine> = {
ebook: { account: "4000-sales-digital", taxCode: "STANDARD" },
physical: { account: "4010-sales-goods", taxCode: "STANDARD" },
book: { account: "4020-sales-books", taxCode: "ZERO" }, // zero-rated
gift_card: { account: "2200-deferred", taxCode: "OUTSIDE" }, // not a sale yet
};
function toLedgerLine(product: { taxClass: string }): LedgerLine {
const line = productMapping[product.taxClass];
if (!line) throw new Error(`Unmapped tax class: ${product.taxClass}`); // fail, never guess
return line;
}Tax is where this bites hardest. Different products may carry different treatments; sales to different places may be taxed differently; some items are exempt or zero-rated. If the mapping is wrong, an automated integration produces wrong tax on every single order, cleanly and consistently, until someone catches it — often not until a return is due. Getting this right is unglamorous and fiddly and absolutely worth doing up front, ideally with your accountant in the room. It is far cheaper to map it correctly once than to unpick months of miscoded transactions later.
Reconciliation is the part that actually matters
Here is the detail that separates an integration that looks like it works from one that genuinely does: the money has to reconcile.
Your customers pay through a payment provider. That provider does not hand you each payment individually the moment it happens. It typically batches them and deposits a lump sum into your bank, often after deducting its fees. So the accounting system must connect three things that do not line up on their own: the sales recorded from the store, the payments the provider processed, and the actual amounts that arrived in the bank.
sales in ledger provider payout (one bank deposit)
┌───────────────┐ ┌──────────────────────────────┐
│ order A 4000 │ │ gross A+B+C = 13 000 │
│ order B 5000 │ → │ less fees − 390 │
│ order C 4000 │ │ ───────────────────────── │
└───────────────┘ │ net deposit = 12 610 │
total = 13 000 └──────────────────────────────┘
the deposit ≠ the sales; the fee must be booked as an expenseAn integration that records the sales but ignores this reconciliation leaves you with books that look busy but do not balance against reality. The deposits do not obviously match the sales, because of batching and fees, and someone still reconciles it by hand — which was the whole thing you were escaping. A properly built one books each payout as a group of sales plus the provider fee as an expense, so what is in the books reconciles cleanly against what is in the bank.
// Book a provider payout: the deposit, the sales it covers, and the fee.
async function recordPayout(payout: {
id: string; net: number; fee: number; chargeIds: string[];
}) {
await ledger.transaction(async (tx) => {
for (const chargeId of payout.chargeIds) {
await tx.settleInvoiceForCharge(chargeId); // mark each sale as paid
}
await tx.postExpense("6100-payment-fees", payout.fee); // fees are a real cost
await tx.postBankDeposit(payout.net); // matches the statement line
});
}This is the hardest part to get right and the part most cheap integrations quietly skip — which is exactly why so many of them leave you no better off than before.
Build for the ways it will go wrong
Connecting two live systems means depending on things outside your control, and those things fail. An integration you can trust is built around that reality. The core ideas are the same ones that make any integration reliable, and they matter just as much when money is involved.
Make every sync safe to repeat. You will sometimes not know whether an order made it across — a connection drops at the wrong moment and you cannot tell. The only safe response is to try again, and that is only safe if trying again cannot create a duplicate. Each order must carry a stable key derived from the order itself, so the accounting side recognises "I already have this one" and ignores the repeat.
async function syncOrder(order: Order) {
// Key derived from OUR domain, stable across every retry of this same order.
const externalId = `store-order:${order.id}`;
const existing = await ledger.findInvoiceByExternalId(externalId);
if (existing) return existing; // already synced — a safe no-op, not a duplicate
return ledger.createInvoice({ externalId, lines: order.items.map(toLine) });
}Without that, retries create duplicate sales, and duplicate sales quietly inflate revenue and wreck reconciliation.
Retry sensibly, then escalate. A sync that fails for a temporary reason should retry, waiting a little longer each time rather than hammering a struggling system. But some failures will not fix themselves, and those must stop retrying and raise a flag. An order stuck in limbo is a problem; an order stuck in limbo that nobody has been told about is a disaster waiting for month-end.
Fail loudly, never silently. The worst outcome is the integration that stops quietly. Orders stop flowing, nothing crashes, no alarm sounds, and you discover a fortnight's gap when you try to close the books. A trustworthy integration tells someone the moment orders stop arriving as expected — not "is the service running" but "are orders actually still flowing into the accounts." An alert that fires when expected activity goes missing catches the silent stall that ordinary error messages do not.
Keep a record you can query. When something goes wrong — and eventually it will — you need to answer "what happened to this specific order?" in seconds, not reconstruct it from fragments over an afternoon. Logging each order's journey from store to accounts is what makes the whole thing operable when it misbehaves.
Don't automate a broken process
One last caution, because it is the most common way this goes wrong before a single line of integration is written. If your product-to-account mapping is a mess, or your tax setup is unclear, or your accounting is already disorganised, connecting your store to it just distributes the disorder faster and at higher volume. Automation is an amplifier. Point it at a clean, well-understood process and it saves enormous effort. Point it at a muddle and it makes the muddle bigger, quicker, and harder to untangle.
So the honest first step is often not building the integration at all, but getting the accounting side clean and the mappings correct and agreed. That groundwork is unglamorous, and it is tempting to skip straight to the automation. Resist it. The businesses that get real value from connecting store to accounts are the ones that did the boring preparation first.
What doing it right buys you
When this integration is built properly, something genuinely good happens: nothing. Orders flow into the accounts, correctly categorised and correctly taxed. Payouts reconcile against the bank without anyone matching them by hand. The books stay current and correct with no daily grind. And when something occasionally goes wrong, you find out immediately, you can see exactly what happened, and you fix it before it touches the month-end.
That quiet correctness is the entire point. You stop spending hours on data entry and reconciliation, you stop worrying whether the numbers are right, and you get back the time you were pouring into moving figures between systems. For a growing store, reclaiming that — reliably, correctly, without creating a new set of problems — is worth far more than the integration costs to build well.