Skip to content
FeaturesPricingAffiliateBlogHelpAboutContact
Get StartedSign In
Back to Blog
guides2026-07-277 min read

Two-Way Square POS Sync With thMenu: Webhooks + REST Polling Hybrid

A Brooklyn cafe case study: Square catalog.version.updated webhooks with a 30-minute polling fallback, tax mapping, and rate-limit-aware batching.

th

thMenu Team

thmenu.com

If you run a 32-seat specialty cafe in Brooklyn using both Square Terminal and a thMenu QR menu, keeping prices in sync across two systems can turn into an operational nightmare. The barista just changed the latte from 5.50 to 5.75 USD, but the customer's QR menu still shows the old price. This article walks through a hybrid architecture that trusts webhooks first, but falls back to polling whenever the network blinks.

Webhook-driven primary sync

Square's catalog.version.updated event fires whenever any item, variation, or modifier changes. Route this webhook to a thMenu Worker endpoint at /api/integrations/square/webhook: verify the HMAC-SHA256 signature (Square sends an x-square-hmacsha256-signature header), parse object_id, then fetch details with /v2/catalog/object/{id}.

The Worker maps the response into thMenu's D1_MENU schema: Square's variations[0].price_money.amount (in cents, e.g. 575) becomes products.price = 5.75 USD. A square_object_id column lives in D1 so future webhooks upsert instead of duplicate. Square's published SLA delivers 99.2% of webhooks within 15 minutes.

Polling fallback and rate limits

For scenarios like a network outage, a brief Cloudflare hiccup, or a rare Square delay (sometimes 5+ minutes), a 30-minute cron polling job acts as a second line of defense. The cron queries /v2/catalog/search with an updated_at filter for the last 35 minutes; anything the webhook missed is reconciled here.

Square's API rate limit is 10 requests per second. For a 500-item cafe this is plenty, but batching matters: the cron pulls 100 items per batch_retrieve call and then sleeps 1 second. A Worker queue plus setTimeout(1000) avoids 429 errors entirely.

Tax mapping: US sales tax vs. TR VAT

The Brooklyn location applies 8.875% NY sales tax; if the same operator has an Istanbul shop, that one charges 10% VAT on food & beverage. Add tax_us_pct and tax_tr_pct columns to products; at render time, restaurant.country picks the right one.

  • Square's Locations API exposes a separate tax rate per location — match by location_id during sync.
  • Show a locale-aware disclaimer ("Tax not included" / "KDV dahildir") on the customer menu.
  • If Stripe Tax is active, extend the mapping to Stripe tax codes (txcd_*).

FAQ

What if webhooks fail entirely? The 30-minute polling cron catches drift within half an hour. Customers may see stale prices in that window — usually an acceptable trade-off.

Does this also sync inventory? No, this covers catalog (price/name) only. Inventory uses a separate Square API and is not yet wired into thMenu.

Which plan unlocks it? Square integration is available on Pro and Platinum. Starter restaurants enter products manually.

Found this helpful? Share it.