Skip to main content

POS Integrations

SalesArck currently supports Square and Clover.

Both providers follow the same shape:

  1. OAuth helpers build and sign the authorization state.
  2. The OAuth callback stores encrypted tokens in pos_connections.
  3. Webhook payloads are stored in pos_webhook_events_raw.
  4. The ingestion pipeline normalizes provider data into NormalizedTransaction.
  5. Wallet credit happens through the shared ingestion core.

Square​

OAuth​

Merchant connect starts from POST /api/v1/client/tenants/:tenantId/pos-connections/square/connect.

The callback at GET /api/v1/oauth/square/callback:

  • verifies the signed state payload
  • exchanges the code for tokens
  • encrypts access and refresh tokens at rest
  • upserts the pos_connections row for (tenant_id, pos_type='square')
  • redirects back to the frontend

Webhooks​

POST /api/v1/webhooks/square:

  1. Verifies the Square signature when a webhook key is configured.
  2. Resolves the tenant by merchant_id and the active Square connection.
  3. Stores the raw payload.
  4. Inserts a webhook_jobs row and returns 200.
  5. The same process then ingests that job, which calls processSquareWebhook(), credits points, and sends the email. POST /api/v1/internal/cron/process-pending-webhooks retries jobs that did not finish.

Poll fallback​

POST /api/v1/internal/cron/square-poll-payments polls Square ListPayments for every active Square connection.

The poll path:

  • refreshes the access token when needed
  • paginates up to 25 pages per connection
  • reuses the same normalization and ingestion core as the webhook path
  • updates square_payments_watermark_at and last_synced_at

Clover​

OAuth​

Merchant connect starts from POST /api/v1/client/tenants/:tenantId/pos-connections/clover/connect.

The callback at GET /api/v1/oauth/clover/callback:

  • verifies the signed state payload
  • exchanges the code for tokens
  • encrypts the access token at rest
  • stores a null refresh token, because Clover does not provide one
  • upserts the pos_connections row for (tenant_id, pos_type='clover')
  • redirects back to the frontend

Webhooks​

POST /api/v1/webhooks/clover:

  1. Handles Clover's verification handshake payload.
  2. Verifies the webhook signature for real notification events.
  3. Rejects payloads that contain multiple merchant IDs.
  4. Resolves the tenant by merchant ID and active Clover connection.
  5. Stores the raw payload.
  6. Inserts a webhook_jobs row and returns 200.
  7. The same process then ingests that job, which calls processCloverWebhook(), credits points, and sends the email. POST /api/v1/internal/cron/process-pending-webhooks retries jobs that did not finish.

Clover webhooks are notification-only. The processor must fetch each payment from Clover's REST API before normalization.

Recovery​

POST /api/v1/internal/cron/clover-poll-payments lists Clover payments modified since clover_payments_watermark_at for every active Clover connection and runs them through the same idempotent ingestion path as webhooks.

A payment that is still settling holds the watermark so the next poll fetches it again. A future-dated timestamp cannot move the watermark past now.

Failed webhook jobs are retried by POST /api/v1/internal/cron/process-pending-webhooks, which claims due webhook_jobs rows. The webhook handler already runs the same job as soon as it is stored. Square and Clover polls stay the fallback when a webhook never arrives.

Adapters​

Square adapter​

normalizeSquarePaymentRecord() accepts webhook or REST payment shapes and keeps only completed payments.

It extracts:

  • provider transaction ID
  • amount in minor units
  • currency
  • transaction timestamp
  • optional contact data

Clover adapter​

normalizeCloverPayment() accepts a fetched Clover payment record and keeps only payments eligible for loyalty credit.

It extracts:

  • provider transaction ID
  • amount in minor units
  • currency
  • transaction timestamp
  • optional contact data from the payment or embedded customer records

Connection Semantics​

pos_connections enforces one row per (tenant_id, pos_type).

That means reconnecting updates the same row in place, and disconnecting only flips status to disconnected.

Environment Variables​

Square​

  • SQUARE_APP_ID
  • SQUARE_APP_SECRET
  • SQUARE_ENVIRONMENT
  • SQUARE_WEBHOOK_SIGNATURE_KEY

Clover​

  • CLOVER_APP_ID
  • CLOVER_APP_SECRET
  • CLOVER_ENVIRONMENT
  • CLOVER_WEBHOOK_SECRET

Shared​

  • ENCRYPTION_KEY
  • API_BASE_URL
  • FRONTEND_URL
Written byDhruv Doshi