Integrating a point-of-sale system looks simple until duplicates, refunds, and timezone drift turn your revenue dashboard into fiction. Here is how to integrate Square, Clover, Toast, and Shopify so the numbers are actually right.
Two ways in: polling vs webhooks
Every POS offers two paths. Polling (pull orders since a cursor on a schedule) is simple and resilient: miss a run and the next catches up. Webhooks (the POS pushes each event) are real-time but lossy: a missed delivery is gone. The production answer is usually both, webhooks for freshness plus a nightly poll to backfill anything the webhooks dropped.
The reconciliation trap
The mistake that corrupts every naive integration: treating each webhook as a new sale. Events arrive twice, out of order, and replayed. If you INSERT on every event you double-count revenue. Dedupe on the POS order ID and make ingestion idempotent:
const claimed = await db.prepare(
'INSERT OR IGNORE INTO pos_orders (source, external_id, total_cents, ts) VALUES (?,?,?,?)'
).bind('square', order.id, order.total, order.created_at).run();
if (claimed.meta.changes === 0) return; // already ingested, never double-count
Refunds, voids, and partials break your totals
A sale is not final. Refunds, voids, comps, and partial returns arrive after the original order, often as separate events. If you only ingest sales, revenue is overstated forever. Model the order as a running state (net = charges minus refunds minus voids), apply adjustments by external ID, and never assume the first event is the last word.
Money is integers; timezones are lies
Store every amount in minor units (cents) as integers: floating-point dollars drift and your books will not tie out. Normalize every timestamp to UTC on ingest, because POS systems report in the store local time, so “todays sales” silently shifts by hours (and across a day boundary) if you do not. These two bugs make an owner stop trusting the dashboard.
Per-provider quirks
Square and Clover expose clean order APIs with webhooks and polling. Toast is restaurant-shaped (checks, items, voids, tips) and needs partner API access. Shopify blends online and in-store orders, so filter by source if you only want POS. Wrap each behind one normalized internal order shape so the rest of your app never cares which POS it came from.
Verify against the source of truth
Once a day, pull each provider daily total and compare it to your ingested sum. If they diverge, alert: a silent drift means a dropped event or a double-count, and you want to know before the owner does. Reconciliation is the difference between a number you can bank on and a number you hope is right.
The rule: dual-path ingest, dedupe by external ID, model adjustments not just sales, integers and UTC, reconcile daily. Frabo ingests Square and Clover on exactly this pattern, because the boring guarantees are what make “here is your real revenue” a sentence you can trust.
