
Pos Integration
- 82 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Connect a physical point-of-sale system to your online store for unified inventory, shared customer records, and omnichannel orders.
About
Integrates a physical POS with the online store to unify inventory, share customer records, and manage omnichannel orders. A developer uses it to keep in-store and online sales in sync.
- Unified inventory and shared customer records across POS and online
- Omnichannel order management
Pos Integration by the numbers
- 82 all-time installs (skills.sh)
- Ranked #3,038 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill pos-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 82 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Connect a physical point-of-sale system to your online store for unified inventory, shared customer records, and omnichannel orders.
Files
POS Integration
Overview
Point-of-sale integration connects your physical retail operations with your online store, enabling unified inventory visibility, centralized order management, and consistent customer profiles across channels. When a customer buys in-store, the online store must reflect the updated inventory immediately; when they return an online order in-store, the refund must flow back to the payment gateway. This skill covers connecting Square and Shopify POS to your commerce platform, synchronizing inventory in real time, and handling cross-channel returns.
When to Use This Skill
- When opening a physical retail location alongside an existing online store
- When inventory discrepancies between in-store and online are causing oversells
- When customers expect to return online purchases in-store (omnichannel returns)
- When franchised or multi-location retail needs unified inventory and reporting
- When building a custom kiosk or tablet-based POS application
Core Instructions
Step 1: Determine your platform and POS integration path
| Platform | Easiest POS Integration | What Gets Unified |
|---|---|---|
| Shopify | Shopify POS (built-in, $89/month for Pro) — designed to work with Shopify's online store natively | Inventory syncs instantly between POS and online store; orders appear in unified admin; customer profiles and loyalty points unified |
| WooCommerce | Square + WooCommerce Square plugin (free) | Inventory syncs bidirectionally; Square POS sales update WooCommerce stock; orders can be imported into WooCommerce |
| BigCommerce | Square + BigCommerce Square integration | Same as WooCommerce but configured in BigCommerce's channel settings; BigCommerce also supports Stripe Terminal directly |
| Custom / Headless | Square API or Stripe Terminal | Full control — sync inventory via webhooks, import POS orders via API, build unified order dashboard |
Step 2: Platform-specific POS setup
---
Shopify + Shopify POS
Shopify POS is the most seamless option for Shopify stores since inventory is managed in a single system:
1. Subscribe to Shopify POS Pro ($89/month or included in Shopify Plus):
- Go to Point of Sale → Devices in your Shopify admin
- Install the Shopify POS app on your iPad or iPhone
- Connect a card reader (Shopify provides its own Tap & Chip reader)
2. Set up locations for each store:
- Go to Settings → Locations and click Add location for each physical store
- Assign inventory quantities per location — Shopify tracks stock at each location separately
3. Inventory automatically stays in sync:
- A sale in the POS app immediately decrements inventory for that location
- The online store can be configured to sell from multiple locations (go to Settings → Shipping and delivery → Local pickup to configure)
4. Configure click-and-collect (Buy Online, Pick Up In Store):
- Go to Settings → Shipping and delivery → Local pickup and enable it for each location
- Customers select their pickup location at checkout; the order appears in that store's POS app under Pickups
---
WooCommerce + Square
Connect Square to WooCommerce:
1. Install the WooCommerce Square plugin (free, developed by Square) from wordpress.org 2. Go to WooCommerce → Settings → Integrations → Square and click Connect with Square 3. Log in to your Square account and select your Square business location 4. In the Square plugin settings, enable Sync inventory (bidirectional) 5. Run the initial sync: go to WooCommerce → System Status → Tools → Sync Products with Square
How inventory sync works:
- When you sell a product in your Square POS, WooCommerce stock decrements automatically (within minutes via webhook)
- When WooCommerce receives an online order, Square stock decrements
- Go to WooCommerce → Square → Sync Log to see sync history and troubleshoot discrepancies
Handle in-store returns of online orders:
- Returns initiated in WooCommerce admin automatically sync to Square if the original payment used Square
- For Stripe-paid online orders returned in-store: process the refund in Square and reconcile in WooCommerce manually, or build a custom refund endpoint (see Custom section below)
---
BigCommerce + Square
1. Go to Channel Manager in your BigCommerce admin and click + Create New Channel 2. Select Square from the channel list 3. Authorize BigCommerce to connect to your Square account 4. Configure inventory sync settings — BigCommerce and Square will sync stock levels bidirectionally 5. Square POS orders can be imported into BigCommerce for unified reporting
---
Custom / Headless + Square API
Initialize the Square client:
// lib/pos/square-client.ts
import { Client, Environment } from 'square';
export const squareClient = new Client({
accessToken: process.env.SQUARE_ACCESS_TOKEN!,
environment: process.env.NODE_ENV === 'production' ? Environment.Production : Environment.Sandbox,
});
export const { catalogApi, inventoryApi, ordersApi, paymentsApi, locationsApi } = squareClient;Handle Square inventory webhooks (in-store sale → update online store):
// POST /api/webhooks/square
import { createHmac } from 'node:crypto';
export async function POST(req: NextRequest) {
const rawBody = Buffer.from(await req.arrayBuffer());
const signature = req.headers.get('x-square-hmacsha256-signature') ?? '';
// Square HMAC includes the full notification URL in the signature
const notificationUrl = `${process.env.APP_URL}/api/webhooks/square`;
const expected = createHmac('sha256', process.env.SQUARE_WEBHOOK_SECRET!)
.update(notificationUrl + rawBody.toString('utf8'))
.digest('base64');
if (signature !== expected) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON.parse(rawBody.toString('utf8'));
if (event.type === 'inventory.count.updated') {
const counts = event.data.object.inventory_counts ?? [];
for (const count of counts) {
const variant = await db.variants.findBySquareCatalogId(count.catalog_object_id);
if (!variant) continue;
await db.inventory.updateLocationQuantity(variant.sku, count.location_id, parseInt(count.quantity));
const total = await db.inventory.getTotalAvailable(variant.sku);
await redis.setex(`inventory:${variant.productId}`, 3600, String(total));
}
}
return NextResponse.json({ accepted: true });
}Handle cross-channel returns (online order returned in-store):
// lib/pos/returns.ts
export async function processInStoreReturn(params: {
orderId: string;
lineItems: Array<{ lineItemId: string; quantity: number }>;
locationId: string;
}) {
const { orderId, lineItems, locationId } = params;
const order = await db.orders.findById(orderId);
if (!order) throw new Error(`Order ${orderId} not found`);
// Calculate refund amount
const refundAmount = lineItems.reduce((sum, item) => {
const line = order.lineItems.find(l => l.id === item.lineItemId)!;
return sum + (line.unitPriceCents * item.quantity);
}, 0);
// Issue refund via original payment gateway
if (order.paymentGateway === 'stripe') {
await stripe.refunds.create({
payment_intent: order.stripePaymentIntentId,
amount: refundAmount,
metadata: { orderId, locationId, channel: 'in_store_return' },
});
} else if (order.paymentGateway === 'square') {
await paymentsApi.createPaymentRefund({
idempotencyKey: `return_${orderId}_${Date.now()}`,
paymentId: order.squarePaymentId!,
amountMoney: { amount: BigInt(refundAmount), currency: 'USD' },
});
}
// Restock returned items at the return location
for (const item of lineItems) {
const line = order.lineItems.find(l => l.id === item.lineItemId)!;
await db.inventory.incrementLocationQuantity(line.sku, locationId, item.quantity);
}
await db.orders.addReturn({ orderId, lineItems, processedAtLocation: locationId, processedAt: new Date() });
}Sync your product catalog to Square (required before Square can track inventory):
export async function syncProductToSquare(product: Product) {
const { result } = await catalogApi.batchUpsertCatalogObjects({
idempotencyKey: `sync_${product.sku}_${Date.now()}`,
batches: [{
objects: [{
type: 'ITEM',
id: `#item_${product.sku}`,
itemData: {
name: product.name,
variations: product.variants.map(variant => ({
type: 'ITEM_VARIATION' as const,
id: `#var_${variant.sku}`,
itemVariationData: {
itemId: `#item_${product.sku}`,
name: variant.name,
sku: variant.sku,
pricingType: 'FIXED_PRICING',
priceMoney: { amount: BigInt(Math.round(variant.price * 100)), currency: 'USD' },
trackInventory: true,
},
})),
},
}],
}],
});
// Store Square-assigned IDs for future inventory updates
for (const mapping of result.idMappings ?? []) {
if (mapping.clientObjectId?.startsWith('#var_')) {
const sku = mapping.clientObjectId.replace('#var_', '');
await db.variants.update(sku, { squareCatalogVariationId: mapping.objectId });
}
}
}Best Practices
- For Shopify merchants, use Shopify POS — it shares a single inventory system with your online store; no sync lag, no duplicate records, and unified reporting out of the box
- Reserve a safety stock buffer for the online store — never expose 100% of physical inventory online; reserve 10–20% as a buffer for in-store sales that happen faster than inventory sync can propagate
- Implement location-aware inventory — track stock per physical location (store), not just as a global total; this enables accurate click-and-collect availability
- Monitor sync lag — measure the time between a POS sale and the inventory update appearing online; alert when lag exceeds 5 minutes
- Build a manual reconciliation tool — inventory discrepancies happen; store managers need a UI to compare POS inventory counts with your system and trigger a manual sync
Common Pitfalls
| Problem | Solution |
|---|---|
| WooCommerce Square sync stops working | Re-authenticate the Square connection in WooCommerce → Settings → Integrations → Square — OAuth tokens expire; the plugin usually prompts for re-auth |
| Inventory oversell due to sync lag | Set safety stock buffer in your marketplace/POS settings; for high-demand items, do a final inventory check at checkout against Square's inventory API |
| Square catalog IDs diverge from your SKUs | Maintain a mapping table between your internal SKU and Square's catalog variation IDs; rebuild it from Square's catalog if it gets out of sync |
| Cross-channel return creates duplicate inventory | Only restock inventory when the return is physically received in-store (status: received), not when the return is initiated |
| Shopify POS not showing online orders for pickup | Ensure the customer selected Pick up in store at checkout and that the correct location is assigned to the POS device in Settings → Locations |
Related Skills
- @marketplace-connectors
- @webhook-architecture
- @flash-sale-scaling
- @monitoring-alerting-commerce
{
"context": "Tests whether the agent implements cross-channel return processing correctly: routing refunds to the original payment gateway, validating returnable quantities, restocking at the correct location only when physically received, and using idempotency keys.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Original gateway routing",
"max_score": 12,
"description": "Refund is issued through the same payment gateway that processed the original order (e.g. Stripe if order.paymentGateway === 'stripe', Square if 'square') — not a single hardcoded gateway"
},
{
"name": "Returnable quantity check",
"max_score": 10,
"description": "Validates that the requested return quantity does not exceed the returnable/eligible quantity on the order line before processing the refund"
},
{
"name": "Already-refunded guard",
"max_score": 8,
"description": "Checks that the order has not already been fully refunded before initiating a new return"
},
{
"name": "Restock on receipt only",
"max_score": 14,
"description": "Inventory is incremented only when the return status is 'received' (physically received in-store) — NOT immediately on return initiation"
},
{
"name": "Location-specific restock",
"max_score": 10,
"description": "Inventory is restocked at the specific locationId where the return was physically processed, not at a global or default location"
},
{
"name": "Online inventory resync after restock",
"max_score": 8,
"description": "After incrementing location inventory, the updated total available quantity is synced to the online store (e.g. calls syncInventoryAcrossChannels or equivalent)"
},
{
"name": "Idempotency key for refund",
"max_score": 8,
"description": "Passes an idempotencyKey when calling the Square refund API (paymentsApi.createPaymentRefund or equivalent)"
},
{
"name": "Refund amount calculation",
"max_score": 8,
"description": "Calculates the refund amount in minor currency units (cents) based on unit price times quantity for each returned line item"
},
{
"name": "Return record persisted",
"max_score": 8,
"description": "Saves a return/refund record to the database that includes the line items returned, refunded amount, gateway refund ID, and the location where it was processed"
},
{
"name": "Line item validation",
"max_score": 6,
"description": "Verifies that each line item being returned actually exists on the original order before processing"
},
{
"name": "Order not-found error",
"max_score": 8,
"description": "Throws or returns an error when the order is not found in the database"
}
]
}
Omnichannel Returns: Buy Online, Return In Any Store
Problem/Feature Description
Redwood Home Goods runs 12 physical stores and a flagship online shop. Their current returns policy only allows online purchases to be returned by mail — but competitor analysis shows that 60% of customers prefer to return items in-person. The operations team has decided to roll out a "return anywhere" policy: any online order can be returned at any physical store location, with a refund issued immediately back to the customer's original payment method.
The technical challenge is that online orders were paid through either Stripe or Square depending on which payment gateway was active when the order was placed. When a store associate processes a return, the system needs to find the original order, validate what can still be returned, process the refund through the correct channel, and update inventory counts at the specific store where the goods were physically handed back. The operations team is also mindful of the impact incorrect inventory counts have on the online availability displayed to shoppers — the timing of when inventory changes are reflected online matters.
Output Specification
Produce the following files:
lib/pos/returns.ts— the main return processing module exporting aprocessInStoreReturnfunctionreturns.test.ts— unit tests (or a test script) covering at least: the already-refunded guard, the returnable quantity check, the gateway routing decision, and the inventory update behavior
The processInStoreReturn function should accept:
{
orderId: string;
lineItems: Array<{lineItemId: string; quantity: number; reason: string; status: string}>;
locationId: string;
}Assume the following are already implemented and can be imported/referenced:
db.orders.findById(orderId)— returns an order with.status,.paymentGateway,.stripePaymentIntentId,.squarePaymentId, and.lineItems(each with.id,.sku,.unitPriceCents,.returnableQuantity)db.orders.addReturn({orderId, lineItems, gatewayRefundId, processedAt, processedAtLocation})db.inventory.incrementLocationQuantity(sku, locationId, qty)db.inventory.getTotalAvailable(sku)syncInventoryAcrossChannels({sku, quantity, source})stripe— Stripe SDK client instancepaymentsApi— Square payments API client
{
"context": "Tests whether the agent correctly implements a Square POS catalog sync module, using the right Square client package, environment configuration, catalog object structure, and handling of Square-assigned IDs.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Square npm package",
"max_score": 8,
"description": "Imports Client and/or Environment from the 'square' package (not a different Square SDK or HTTP client wrapper)"
},
{
"name": "Environment switching",
"max_score": 8,
"description": "Sets Square environment to Environment.Production when NODE_ENV is 'production' and Environment.Sandbox otherwise"
},
{
"name": "Description truncation",
"max_score": 10,
"description": "Truncates catalog item description to 500 characters (e.g. substring(0, 500) or slice(0, 500))"
},
{
"name": "Hash-prefix new IDs",
"max_score": 10,
"description": "Uses a '#' prefix on catalog object IDs when constructing new CatalogObject payloads (e.g. '#item_...' or '#var_...')"
},
{
"name": "Batch upsert method",
"max_score": 10,
"description": "Calls catalogApi.batchUpsertCatalogObjects (not individual upsertCatalogObject per item)"
},
{
"name": "Idempotency key",
"max_score": 8,
"description": "Passes an idempotencyKey to batchUpsertCatalogObjects (not absent or empty)"
},
{
"name": "ID mapping persistence",
"max_score": 12,
"description": "Reads result.idMappings from the batchUpsertCatalogObjects response and stores the Square-assigned variation IDs back to the internal SKU or product record"
},
{
"name": "Track inventory flag",
"max_score": 8,
"description": "Sets trackInventory: true on item variations in the CatalogObject payload"
},
{
"name": "Fixed pricing type",
"max_score": 8,
"description": "Uses pricingType: 'FIXED_PRICING' on variations (not VARIABLE_PRICING)"
},
{
"name": "Price in minor units",
"max_score": 9,
"description": "Converts price to minor currency units (cents) for priceMoney.amount (e.g. multiplies by 100)"
},
{
"name": "Catalog as sync target",
"max_score": 9,
"description": "Syncs FROM the commerce/PIM catalog TO Square (not the reverse direction — Square is populated from the existing product data)"
}
]
}
Brick-and-Mortar Expansion: Square POS Catalog Setup
Problem/Feature Description
A fashion retailer called Varda Apparel has been running a successful online store for three years. They are now opening two physical boutique locations and have chosen Square as their point-of-sale system. Their existing product database contains hundreds of SKUs with names, descriptions, variants (size/color combinations), and prices. Before they can start selling in-store, every product needs to be available in Square so that cashiers can ring up sales, and so that in-store inventory movements are trackable per location.
The engineering team needs a TypeScript module that reads from the Varda product database and pushes the catalog into Square. The integration must be production-safe: it should work against Square's sandbox during development and switch to the live environment automatically when deployed. Catalog syncs will be triggered both on product updates and on a nightly batch job, so the implementation must be safe to run repeatedly without creating duplicate catalog entries or payments.
Output Specification
Produce a TypeScript file named catalog-sync.ts that exports:
- A
syncProductToSquare(product: Product)function that syncs a single product and its variants to Square - A
Square clientsetup (may be in a separatesquare-client.tsfile if you prefer)
Also produce a brief DESIGN.md file (max 300 words) explaining: 1. How new vs. existing catalog objects are identified 2. How Square-assigned IDs are stored back into the product database after the first sync
The following type definitions are provided as the starting data model. Extract them before beginning.
=============== FILE: inputs/types.ts =============== export interface Product { sku: string; name: string; description?: string; variants: Variant[]; }
export interface Variant { sku: string; name: string; price: number; // in dollars, e.g. 29.99 squareCatalogVariationId?: string; // populated after first sync }
{
"context": "Tests whether the agent correctly builds a Square webhook endpoint that verifies signatures using the right HMAC construction, handles the correct event types, and updates inventory per location with safety stock logic.",
"type": "weighted_checklist",
"checklist": [
{
"name": "HMAC-SHA256 verification",
"max_score": 10,
"description": "Uses createHmac('sha256', ...) with the Square webhook secret to verify the incoming signature"
},
{
"name": "Full URL in HMAC input",
"max_score": 12,
"description": "The HMAC is computed over the full notification URL concatenated with the raw body (not just the body alone, and not just the path)"
},
{
"name": "Raw body for verification",
"max_score": 8,
"description": "Reads the raw request body (Buffer/bytes) before any JSON parsing, and uses the raw bytes for signature verification"
},
{
"name": "401 on invalid signature",
"max_score": 8,
"description": "Returns HTTP 401 (or equivalent rejection) when the computed signature does not match the x-square-hmacsha256-signature header"
},
{
"name": "inventory.count.updated handler",
"max_score": 8,
"description": "Handles the 'inventory.count.updated' event type and triggers an inventory update"
},
{
"name": "catalog.version.updated handler",
"max_score": 6,
"description": "Handles the 'catalog.version.updated' event type"
},
{
"name": "payment.completed handler",
"max_score": 6,
"description": "Handles the 'payment.completed' event type"
},
{
"name": "refund.created handler",
"max_score": 6,
"description": "Handles the 'refund.created' event type"
},
{
"name": "Per-location inventory update",
"max_score": 10,
"description": "Updates inventory quantity at the specific location_id (locationId) from the inventory count event, not as a single global quantity"
},
{
"name": "Safety stock deduction",
"max_score": 10,
"description": "The total available quantity exposed online is less than 100% of total physical stock — either by applying a percentage reduction or subtracting a buffer amount"
},
{
"name": "SKU lookup via mapping",
"max_score": 8,
"description": "Looks up the internal SKU/variant by the Square catalog_object_id (variation ID) from the event — does NOT assume catalog_object_id equals the SKU"
},
{
"name": "Accepted response",
"max_score": 8,
"description": "Returns a successful 200 response with an accepted acknowledgement when the event is processed"
}
]
}
Real-Time Inventory Sync: Square POS Webhook Handler
Problem/Feature Description
Pinnacle Sports is a multi-location sporting goods retailer with an online store built on Next.js and several physical locations running Square POS terminals. When a staff member rings up a sale at any store, the online store needs to reflect the updated stock immediately — customers have been completing purchases online for items that were already sold in-store, leading to oversells and angry order cancellations.
The team wants to build a webhook endpoint that Square will call whenever inventory changes, payments complete, or catalog updates occur. Security is critical: Square signs every webhook delivery, and the handler must reject any unsigned or tampered requests. The online store should never expose the full physical stock count directly — the visible quantity should account for the fact that in-store sales can happen faster than the sync propagates.
The endpoint will be deployed as a Next.js App Router API route at /api/webhooks/square.
Output Specification
Produce the following files:
app/api/webhooks/square/route.ts— the Next.js App Router POST handlerlib/pos/inventory-handler.ts— the inventory update logic called from the webhook (may be inlined into route.ts if you prefer)
Also produce a NOTES.md file (max 200 words) that explains: 1. How the signature verification works and why it is constructed the way it is 2. How online-visible inventory is calculated from the raw location counts
Assume the following are already implemented (do not redefine them, just import/reference them):
db.variants.findBySquareCatalogId(variationId)— returns a variant object with.skudb.inventory.updateLocationQuantity(sku, locationId, qty)— sets qty for one locationdb.inventory.getTotalAvailable(sku)— returns total available (you must implement the safety stock logic here or in the webhook handler)syncInventoryAcrossChannels({sku, quantity, source})— pushes the quantity to the online storesyncCatalogFromSquare()— re-fetches catalog from SquarehandleSquarePayment(obj)andhandleSquareRefund(obj)— stubs you may reference
{
"name": "finsi/pos-integration",
"version": "0.1.0",
"summary": "Point-of-sale integration with online inventory and unified order management",
"skills": {
"pos-integration": {
"path": "SKILL.md"
}
}
}