All help

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

DirectionWhatWhen
Square → SupplySynkCompleted sale → one order (channel: square, paid, fulfilled), stock out of the mapped locationWebhook payment.created / payment.updated, or order.updated when the Square order becomes COMPLETED (whichever comes first)
Square → SupplySynkRefund with returned items → items restocked on the original orderWebhook refund.created / refund.updated when COMPLETED
Square → SupplySynkPayment (card, cash, …, tips, fees, refunds) → daily takingsEvery payment event; "Refresh" re-reads a date range
SupplySynk → SquareOn-hand count of each changed, linked product → every Square location mapped to that locationRight 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)

  1. 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.

  2. OAuth. Under OAuth, set the redirect URL to <NEXT_PUBLIC_APP_URL>/api/integrations/square/callback.

  3. Webhooks. Under Webhooks → Subscriptions, add an endpoint <NEXT_PUBLIC_APP_URL>/api/integrations/square/webhook with events payment.created, payment.updated, order.updated, refund.created, refund.updated, oauth.authorization.revoked. Copy its signature key.

  4. Environment variables (Vercel → Settings → Environment Variables, and .env.local):

    VariableValue
    SQUARE_ENVIRONMENTsandbox or production
    SQUARE_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 from BETTER_AUTH_SECRET. Changing it means businesses reconnect.
  5. 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:

  1. Connect Square (owner or admin). Square asks you to approve access; you come back to SupplySynk.
  2. Locations. Each Square location sells from a SupplySynk location. All start on your default location; change them if needed.
  3. 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.
  4. 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 toolScope
GET /squareget_square_statusread:locations
DELETE /square— (dashboard or REST only)manage:integrations
PATCH /square/locationsmap_square_locationsmanage:integrations
GET /square/catalogreview_square_catalogwrite:products
POST /square/cataloglink_square_catalogwrite:products
GET /square/eventsget_square_sync_logread:orders
POST /square/events/{id}/retryretry_square_eventwrite:orders
POST /square/stock-syncsync_square_stockwrite:inventory
GET /square/sales-summaryget_square_sales_summaryread:reports
POST /square/sales-summary/refreshrefresh_square_paymentswrite: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

PieceWhere
Square REST client, OAuth, configsrc/lib/integrations/square/
Token encryption, webhook signature, OAuth statesrc/lib/integrations/square/crypto.ts
Connect / disconnect / tokens / locationssrc/lib/services/square/connection.ts
Sales, refunds, sync logsrc/lib/services/square/sales.ts
Stock pushsrc/lib/services/square/stock.ts (scheduled from runStockTransaction)
Catalog linksrc/lib/services/square/catalog.ts
Daily takingssrc/lib/services/square/summary.ts
OAuth and webhook routessrc/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