SHOPIFY → XERO
Killing double entry on every order
Every online order that lands in Shopify shouldn't need a human to re-type it into Xero. Here's exactly how we'd make orders become invoices on their own.
The problem
It's the end of the day and someone on your team opens Shopify, then opens Xero, and starts copying. Order number, customer name, line items, GST, shipping. Then the next order. Then the next. It's nobody's favourite hour, and it's the hour where a transposed number quietly becomes a wrong invoice that you find out about at BAS time.
For a store doing even 30–40 orders a day, that's a job in itself — and it's a job that scales with your success, which is the wrong way round. The busier you get, the more of someone's day disappears into re-keying data that already exists, perfectly structured, one tab over.
The cost isn't just the time. It's the typos, the orders that get missed at the end of a long day, and the fact that your books are always a day or two behind reality.
What "fixed" looks like
An order comes in. Before anyone's looked at it, the matching invoice already exists in Xero — right customer, right line items, GST handled, marked paid and ready to reconcile against your next Shopify payout. Nobody copied anything. The end-of-day re-keying hour simply stops existing, and your books are current to the minute.
How we'd connect it
Shopify can tell you the instant something happens. We subscribe to its orders/create webhook, verify the message is genuinely from Shopify, translate the order into Xero's shape, and write it straight into your accounts.
Two pieces matter here, and they're the two that cheap "connectors" get wrong: proving the request actually came from Shopify, and mapping GST and payments correctly so your accountant isn't cleaning up after the robot.
1. Verify the webhook
Anyone can POST to a public URL. Shopify signs every webhook with an HMAC so you can prove it's legitimate — we check it with a timing-safe comparison before trusting a single byte.
import crypto from "node:crypto"; export function verifyShopify(rawBody, hmacHeader, secret){ const digest = crypto .createHmac("sha256", secret) .update(rawBody, "utf8") .digest("base64"); // timing-safe — never a plain === on a signature return crypto.timingSafeEqual( Buffer.from(digest), Buffer.from(hmacHeader) ); }
2. Map the order to a Xero invoice
This is where the real work lives. Each Shopify line item becomes a Xero line item, with the right account code and the right GST tax type. We add shipping as its own line, attach (or create) the customer contact, and stamp the Shopify order number on the invoice reference so the two systems can always be matched back up.
function toXeroInvoice(order){ return { Type: "ACCREC", Contact: { Name: order.customer?.default_address?.name ?? order.email }, Reference: `Shopify #` + order.order_number, Date: order.created_at.slice(0, 10), LineAmountTypes: "Inclusive", // Shopify prices include GST LineItems: [ ...order.line_items.map(li => ({ Description: li.title, Quantity: li.quantity, UnitAmount: Number(li.price), AccountCode: "200", // sales TaxType: li.taxable ? "OUTPUT" : "EXEMPTOUTPUT" })), ...shippingLine(order) // freight as its own line ] }; }
Then it's one authenticated call to Xero's Accounting API to create the invoice, and — if the order's already paid in Shopify — a second call to record the payment against a clearing account, so it's sitting there ready to reconcile.
The edge cases that bite
A demo that handles the happy path is easy. The reason a "$49/mo connector" makes your bookkeeper unhappy is everything below — this is the part we actually get paid for.
- Duplicate webhooks. Shopify can and will send the same event twice, and it retries on any non-200 response. Every order is processed idempotently — we check for an existing invoice by reference before creating, so a retry never doubles your revenue.
- GST done properly. Tax-inclusive pricing, GST-free items, and the right Xero tax types — so your activity statement is right the first time, not reverse-engineered later.
- Refunds and partial refunds. A refund in Shopify becomes a credit note in Xero, not a manual adjustment you have to remember to make.
- Payments and payouts. Orders post against a clearing account that reconciles cleanly against the lumped Shopify/Stripe payout — instead of a thousand tiny unmatched transactions.
- Rate limits. Xero allows 60 calls a minute. On a busy sale day we queue and back off on 429s rather than dropping orders on the floor.
- When Xero is down. Failed pushes land in a retry queue with alerting — nothing is silently lost, and you find out from us, not from your accountant.
So why not just use an app?
You can, and for a lot of stores you should. A2X, Amaka and Link My Books all push Shopify sales into Xero, and they're cheap. The honest answer to "isn't there an app for this?" is: the app does the generic 80%. We get called when the last 20% is the part that's actually your business.
- Posts a daily summary or per-order invoice on a fixed template
- Handles standard GST and one straightforward payout reconciliation
- Maps to the account codes it supports, the way it supports them
- Great when your setup looks like everyone else's
- GST-free lines, mixed tax, bundles or your own product logic
- A supplier's weird CSV, a second sales channel, or a marketplace in the mix
- Refunds and partial refunds that have to land as the right credit note
- It "almost" works and your bookkeeper is fixing it by hand every month
What it'd take for you
A clean Shopify → Xero build like this typically lands in the $5k–$10k range and takes a week or two, depending on how unusual your GST, product, and payment setup is. You'd watch it run on real orders before it goes anywhere near your live books.
Sound like your end-of-day hour?
tell us what's not talking to what · 20-min fit call
Book a fit call