Square Integration
Square POS integration enables independent bookstores to sync their physical inventory with BookWish.
Overview
BookWish integrates with Square to:
- OAuth Authorization: Connect bookstore Square accounts securely
- Catalog Sync: Import book items from Square catalog
- Inventory Counts: Real-time inventory levels from Square locations
- ISBN Extraction: Parse ISBNs from GTIN/UPC fields
- Webhook Verification: Validate Square webhook signatures
Implementation
Location: /backend/src/integrations/square.ts
Configuration
Required environment variables:
SQUARE_APP_ID=sq0idp-...
SQUARE_APP_SECRET=sq0csp-...
SQUARE_ENVIRONMENT=sandbox # or 'production'
SQUARE_WEBHOOK_SIGNATURE_KEY=...
The integration uses the official Square Node.js SDK and supports both sandbox and production environments.
Features
1. OAuth Authorization
Bookstore owners authorize BookWish to access their Square account.
Generate Authorization URL
import { getAuthorizationUrl } from '../integrations/square';
const authUrl = getAuthorizationUrl(
'store_123', // Store ID (used as state parameter)
'https://bookwish.io/store/square/callback' // Redirect URI
);
// Redirect user to authUrl
// User authorizes on Square and returns to callback
The authorization URL requests these scopes:
ITEMS_READ: Read catalog itemsINVENTORY_READ: Read inventory countsMERCHANT_PROFILE_READ: Read merchant info
Exchange Code for Tokens
After user authorization, exchange the code for access/refresh tokens:
import { exchangeCodeForTokens } from '../integrations/square';
const tokens = await exchangeCodeForTokens(authorizationCode);
// Returns:
// {
// accessToken: 'EAAAl...',
// refreshToken: 'EQAAl...',
// expiresAt: Date, // Typically 30 days from now
// merchantId: 'MERCHANT_ID'
// }
// Store tokens securely in database
Refresh Expired Token
Square access tokens expire after 30 days:
import { refreshAccessToken } from '../integrations/square';
const newTokens = await refreshAccessToken(refreshToken);
// Returns new accessToken and refreshToken
// Update stored tokens in database
2. Catalog Sync
Fetch all book items from Square catalog.
Fetch Catalog Items
import { fetchCatalog } from '../integrations/square';
const items = await fetchCatalog(accessToken);
// Returns array of:
// [
// {
// id: 'CATALOG_OBJECT_ID',
// name: 'The Great Gatsby',
// sku: 'BOOK-001',
// isbn: '9780743273565', // Extracted from GTIN/UPC
// variationId: 'VARIATION_ID',
// priceCents: 1599 // $15.99
// },
// // ... more items
// ]
The integration:
- Fetches all
ITEMtype objects - Extracts variations (price points)
- Attempts to extract ISBN from UPC/GTIN field
- Handles pagination automatically
ISBN Extraction
Square doesn't have dedicated ISBN fields. The integration extracts ISBNs from:
- UPC/GTIN field
- SKU field (if ISBN-formatted)
Supports:
- ISBN-13: 13 digits starting with 978 or 979
- ISBN-10: 10 digits
3. Inventory Counts
Get real-time inventory levels for catalog items.
Fetch Inventory
import { fetchInventoryCounts } from '../integrations/square';
const catalogIds = ['CATALOG_ID_1', 'CATALOG_ID_2', /* ... */];
const counts = await fetchInventoryCounts(accessToken, catalogIds);
// Returns:
// [
// {
// catalogObjectId: 'CATALOG_ID_1',
// variationId: 'VARIATION_ID_1',
// quantity: 5,
// locationId: 'LOCATION_ID'
// },
// // ... more counts
// ]
Notes:
- Only returns items with
state: 'IN_STOCK' - Supports batching up to 100 IDs per request
- Automatically handles large catalogs with batching
4. Webhook Verification
Verify that webhook requests are from Square.
Verify Signature
import { verifyWebhookSignature } from '../integrations/square';
// In webhook handler
const payload = request.rawBody.toString(); // Raw string body
const signature = request.headers['x-square-signature'];
const isValid = verifyWebhookSignature(payload, signature);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = JSON.parse(payload);
Verification uses HMAC-SHA256 with timing-safe comparison to prevent timing attacks.
Common Webhook Events
Square sends webhooks for:
inventory.count.updated: Inventory quantity changedcatalog.version.updated: Catalog items modifiedlocation.updated: Store location info changed
Data Structures
SquareTokens
interface SquareTokens {
accessToken: string;
refreshToken: string;
expiresAt: Date;
merchantId: string;
}
SquareCatalogItem
interface SquareCatalogItem {
id: string; // Catalog object ID
name: string; // Item name
sku?: string; // SKU code
isbn?: string; // Extracted ISBN
variationId: string; // Variation ID (price point)
priceCents: number; // Price in cents
}
SquareInventoryCount
interface SquareInventoryCount {
catalogObjectId: string; // Catalog object ID
variationId: string; // Variation ID
quantity: number; // Stock quantity
locationId: string; // Square location ID
}
Error Handling
Common errors and how to handle them:
try {
const catalog = await fetchCatalog(accessToken);
} catch (error) {
if (error.message.includes('OAuth')) {
// Token expired or invalid - refresh token
} else if (error.message.includes('not configured')) {
// Square credentials missing
} else {
// Other Square API error
}
}
Common Error Scenarios
- Token Expired: Refresh using
refreshAccessToken() - Invalid Credentials: Re-authorize with
getAuthorizationUrl() - Rate Limit: Square has rate limits, implement exponential backoff
- Merchant Not Found: Invalid merchant ID
Sync Strategy
Recommended approach for syncing Square inventory:
Initial Sync
- User authorizes Square via OAuth
- Store access/refresh tokens in database
- Fetch entire catalog using
fetchCatalog() - Extract catalog IDs
- Fetch inventory counts using
fetchInventoryCounts() - Store in database with mapping: Square catalog ID → BookWish inventory item
Incremental Sync
- Set up Square webhook for
inventory.count.updated - On webhook, update specific item quantities
- Set up cron job to refresh entire catalog daily (catch any missed updates)
Token Management
- Check if token expires within 7 days
- If yes, refresh proactively using
refreshAccessToken() - Store new tokens in database
- If refresh fails, notify store owner to re-authorize
Testing
Sandbox Environment
Square provides a sandbox environment for testing:
- Set
SQUARE_ENVIRONMENT=sandbox - Use sandbox credentials from Square Developer Dashboard
- Test with sandbox merchant accounts
- Sandbox data is isolated from production
Test OAuth Flow
- Navigate to authorization URL
- Sign in with Square sandbox account
- Grant permissions
- Verify tokens are exchanged successfully
Test Webhook Locally
Use Square's webhook testing tool or ngrok:
# Expose local server
ngrok http 3000
# Configure webhook URL in Square Dashboard
https://your-ngrok-url.ngrok.io/webhooks/square
Best Practices
- Token Security: Store tokens encrypted in database
- Refresh Proactively: Refresh tokens before expiration
- Handle Rate Limits: Implement exponential backoff
- Batch Inventory Calls: Use 100-item batches for efficiency
- Webhook Verification: Always verify signatures
- Error Recovery: Gracefully handle API failures
- Sync Frequency: Don't sync more than once per minute per store
Limitations
- Read-Only: Integration only reads data, doesn't write to Square
- Books Only: Designed for book retailers, may include non-book items
- ISBN Detection: Not all Square items have ISBNs
- Single Location: Multi-location stores may need additional logic
- Token Expiration: Tokens expire after 30 days
- Rate Limits: Square enforces API rate limits
Rate Limits
Square API rate limits (per merchant):
- Production: 50 requests per second
- Sandbox: 50 requests per second
- OAuth: 10 token requests per minute
Job Integration
BookWish includes a background job for automated Square syncing:
Location: /backend/src/jobs/square-sync.job.ts
This job:
- Runs on schedule (configurable interval)
- Fetches catalog and inventory for all connected stores
- Updates database with latest Square data
- Handles token refresh automatically