A practical guide to integrating POS with Shopee, Tokopedia, and your website: a single source of stock, idempotent webhooks, stock reservations, and safe retry patterns to prevent overselling.
If you manage a pharmacy, retail store, or restaurant that also sells on Shopee, Tokopedia, and your own website, chances are you've experienced this: the cashier's stock shows 3 units, but the marketplace is still accepting the 5th order because the last sync ran 40 minutes ago. Orders get canceled, store ratings drop, and the warehouse team has to do manual stocktaking every night. Omnichannel is not just about having many sales channels — the core is one inventory data, one price, and one customer history that is consistent across all touchpoints. This article discusses how to build POS to e-commerce integration concretely: architecture, SKU mapping, stock conflict handling, to safe retry patterns, based on implementation experience in retail, pharmacy, and restaurant POS.
The first step is not writing code, but deciding who owns the data. Choose one system as the master for stock and price — generally POS or central ERP, not the marketplace. The marketplace is just a mirror. The safest scheme: the product_variants table in POS stores on-hand stock, reserved stock (orders received but not yet shipped), and available stock = on-hand − reserved. All channels read the available number, not on-hand. Without this separation, marketplace orders that haven't been picked will continue to be counted as available stock and trigger repeated overselling on the same day.
Second, never use synchronous synchronization. Marketplace API calls can take 800–2,000 ms; if the cashier waits for that response on every transaction, the queue at the counter immediately piles up. Use the incoming webhook + message queue pattern: the webhook only validates the signature and throws the job to Redis 7 + BullMQ 5 (or RabbitMQ 3.13), then a separate worker processes pulling orders and pushing stock. As a result, the webhook endpoint can respond in under 1 second and the marketplace does not flag your endpoint as slow.
Third, create an explicit mapping table: channel (shopee/tokopedia/woocommerce), channel_sku, variant_id, price_override, and buffer_stock. One POS SKU can legitimately map to many channels at once. For fast-moving products such as over-the-counter medicine or signature menu items, leave a 5–10% buffer so that stock on online channels never hits zero at the same time as a walk-in transaction. Incomplete mapping is the number one cause of unsynchronized stock, so validate upfront: target 100% of active SKUs mapped before go-live.
The proven implementation order: (1) SKU mapping and validation, (2) store credentials per channel — Shopee Open Platform API v2 uses partner_id, partner_key, and shop_id; Tokopedia uses Fulfillment API with client_id; WooCommerce uses REST API v3 with consumer key/secret, (3) build webhook endpoint, (4) build worker, (5) run scheduled reconciliation. Do not skip to step 3 before mapping is complete — debugging data is far more expensive than debugging code.
Pay attention to rate limits. Shopee Open Platform limits the number of requests per shop per minute, while WooCommerce has no built-in rate limit but will collapse on shared hosting if you send 500 parallel requests. The solution is batching: send stock updates for 100 SKUs per request, not one request per item, and limit workers to 5–10 concurrency. For a catalog of 3,000 SKUs, batching cuts push time from about 6 minutes to under 30 seconds.
app.post('/webhook/shopee/order', express.json(), async (req, res) => {
const sig = crypto.createHmac('sha256', process.env.SHOPEE_SECRET)
.update(JSON.stringify(req.body)).digest('hex');
if (sig !== req.headers['x-shopee-signature']) return res.sendStatus(401);
const key = 'order:' + req.body.order_sn;
if (!(await redis.set(key, 1, 'NX', 'EX', 86400))) return res.sendStatus(200);
await queue.add('pull-order', { orderSn: req.body.order_sn },
{ attempts: 5, backoff: { type: 'exponential', delay: 2000 } });
res.sendStatus(200); // balas < 1 detik, proses async
});
Two important things in the code above. First, HMAC SHA-256 verification ensures the payload is genuinely from the marketplace, not from someone who guessed the webhook URL. Second, SET NX EX on Redis acts as an idempotency key: marketplaces often resend the same webhook 2–4 times when the network is unstable, and without this key you risk creating duplicate orders in POS. Retrying 5 times with exponential backoff handles temporary failures, and after that the job goes into a dead letter queue for manual inspection.
When an order comes in, do not just reduce stock at the end. Perform an atomic reservation as soon as the order is received, for example UPDATE stock SET reserved = reserved + :qty WHERE variant_id = :id AND (on_hand - reserved) >= :qty. If the affected rows are zero, it means stock is out and the order must be flagged. Reservations are automatically released after 15 minutes if payment is not received, so stock is not locked by abandoned carts.
When reservation fails even though the marketplace has already accepted the order, do not auto-cancel immediately. In our pharmacy retail implementation, 2–4% of daily orders fall into this category and the majority can still be saved through product substitution or partial shipments. Create a needs decision queue that appears on the operational dashboard with a 2-hour time limit before escalation to refund.
Lastly, set up monitoring that non-technical teams can read: sync lag per channel (target under 60 seconds), number of jobs in the dead letter queue, and daily reconciliation discrepancy. Reconciliation should use three directions: POS stock, stock reported by channel, and quantity actually shipped by courier. A discrepancy of more than 2 units per SKU automatically triggers a ticket for spot-check stock opname, rather than waiting until month-end.
order_sn or event ID in Redis/DB with a TTL of at least 24 hours before processing the webhook.No. What is needed is near real-time: webhooks come in immediately, stock push completes under 60 seconds. For large catalogs, a full sync 2–4 times a day is enough, while per-transaction stock updates run event-driven.
The POS keeps running in local mode (SQLite or IndexedDB) and webhooks are captured by the cloud server, not the cash register. When the connection is restored, the job queue is processed and reconciliation closes the gap. The key: the webhook endpoint must not depend on the store's network.
Generally marketplaces with their own fulfillment rules, because order status changes several times (paid, ready to ship, shipped, delivered) and each transition triggers a different stock effect. Your own website via WooCommerce REST API v3 is actually the easiest and suitable as a baseline before adding other channels.
For one POS with 1–3 channels and a catalog under 5,000 SKUs, 4–6 weeks is enough: 1 week for data mapping, 2 weeks for integration and workers, 1 week for parallel testing, and the rest for stabilization and reconciliation.
POS and e-commerce integration is not a one-off project — it is a system that must be monitored, reconciled, and refined every week. Start with one channel, make sure SKU mapping is clean, then add the next channel after sync lag is stable under one minute. If you are preparing omnichannel for a pharmacy, clinic, retail, or restaurant and need an architecture that is secure from the start — including stock reservation, webhook idempotency, and a reconciliation dashboard — please contact Nugroho Setiawan for a technical consultation session and mapping of your system needs.
Related solutions by PT Pilar Inovasi Informatika (Pilar Inovasi IT products): PilarPOS Clinic Management System BPJS & SatuSehat Integration. Need a free consultation?
Belum ada komentar. Jadilah yang pertama!