Square: in-store sales
A sale rung up in the Square app (Tap to Pay on a phone, a card reader, or cash) becomes a SupplySynk order and takes stock out of the right location, usually within seconds. Stock changed in SupplySynk (receiving, counts, online orders) is pushed back to Square so Square never sells stock you don't have. SupplySynk stays the source of truth for the catalog and stock (D-2, D-3).
Requirements: POS-1 … POS-7 in product/requirements.md.
How it works
| Direction | What | When |
|---|---|---|
| Square → SupplySynk | Completed sale → one order (channel: square, paid, fulfilled), stock out of the mapped location | Webhook payment.created / payment.updated, or order.updated when the Square order becomes COMPLETED (whichever comes first) |
| Square → SupplySynk | Refund with returned items → items restocked on the original order | Webhook refund.created / refund.updated when COMPLETED |
| Square → SupplySynk | Payment (card, cash, …, tips, fees, refunds) → daily takings | Every payment event; "Refresh" re-reads a date range |
| SupplySynk → Square | On-hand count of each changed, linked product → every Square location mapped to that location | Right after the change, plus a full sync daily and on demand |
Exactly once. Events are stored by Square's event_id (a replay changes
nothing). A Square order maps to at most one SupplySynk order (unique index on
the Square order id), and a refund restocks at most once (square_refunds),
however many events report it. The order, its items and the stock movement
commit in one transaction.
Failures. Every event lands in the sync log (Settings → Square, or
GET /api/v1/square/events). If Square is briefly unavailable, the webhook
answers 500 and Square delivers again; the daily job retries failures from the
last 7 days; anyone with write:orders can retry from the log. Problems only
the owner can fix (a Square location not linked to a SupplySynk location)
answer 200 and wait in the log; retry after fixing.
Rules worth knowing
- A sale is recorded even if SupplySynk shows too little stock (it already happened at the till); stock can go negative and shows up for a count.
- Lines for items not linked to a product (custom amounts, unlinked items) are noted in the log; the order total still includes them.
- Split payments: the order is recorded when Square marks it fully paid.
- Amount-only refunds (no items returned) don't change stock.
- Stock changes that came from Square are not pushed back (Square already has them); the daily full sync reconciles everything.
- Square counts are on-hand quantities; negative stock is sent as 0.
- Daily takings come from Square's payments, so they include unitemized sales and tips; refunds count on the day of the original payment.
Setup (once per environment)
-
Square app. In the Square Developer Dashboard create an application (one app serves every business). Use the Sandbox tab for testing, Production for real stores.
-
OAuth. Under OAuth, set the redirect URL to
<NEXT_PUBLIC_APP_URL>/api/integrations/square/callback. -
Webhooks. Under Webhooks → Subscriptions, add an endpoint
<NEXT_PUBLIC_APP_URL>/api/integrations/square/webhookwith eventspayment.created,payment.updated,order.updated,refund.created,refund.updated,oauth.authorization.revoked. Copy its signature key. -
Environment variables (Vercel → Settings → Environment Variables, and
.env.local):Variable Value SQUARE_ENVIRONMENTsandboxorproductionSQUARE_APPLICATION_IDApplication ID ( sq0idp-…/sandbox-sq0idb-…)SQUARE_APPLICATION_SECRETApplication secret (OAuth page) SQUARE_WEBHOOK_SIGNATURE_KEYSignature key of the webhook subscription SQUARE_WEBHOOK_URLOptional: the exact notification URL, if it differs from the default above INTEGRATIONS_ENCRYPTION_KEYOptional: 32 random bytes, base64 ( openssl rand -base64 32). Encrypts Square tokens at rest; defaults to a key derived fromBETTER_AUTH_SECRET. Changing it means businesses reconnect. -
Database: migration
0010_square(docs/migrations/09-apply-0010.sql).
Square checks the signature against the exact notification URL, so the URL in
the subscription and SQUARE_WEBHOOK_URL (or the default) must match
character for character. Local testing needs a public URL (e.g. a tunnel).
For the business owner
Settings → Square:
- Connect Square (owner or admin). Square asks you to approve access; you come back to SupplySynk.
- Locations. Each Square location sells from a SupplySynk location. All start on your default location; change them if needed.
- Link products. SupplySynk suggests matches by SKU, then barcode. Link them, import items that exist only in Square, or create SupplySynk-only products in Square. Choose whose stock counts to start from.
- Ring up a sale in Square. It appears in the sync log and under Orders.
Disconnecting revokes Square's access. Links, the sync log and sales history are kept, so reconnecting the same Square account picks up where you left off.
Square online checkout (D-21)
The same connection can take payment for online store orders: in
Settings → Payments, Use my Square connection. A checkout becomes a
Square-hosted payment link (Checkout API CreatePaymentLink) for an order on
the shop's Square account, at the Square location linked to the pickup
location; the money is the shop's. Square's payment.updated (COMPLETED) for
that order arrives on the webhook above and confirms the online checkout
instead of being recorded as an in-store sale (matched by the Square order id
stored as the checkout's provider reference); the matching order.updated is
skipped, and refunds of it are noted on the online order. The stock was
already held for pickup, so nothing is taken out twice.
Online checkout needs two more scopes, ORDERS_WRITE and PAYMENTS_WRITE.
They are asked for only when a shop wants it (incremental re-authorisation:
Settings → Payments → Allow online payments in Square, i.e.
/api/integrations/square/connect?online=1), and kept on later reconnects
while Square online checkout is set up. Shops that only sync in-store sales
never grant them. Your Square account's currency must match the
business currency.
API and MCP
Everything after the one-time connection is available to apps and agents:
REST (/api/v1) | MCP tool | Scope |
|---|---|---|
GET /square | get_square_status | read:locations |
DELETE /square | — (dashboard or REST only) | manage:integrations |
PATCH /square/locations | map_square_locations | manage:integrations |
GET /square/catalog | review_square_catalog | write:products |
POST /square/catalog | link_square_catalog | write:products |
GET /square/events | get_square_sync_log | read:orders |
POST /square/events/{id}/retry | retry_square_event | write:orders |
POST /square/stock-sync | sync_square_stock | write:inventory |
GET /square/sales-summary | get_square_sales_summary | read:reports |
POST /square/sales-summary/refresh | refresh_square_payments | write:orders |
Square sales are ordinary orders (channel: "square", channelOrderId = the
Square order id), so GET /api/v1/orders, order.created webhooks and
Insights include them. Request and response schemas: GET /api/v1/openapi.json.
Code map
| Piece | Where |
|---|---|
| Square REST client, OAuth, config | src/lib/integrations/square/ |
| Token encryption, webhook signature, OAuth state | src/lib/integrations/square/crypto.ts |
| Connect / disconnect / tokens / locations | src/lib/services/square/connection.ts |
| Sales, refunds, sync log | src/lib/services/square/sales.ts |
| Stock push | src/lib/services/square/stock.ts (scheduled from runStockTransaction) |
| Catalog link | src/lib/services/square/catalog.ts |
| Daily takings | src/lib/services/square/summary.ts |
| OAuth and webhook routes | src/app/api/integrations/square/ |
| Daily jobs (token refresh, retries, full sync) | src/app/api/cron/maintenance/route.ts |
| Tests (fake Square API) | src/test/square.integration.test.ts |