Storefronts and marketplaces
Post each order to the books exactly once
An order is paid on the storefront, and some minutes later an invoice should appear in the ledger. The difficulty is not the posting. It is that the same order can arrive twice: Shopify retries a webhook it believes failed, and a run that crashed halfway does not know whether the invoice landed. A second invoice is not a display bug — it overstates revenue, and running the step again does not take it back. This is the point where a workflow tool stops being enough.
- Triggers
- Two
- Runs
- Every 15 minutes
- Stack
- 5 tools
- Durable execution
- Required
The flow
The top track records the sale in a table you own, and writes nothing to the ledger. The bottom track posts on a timer, and refuses to post an order it has already claimed.
An order is paid
Shopify Admin API
Build the journal line
n8n · Make
Write the posting key
Sheets · Airtable
Every 15 minutes
n8n schedule
List what is unposted
n8n
Missing from the ledger?
Temporal · Inngest
Post the invoice
Xero API
Leave it alone
—
Step by step
- 01
An order is paid
Shopify Admin APIA webhook fires on payment. Treat it as a signal that an order exists, not as permission to post — the same delivery can arrive again, and Shopify says so in its own documentation.
- 02
Build the journal line
n8n · MakeTurn the order into the fields your ledger expects: net, tax, platform fee, payout account. Nothing is written here. The step is pure computation, so re-running it costs nothing — which is exactly why it belongs in the workflow tool.
- 03
Write the posting key
Sheets · AirtableOne row per order, keyed on the order id, with a posting state. This row — not the webhook — decides whether an order still needs posting. Claim before you write, confirm after.
- 04
Every 15 minutes
n8n scheduleRuns regardless of any webhook. A delivery that never arrived leaves nothing to notice, and month-end is a poor time to discover it.
- 05
List what is unposted
n8nRead rows that were claimed but never confirmed as posted. That set includes runs which died between the ledger write and the confirmation — the case a workflow tool loses quietly.
- 06
Missing from the ledger?
Temporal · InngestLook the order id up in the ledger before writing, and hold the lookup and the write inside one run that can be retried whole. This is the step n8n and Make cannot own: neither records that the write already succeeded, so a retry after a lost response re-posts the invoice and books the revenue twice. Either durable execution, or an idempotency key the ledger itself enforces.
- 07
Post the invoice
Xero APIWrite the invoice with the order id in the invoice reference, and send the same stable order key in Xero's Idempotency-Key header. The reference is for lookup and reconciliation; the header is the part Xero treats as the retry guard. If another ledger does not enforce one, the reference plus the check above has to live inside the same durable run.
What the platform will not allow
None of the caution above is taste. Each part answers a behaviour the platforms document themselves.
Webhook delivery "isn't always guaranteed", and Shopify "doesn't guarantee ordering within a topic, or across different topics for the same resource". The same page tells you to ignore duplicate deliveries using the X-Shopify-Webhook-Id header — a header that exists because the same order event does arrive more than once.
Shopify — About webhooks (best practices)Shopify allows one second to connect and five seconds for the whole request, then retries eight times over four hours; after eight consecutive failures an Admin API subscription is deleted automatically. The documentation's own advice is to queue and respond first — so a ledger write held inside the webhook handler will time out, and the timeout buys you a second delivery of the same order.
Shopify — Deliver webhooks through HTTPSXero documents client-supplied idempotency for mutating POST, PUT and PATCH requests through the Idempotency-Key HTTP header. The key is per app, capped at 128 characters, and the cached response is for the same exact request for six minutes.
Xero API — Idempotent requestsThe Xero invoice model exposes a Reference field, so the storefront order id can be written onto the invoice for lookup and reconciliation; that field is not the idempotency key itself.
Xero Accounting API — InvoicesWhat "the receiving system enforces it" looks like, in a system that documents it: Stripe saves the status code and body of the first request for a given key, and subsequent requests with the same key "return the same result, including 500 errors". Keys run to 255 characters, may be pruned once "at least 24 hours old", and reusing one with different parameters is an error rather than a second write. If your ledger has no equivalent, the guard falls back to your own key table inside a durable run.
Stripe API reference — Idempotent requestsThe GraphQL Admin API is metered by calculated query cost rather than request count: 100 points per second on standard plans, and no single query may exceed 1,000 points. A month-end catch-up that re-reads a large order set is paced by that budget, not by how fast the workflow loops.
Shopify — API rate limits
When this is not worth building
Three cases where the upkeep costs more than the double posts.
You post one daily total by hand and your accountant is content with it. A summarised journal entry is a single line a day; automating per order buys precision nobody asked for, and introduces a system that can double-post.
Your ledger has no external reference field to key on. With nowhere to record the order id on the invoice, the check in this flow has nothing to look up, and you are trusting your own table to be right on its own.
Nobody reconciles. If no one compares the ledger against the payout report at month end, a double post survives, and the automation has only made the wrong figure arrive sooner.