A technical guide to unifying POS and e-commerce: single source of truth for inventory, atomic reservations, order synchronization, and payment reconciliation.
Stock going negative in-store while the marketplace still accepts orders is a classic symptom of POS and e-commerce running separately. The warehouse team updates stock manually twice a day, the marketplace admin checks one by one, and during a flash sale one SKU is sold twice on different channels. For a pharmacy with 1,200 SKUs and three sales channels (cashier, WhatsApp orders, Shopee), a stock discrepancy of just 0.5% means 6 items mis-sold per day and 2–3 hours of manual reconciliation every week. Omnichannel integration is not merely connecting APIs; it is deciding who holds the truth of stock data, how reservations are locked, and how failures are handled. This article discusses practical steps, including code patterns we use in pharmacy, retail, and restaurant POS projects in Yogyakarta and Magelang. All examples can be directly adapted to your stack.
1. Determine the Single Source of Truth Before Touching the API
The most expensive mistake in omnichannel integration is letting two systems both write stock. Choose one single source of truth (SSOT). For retail with a single store warehouse, POS is the SSOT because it changes fastest. For a distributor with 4 warehouses, the ERP (e.g., Odoo 17 or Accurate Online) becomes the SSOT, and POS only reads a local cache. E-commerce should never hold the authoritative stock figure — it only stores a copy for display and notifies the platform when stock changes.
Architecturally, use an event-driven pattern. POS/ERP publishes events stock.changed and order.created to a message broker (Redis 7.2 + BullMQ 5 or RabbitMQ 3.13). A separate worker consumes the events and pushes them to external channels: WooCommerce REST API v3, Shopee Open Platform v2, Tokopedia API, or TikTok Shop Partner Center. Do not call the marketplace API directly from within the cashier transaction — a single 30-second timeout will block the checkout queue at the front.
The e-commerce side also needs a reverse flow. Orders from the marketplace come in via webhook, not polling. Polling every 1 minute for 500 orders/hour does look safe, but you only get data after the order is created, not when the customer clicks pay — the oversell gap remains. Webhooks give 1–3 second latency and save API rate limit quota.
Finally, create an explicit SKU mapping table (sku_mapping) containing pos_sku, channel, channel_sku, and conversion_factor for products sold per box on the marketplace but per piece at the cashier. Without this table, the integration will fail on the first product that has different packaging.
2. Stock Synchronization: Atomic Reservations, Not Just Updates
Reducing the stock figure when an order comes in is not enough. The correct approach is to reserve: stock is reduced from available, moved to reserved, and automatically returned if payment fails within 15 minutes. This pattern prevents oversell without having to hold stock permanently. In PostgreSQL 16, perform the deduction in a single statement with the clause WHERE available >= qty so it is safe from race conditions between workers.
// Node.js 20 LTS + Express 4.19 — reservasi stok atomik & idempotent
app.post('/webhook/order', async (req, res) => {
const { order_id, sku, qty } = req.body;
await db.tx(async (t) => {
const r = await t.query(
`UPDATE stok SET available = available - $1
WHERE sku = $2 AND available >= $1 RETURNING available`, [qty, sku]);
if (!r.rowCount) throw new Error('STOK_KURANG');
await t.query(`INSERT INTO reservasi(order_id,sku,qty,expires_at)
VALUES ($1,$2,$3, now() + interval '15 minutes')
ON CONFLICT (order_id) DO NOTHING`, [order_id, sku, qty]);
});
res.json({ status: 'reserved' });
});
Note the ON CONFLICT (order_id) DO NOTHING. Marketplaces often send the same webhook 2–5 times on timeout. Without an idempotency key, one order can reserve stock multiple times. Also store the X-Request-Id from the header for auditing.
For large catalogs, do not push the entire stock to the marketplace. Send only changed SKUs (delta sync). For a retail client with 8,000 SKUs and 3 channels, full sync every 5 minutes consumes 24,000 API calls per hour and immediately hits Shopee's rate limit. Delta sync reduces it to 200–400 calls/hour.
Add a safety buffer of 2–3 units for fast-moving SKUs. Its purpose is to absorb the difference between the time a customer clicks pay on the marketplace and the time the webhook arrives. For a restaurant selling 300 portions/day, this buffer eliminates almost all complaints of "order cancelled because out of stock".
3. Order, Payment, and Daily Reconciliation Sync
The flow we use: incoming order webhook → validate stock & reserve → save local order with status pending → call payment gateway (Midtrans Snap or Xendit Invoice) → after status settlement, push order to POS/ERP for picking → send tracking number back to marketplace. Each step has its own status so failures can be retried without duplicating orders.
Use exponential backoff with 5 attempts (2s, 8s, 30s, 2m, 10m) then throw to a dead-letter queue. In BullMQ 5, this is configured simply via the attempts option and backoff: { type: 'exponential', delay: 2000 }. Permanently failed orders appear on the operational dashboard, not silently lost in logs.
Reconciliation is the part most often forgotten. Create a daily job at 02:00 that compares three sources: total POS transactions, total channel orders, and total payment gateway settlements. The difference must be zero; if not, output the list of unmatched orders along with their status. Our client in Magelang cut book-closing time from 6 hours to 40 minutes after this automated reconciliation was in place.
For cross-channel reporting, store a single table fact_sales with columns channel, order_id, gross, discount, fee_channel, and net. Marketplace commission fees (Shopee 6–8%, TikTok 5–8%) often make margins look larger than they are; include them as a separate column so profit reports per channel are accurate.
Best Practices
- Establish a single SSOT for stock and forbid other channels from writing stock figures directly.
- Always use time-bound reservations (10–15 minutes) plus a reservation-release job that runs every minute.
- Require an idempotency key on all webhook endpoints and store request logs for at least 30 days.
- Separate the synchronization worker from the cashier transaction process; use a queue with retry and a dead-letter queue.
- Run automated reconciliation daily and send a discrepancy summary to operational email/WhatsApp.
FAQ
How long does POS–e-commerce integration take?
For one channel (WooCommerce or Shopee) and one POS, 3–5 effective work weeks are enough to cover SKU mapping, stock reservations, order synchronization, and reconciliation. Add 2 weeks for each subsequent channel, especially because the marketplace API approval process takes time.
Do I have to use middleware like n8n or can it be direct?
For 1–2 channels and volume below 300 orders/day, self-hosted n8n is sufficient. Above that, or if you need guaranteed ordering and precise retry, use a custom worker with BullMQ or RabbitMQ plus a transactional database.
How do you handle products whose stock is shared between branches?
Separate stock per location and create explicit allocation rules, for example prioritize the branch closest to the customer. Avoid merging all branches' stock into a single number on the marketplace because the risk of overselling at one location remains high.
What happens if the marketplace is down during synchronization?
Orders and events are still stored in the local queue. Once the API recovers, the worker reprocesses them based on time order. That is the main reason integrations should use a queue, not direct API calls inside transactions.
A well-organized omnichannel integration is not a one-off project — it needs a clear SSOT, safe stock reservations, and reconciliation that runs by itself every day. If you are preparing a POS, ERP, or e-commerce website that must talk to each other, our team helps from architecture audit to implementation and operational support. We provide IT consulting and application development for SIMRS, SIM Klinik, ERP, POS, and websites for clients in Yogyakarta (DIY) and Magelang, Central Java. Contact Nugroho Setiawan for an initial consultation session and mapping of your system needs.
This article is part of the knowledge base of Nugroho Setiawan, an IT consultant in Jogja (Yogyakarta). Related solutions: PilarPOS Clinic Management System BPJS & SatuSehat Integration. Need a free consultation?
Komentar
Belum ada komentar. Jadilah yang pertama!
Saya membantu konsultasi IT & pengembangan aplikasi untuk rumah sakit, klinik, dan bisnis di Yogyakarta (DIY), Magelang, dan sekitarnya.