Pixeldev

Work/Square Sync for Woo

A WooCommerce and Square sync that has to be right, because it moves real stock and real money.

Square Sync for Woo is a plugin I build, sell and support. It keeps a WooCommerce store and a Square point of sale agreeing on products, stock, orders, customers, gift cards and loyalty, in real time and in both directions. Behind it sit two more systems I run: a webhook relay that delivers Square events to every customer site, and the licensing and update platform at squaresyncforwoo.com.

Product
Square Sync for Woo
Industry
Retail and hospitality, WooCommerce stores on Square POS
Services
Product design, plugin development, infrastructure, support
Stack
PHP, React, Node.js, Fastify, BullMQ, PostgreSQL, Redis, Next.js
Timeline
2021 to now, 416 releases
The Square Sync for Woo dashboard inside wp-admin: webhook health banner, queue and error stats, active sync features, licence and the live sync feed
The plugin dashboard: webhook health, queue depth, errors and every active sync rule, with the live sync feed alongside.
416versions released, from 1.3.0 to 10.4.21
51integration test suites run against a fake Square API
10Square webhook event types handled in real time

The problem

Plenty of businesses sell in a shop through Square and online through WooCommerce. The two systems each think they own the stock count. Sell the last bag of coffee at the counter and the website will happily sell it again unless something tells it, quickly and correctly.

The obvious fixes break in boring ways. Square webhooks go missing when a site is slow or a host blocks the request. WP cron only runs when someone visits. Square rate limits bulk work. And every sync that writes back risks an echo: a Woo edit goes to Square, Square fires a webhook, and the change comes straight back as a second update or, worse, a duplicate order.

What I built

A WordPress plugin with a React admin app inside wp-admin and a PHP backend built on Action Scheduler queues, plus the services it depends on. Merchants choose which fields each side owns, and the plugin enforces that ownership on every import, export and webhook.

  1. 1Two-way product, stock and price sync, with variations, categories, images, modifiers, custom attributes and decimal (yardage) stock.
  2. 2Order sync both ways: online orders pushed to Square with payment, POS sales imported into WooCommerce, with order splitting across locations, pickup and delivery.
  3. 3A Square payment gateway for card, Apple Pay, Google Pay, Afterpay and Cash App, plus gift cards and Square Loyalty at checkout.
  4. 4Customer sync with Square group to WordPress role mapping, and agency permissions so a developer can hand a client a restricted view.
  5. 5A sync log that records every event with its direction, the objects involved and a retry button.
  6. 6A webhook relay in Node.js (Fastify, BullMQ, PostgreSQL, Redis) that receives Square events once and delivers them to each licensed site.
  7. 7The marketing, docs, licensing and update platform: a Next.js and Turborepo monorepo with Stripe billing, a licence API, signed update manifests and tagged releases deployed by GitHub Actions.
Products screen listing the Square catalogue with variations, stock, categories and Imported, Partial or Not imported status for each item
The Square catalogue as the plugin sees it: what is linked, what is partial, and what can be imported or synced from one table.

The hard part

Most of the work is in the failures that make no noise. A stock push that quietly does nothing, a gift card charged as a $0 payment, a product imported twice by two queue workers at once. Each looks like success in a naive log. I treat every one of those as a test: 51 integration suites run inside a real WordPress install with Square faked at the HTTP layer, so they assert on exactly what would have been sent to Square, not just on what WooCommerce ended up with.

Some concrete examples. Loop guards and push markers stop an order the site created in Square from being imported back as a second order. A cross-process lock per Square item means parallel workers never create the same product twice. Square's order search is cursor-paginated with no total, so the orders screen keeps pulling pages until a filtered view is genuinely full. And when Square rejects something, the log does not just show the error code: it records a plain-English cause and fix at the moment it happens, written only for errors I have actually diagnosed.

Sync Activity log with an expanded Square VERSION_MISMATCH error, showing the raw error, a plain-English explanation and fix, and links to the product and Square object
A failed sync explained in the merchant's terms, with the cause, the fix, the linked records and a one-click retry.

Webhooks that actually arrive

Square sends each event once. If a customer's site is down, slow or behind a firewall, that update is gone, and the shop drifts out of sync with nothing to show for it. So the plugin does not take webhooks from Square directly. My relay takes them, verifies the signature, stores them in PostgreSQL and delivers them through BullMQ with a per-site circuit breaker.

When a site fails three times in a row the circuit opens and retries back off at 1, 5, 15 and 60 minutes. Held events are kept rather than dropped, and replayed oldest first (up to 500 at a time) once the site responds. A fair scheduler stops one busy merchant starving everyone else, and stores running several sites are isolated by callback URL. Inside the plugin, merchants see all of this as one plain verdict, with the controls to test, resume or discard a backlog themselves instead of opening a ticket.