POS Integrations
Connect your point-of-sale system to Tabsy in minutes. After each transaction your POS sends one HTTP request to Tabsy — the shopper's digital receipt appears in their vault instantly.
How it works
Every integration follows the same pattern regardless of your POS:
Payment is accepted and the sale is finalised.
Square, Shopify, and Lightspeed all emit a webhook event you can subscribe to. Custom systems can call Tabsy directly.
Translate your POS fields (items, total, customer ID) into the Tabsy receipt format shown below.
One HTTP call. Receipt lands in the shopper's vault in under 500 ms.
Customer identification
Tabsy matches each receipt to a shopper account using one of three identifiers. Pass whichever you have — Tabsy tries them in this order:
| Method | Field | Notes |
|---|---|---|
| Member code Recommended | identifier.member_code | Shopper shows their Tabsy QR at checkout. Most reliable — guaranteed unique match. |
identifier.email | Use the email on file in your POS. Works if the shopper signed up to Tabsy with the same email. | |
| Phone | identifier.phone | E.164 format preferred (e.g. +971501234567). Falls back to email if both provided. |
If no match is found the receipt is held for 30 days and linked automatically if the shopper signs up later.
◼
Square
Subscribe to Square's payment.created webhook. Because Square's webhook payload is lightweight, the handler re-fetches the full payment to get the order_id, then fetches the order for line items and the shopper's member code.
order_id, so the order fetch always returns 404. Use the real Square POS app (on a device in sandbox mode) to trigger a proper end-to-end test.
Step 1 — Subscribe to the webhook
In the Square Developer Dashboard go to your app → Webhooks → Add endpoint. Subscribe to the payment.created event and set the URL to your Tabsy webhook endpoint — no server required, it runs on Supabase:
Copy the Signature key shown after saving — you'll need it in Step 2.
Step 2 — How the member code is passed
When a shopper presents their Tabsy QR at checkout, the cashier types the member code (e.g. A7K2M9) into the Square POS Order Note or Reference ID field using the format TABSY:A7K2M9. The handler extracts it automatically. If no code is present it falls back to the customer's email address.
Step 3 — Field mapping
| Source | Tabsy field | Notes |
|---|---|---|
payment.id (re-fetched) | external_id | Idempotency key — prevents duplicate receipts. |
payment.amount_money.amount ÷ 100 | total | Square stores amounts in the smallest currency unit. |
payment.amount_money.currency | currency | Already ISO 4217. |
payment.created_at | receipt_date | ISO 8601 — pass through directly. |
order.reference_id → parse TABSY:CODE | identifier.member_code | Most reliable match. Set by cashier at checkout. |
payment.buyer_email_address | identifier.email | Fallback if no member code present. |
order.line_items[].name | items[].name | Fetched via separate Orders API call. |
order.line_items[].base_price_money.amount ÷ 100 | items[].price | |
order.line_items[].quantity | items[].qty |
Step 4 — Webhook handler
const express = require('express'); const crypto = require('crypto'); const app = express(); app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })); const SQUARE_SIGNATURE_KEY = process.env.SQUARE_SIGNATURE_KEY; const SQUARE_ACCESS_TOKEN = process.env.SQUARE_ACCESS_TOKEN; const TABSY_API_KEY = process.env.TABSY_API_KEY; const SQUARE_BASE = 'https://connect.squareup.com'; // use squareupsandbox.com for sandbox const TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt'; // Extract TABSY:CODE from an order note or reference_id function extractMemberCode(str) { const m = (str || '').match(/TABSY:([A-Z0-9]{4,12})/i); return m ? m[1].toUpperCase() : null; } app.post('/webhooks/square', async (req, res) => { res.sendStatus(200); // acknowledge immediately // Verify Square HMAC signature const proto = req.headers['x-forwarded-proto'] || req.protocol; const host = req.headers['x-forwarded-host'] || req.get('host'); const url = `${proto}://${host}${req.originalUrl}`; const expected = crypto.createHmac('sha256', SQUARE_SIGNATURE_KEY) .update(url + (req.rawBody || '')).digest('base64'); const received = req.headers['x-square-hmacsha256-signature'] || ''; if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) return; const event = req.body; if (event.type !== 'payment.created') return; // Re-fetch full payment — webhook payload omits order_id const payId = event?.data?.object?.payment?.id; const payRes = await fetch(`${SQUARE_BASE}/v2/payments/${payId}`, { headers: { 'Authorization': `Bearer ${SQUARE_ACCESS_TOKEN}`, 'Square-Version': '2024-01-18' } }); const payment = (await payRes.json()).payment; if (!payment) return; // Fetch order for line items and member code let items = [], memberCode = null; if (payment.order_id) { const ordRes = await fetch(`${SQUARE_BASE}/v2/orders/${payment.order_id}`, { headers: { 'Authorization': `Bearer ${SQUARE_ACCESS_TOKEN}`, 'Square-Version': '2024-01-18' } }); const order = (await ordRes.json()).order; memberCode = extractMemberCode(order?.reference_id) || extractMemberCode(order?.note); items = (order?.line_items ?? []).map(li => ({ name: li.name, price: Number(li.base_price_money?.amount ?? 0) / 100, qty: Number(li.quantity ?? 1), })); } const identifier = memberCode ? { member_code: memberCode } : payment.buyer_email_address ? { email: payment.buyer_email_address } : {}; await fetch(TABSY_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${TABSY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ external_id: payment.id, identifier, total: Number(payment.amount_money?.amount ?? 0) / 100, currency: payment.amount_money?.currency ?? 'USD', receipt_date: payment.created_at, items, }), }); }); app.listen(3000);
import os, hmac, hashlib, base64, re, requests from flask import Flask, request app = Flask(__name__) SQUARE_SIGNATURE_KEY = os.environ['SQUARE_SIGNATURE_KEY'].encode() SQUARE_ACCESS_TOKEN = os.environ['SQUARE_ACCESS_TOKEN'] TABSY_API_KEY = os.environ['TABSY_API_KEY'] SQUARE_BASE = 'https://connect.squareup.com' # use squareupsandbox.com for sandbox TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt' SQ_HEADERS = {'Authorization': f'Bearer {SQUARE_ACCESS_TOKEN}', 'Square-Version': '2024-01-18'} def extract_member_code(s): m = re.search(r'TABSY:([A-Z0-9]{4,12})', (s or '').upper()) return m.group(1) if m else None @app.route('/webhooks/square', methods=['POST']) def square_webhook(): # Verify HMAC signature proto = request.headers.get('X-Forwarded-Proto', 'https') host = request.headers.get('X-Forwarded-Host', request.host) url = f'{proto}://{host}{request.path}' sig = base64.b64encode(hmac.new(SQUARE_SIGNATURE_KEY, (url + request.get_data(as_text=True)).encode(), hashlib.sha256).digest()) if not hmac.compare_digest(sig, request.headers.get('X-Square-Hmacsha256-Signature', '').encode()): return '', 200 event = request.get_json() if event.get('type') != 'payment.created': return '', 200 # Re-fetch full payment to get order_id pay_id = event['data']['object']['payment']['id'] payment = requests.get(f'{SQUARE_BASE}/v2/payments/{pay_id}', headers=SQ_HEADERS).json().get('payment', {}) # Fetch order for line items and member code items, member_code = [], None if order_id := payment.get('order_id'): order = requests.get(f'{SQUARE_BASE}/v2/orders/{order_id}', headers=SQ_HEADERS).json().get('order', {}) member_code = extract_member_code(order.get('reference_id')) or extract_member_code(order.get('note')) items = [{'name': li['name'], 'price': li['base_price_money']['amount'] / 100, 'qty': int(li.get('quantity', 1))} for li in order.get('line_items', [])] if member_code: identifier = {'member_code': member_code} elif payment.get('buyer_email_address'): identifier = {'email': payment['buyer_email_address']} else: identifier = {} requests.post(TABSY_URL, headers={'Authorization': f'Bearer {TABSY_API_KEY}'}, json={ 'external_id': payment['id'], 'identifier': identifier, 'total': payment['amount_money']['amount'] / 100, 'currency': payment['amount_money']['currency'], 'receipt_date': payment['created_at'], 'items': items, }, timeout=10) return '', 200 if __name__ == '__main__': app.run(port=3000)
TABSY:A7K2M9 (the shopper's code from their QR). The handler extracts it automatically. Without a code, the receipt still lands in the shopper's vault if their email matches a Tabsy account.
SQUARE_SIGNATURE_KEY, SQUARE_ACCESS_TOKEN) with your production Square credentials and set SQUARE_ENV=production. No code changes needed.
Step 2 — Set your credentials
The webhook function reads credentials from Supabase secrets — nothing is hardcoded. Set them once via the Supabase CLI:
supabase secrets set \
SQUARE_SIGNATURE_KEY="your_signature_key" \
SQUARE_ACCESS_TOKEN="your_access_token" \
TABSY_API_KEY="your_tabsy_api_key" \
SQUARE_ENV="production" \ # or "sandbox" for testing
--project-ref YOUR_PROJECT_REF
🛍
Shopify POS
Subscribe to Shopify's orders/paid webhook. This fires both for online checkouts and in-store Shopify POS transactions.
Step 1 — Register the webhook
In your Shopify Admin go to Settings → Notifications → Webhooks and add a webhook for the Order payment event. Or register it via the Admin API:
const res = await fetch( `https://${SHOP}.myshopify.com/admin/api/2024-01/webhooks.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ webhook: { topic: 'orders/paid', address: 'https://yourserver.com/webhooks/shopify', format: 'json' } }), } );
requests.post(
f'https://{SHOP}.myshopify.com/admin/api/2024-01/webhooks.json',
json={'webhook': {'topic': 'orders/paid',
'address': 'https://yourserver.com/webhooks/shopify',
'format': 'json'}},
headers={'X-Shopify-Access-Token': SHOPIFY_TOKEN},
)
Step 2 — Field mapping
| Shopify field | Tabsy field | Notes |
|---|---|---|
order.id | external_id | Unique per order — prevents duplicates. |
order.total_price | total | String — cast to float. |
order.currency | currency | Already ISO 4217. |
order.created_at | receipt_date | ISO 8601 — pass through. |
order.line_items[].title | items[].name | |
order.line_items[].price | items[].price | String — cast to float. |
order.line_items[].quantity | items[].qty | |
order.line_items[].sku | items[].sku | |
order.customer.email | identifier.email | Or store member code in customer note / metafield. |
order.note or metafield | identifier.member_code | If staff enters member code at checkout. |
Step 3 — Webhook handler
import express from 'express'; import crypto from 'crypto'; const app = express(); app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })); const SHOPIFY_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET; const TABSY_API_KEY = process.env.TABSY_API_KEY; const TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt'; // Verify Shopify HMAC signature function verifyShopify(req) { const hmac = req.headers['x-shopify-hmac-sha256']; const digest = crypto.createHmac('sha256', SHOPIFY_SECRET) .update(req.rawBody).digest('base64'); return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(digest)); } app.post('/webhooks/shopify', async (req, res) => { if (!verifyShopify(req)) return res.sendStatus(401); res.sendStatus(200); const order = req.body; // Prefer member code from order note; fall back to customer email const memberCode = order.note_attributes ?.find(a => a.name === 'tabsy_member_code')?.value; const identifier = memberCode ? { member_code: memberCode } : order.customer?.email ? { email: order.customer.email } : order.customer?.phone ? { phone: order.customer.phone } : {}; const items = (order.line_items ?? []).map(li => ({ name: li.title, price: parseFloat(li.price), qty: li.quantity, sku: li.sku || undefined, })); await fetch(TABSY_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${TABSY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ external_id: String(order.id), identifier, total: parseFloat(order.total_price), currency: order.currency, receipt_date: order.created_at, items, }), }); }); app.listen(3000);
import os, hmac, hashlib, base64, requests from flask import Flask, request app = Flask(__name__) SHOPIFY_SECRET = os.environ['SHOPIFY_WEBHOOK_SECRET'].encode() TABSY_API_KEY = os.environ['TABSY_API_KEY'] TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt' def verify_shopify(req): digest = base64.b64encode( hmac.new(SHOPIFY_SECRET, req.get_data(), hashlib.sha256).digest() ) return hmac.compare_digest( digest, req.headers.get('X-Shopify-Hmac-Sha256', '').encode() ) @app.route('/webhooks/shopify', methods=['POST']) def shopify_webhook(): if not verify_shopify(request): return '', 401 order = request.get_json() # Prefer member code from note attributes member_code = next( (a['value'] for a in order.get('note_attributes', []) if a['name'] == 'tabsy_member_code'), None ) cust = order.get('customer') or {} if member_code: identifier = {'member_code': member_code} elif cust.get('email'): identifier = {'email': cust['email']} elif cust.get('phone'): identifier = {'phone': cust['phone']} else: identifier = {} items = [ {'name': li['title'], 'price': float(li['price']), 'qty': li['quantity'], 'sku': li.get('sku') or None} for li in order.get('line_items', []) ] requests.post(TABSY_URL, json={ 'external_id': str(order['id']), 'identifier': identifier, 'total': float(order['total_price']), 'currency': order['currency'], 'receipt_date': order['created_at'], 'items': items, }, headers={'Authorization': f'Bearer {TABSY_API_KEY}'}, timeout=10, ) return '', 200 if __name__ == '__main__': app.run(port=3000)
tabsy_member_code. Staff can scan the shopper's Tabsy QR and paste the code before completing the sale. It arrives in order.note_attributes.
⚡
Lightspeed Retail
Lightspeed X-Series (formerly Vend) supports webhooks on sale completion. Lightspeed R-Series (legacy) requires polling the Sales API instead.
Step 1 — Register the webhook (X-Series)
In Lightspeed go to Setup → Webhooks and add a webhook for the Sale completed event pointing at your handler URL.
Step 2 — Field mapping
| Lightspeed field | Tabsy field | Notes |
|---|---|---|
sale.id | external_id | Unique per sale. |
sale.total_price | total | Already a decimal. |
sale.currency or outlet default | currency | Set a default if not on the payload. |
sale.sale_date | receipt_date | ISO 8601. |
sale.line_items[].product.name | items[].name | |
sale.line_items[].price | items[].price | |
sale.line_items[].quantity | items[].qty | |
sale.customer.email | identifier.email | Or store member code in a custom customer field. |
Step 3 — Webhook handler
import express from 'express'; const app = express(); app.use(express.json()); const TABSY_API_KEY = process.env.TABSY_API_KEY; const DEFAULT_CCY = 'AED'; // set your outlet's default currency const TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt'; app.post('/webhooks/lightspeed', async (req, res) => { res.sendStatus(200); const sale = req.body?.sale ?? req.body; const cust = sale.customer ?? {}; // Check for member code in custom customer attributes const memberCode = cust.custom_field_1 ?? null; // configure your field name const identifier = memberCode ? { member_code: memberCode } : cust.email ? { email: cust.email } : cust.phone ? { phone: cust.phone } : {}; const items = (sale.line_items ?? []) .filter(li => li.type !== 'payment') .map(li => ({ name: li.product?.name ?? li.name, price: parseFloat(li.price ?? li.unit_price ?? 0), qty: parseFloat(li.quantity ?? 1), sku: li.product?.sku || undefined, })); await fetch(TABSY_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${TABSY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ external_id: String(sale.id), identifier, total: parseFloat(sale.total_price), currency: sale.currency ?? DEFAULT_CCY, receipt_date: sale.sale_date, items, }), }); }); app.listen(3000);
import os, requests from flask import Flask, request app = Flask(__name__) TABSY_API_KEY = os.environ['TABSY_API_KEY'] DEFAULT_CCY = 'AED' # set your outlet's default currency TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt' @app.route('/webhooks/lightspeed', methods=['POST']) def lightspeed_webhook(): body = request.get_json() sale = body.get('sale', body) cust = sale.get('customer') or {} # Check custom customer field for member code member_code = cust.get('custom_field_1') # configure your field name if member_code: identifier = {'member_code': member_code} elif cust.get('email'): identifier = {'email': cust['email']} elif cust.get('phone'): identifier = {'phone': cust['phone']} else: identifier = {} items = [ {'name': li.get('product', {}).get('name', li.get('name', '')), 'price': float(li.get('price', li.get('unit_price', 0))), 'qty': float(li.get('quantity', 1)), 'sku': li.get('product', {}).get('sku') or None} for li in sale.get('line_items', []) if li.get('type') != 'payment' ] requests.post(TABSY_URL, json={ 'external_id': str(sale['id']), 'identifier': identifier, 'total': float(sale['total_price']), 'currency': sale.get('currency', DEFAULT_CCY), 'receipt_date': sale['sale_date'], 'items': items, }, headers={'Authorization': f'Bearer {TABSY_API_KEY}'}, timeout=10, ) return '', 200 if __name__ == '__main__': app.run(port=3000)
Tabsy Member Code. Reference it as custom_field_1 (or whichever slot you assign it to) in the code above.
Custom / bespoke POS
If you run a custom-built POS system, call the Tabsy API directly from your sale completion handler — no middleware needed.
const TABSY_API_KEY = process.env.TABSY_API_KEY; const TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt'; /** * Call after every completed sale. * @param {object} sale — your internal sale object */ async function sendToTabsy(sale) { const res = await fetch(TABSY_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${TABSY_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ external_id: sale.id, identifier: { member_code: sale.tabsy_member_code ?? undefined, email: sale.customer_email ?? undefined, phone: sale.customer_phone ?? undefined, }, total: sale.total, currency: sale.currency, // e.g. "AED" receipt_date: sale.completed_at, // ISO 8601 return_window_days: sale.return_days ?? undefined, items: sale.items.map(i => ({ name: i.name, price: i.unit_price, qty: i.quantity, sku: i.sku ?? undefined, warranty_months: i.warranty ?? undefined, category: i.category ?? undefined, })), }), }); if (!res.ok) { const err = await res.text(); console.error(`Tabsy error ${res.status}: ${err}`); } }
import os, requests as http TABSY_API_KEY = os.environ['TABSY_API_KEY'] TABSY_URL = 'https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/pos-receipt' def send_to_tabsy(sale: dict): """Call after every completed sale.""" identifier = {} if sale.get('tabsy_member_code'): identifier['member_code'] = sale['tabsy_member_code'] elif sale.get('customer_email'): identifier['email'] = sale['customer_email'] elif sale.get('customer_phone'): identifier['phone'] = sale['customer_phone'] res = http.post( TABSY_URL, json={ 'external_id': sale['id'], 'identifier': identifier, 'total': sale['total'], 'currency': sale['currency'], # e.g. "AED" 'receipt_date': sale['completed_at'], # ISO 8601 'return_window_days': sale.get('return_days'), 'items': [ { 'name': i['name'], 'price': i['unit_price'], 'qty': i['quantity'], 'sku': i.get('sku'), 'warranty_months': i.get('warranty'), 'category': i.get('category'), } for i in sale['items'] ], }, headers={'Authorization': f'Bearer {TABSY_API_KEY}'}, timeout=10, ) if not res.ok: print(f'Tabsy error {res.status_code}: {res.text}')