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.

Before you start: generate your API key in the Retailer Portal under API Keys. You'll need it for every request.

How it works

Every integration follows the same pattern regardless of your POS:

Transaction completes on your POS

Payment is accepted and the sale is finalised.

Your POS fires a webhook (or your middleware runs)

Square, Shopify, and Lightspeed all emit a webhook event you can subscribe to. Custom systems can call Tabsy directly.

Your handler maps the sale to the Tabsy payload

Translate your POS fields (items, total, customer ID) into the Tabsy receipt format shown below.

POST to the Tabsy API

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:

MethodFieldNotes
Member code Recommendedidentifier.member_codeShopper shows their Tabsy QR at checkout. Most reliable — guaranteed unique match.
Emailidentifier.emailUse the email on file in your POS. Works if the shopper signed up to Tabsy with the same email.
Phoneidentifier.phoneE.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.

Why not the Square dashboard "Send test event" button? That button sends a hardcoded synthetic payload with no real 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 → WebhooksAdd endpoint. Subscribe to the payment.created event and set the URL to your Tabsy webhook endpoint — no server required, it runs on Supabase:

POST https://zhnyoztnyvudmqpbikrj.supabase.co/functions/v1/square-webhook

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

SourceTabsy fieldNotes
payment.id (re-fetched)external_idIdempotency key — prevents duplicate receipts.
payment.amount_money.amount ÷ 100totalSquare stores amounts in the smallest currency unit.
payment.amount_money.currencycurrencyAlready ISO 4217.
payment.created_atreceipt_dateISO 8601 — pass through directly.
order.reference_id → parse TABSY:CODEidentifier.member_codeMost reliable match. Set by cashier at checkout.
payment.buyer_email_addressidentifier.emailFallback if no member code present.
order.line_items[].nameitems[].nameFetched via separate Orders API call.
order.line_items[].base_price_money.amount ÷ 100items[].price
order.line_items[].quantityitems[].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)
Member code via Square POS: In the Square POS app, staff tap Add note or set the Reference ID on the order and type 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.
Going to production? In the Square Developer Dashboard switch from the Sandbox webhook tab to the Production tab and add the same endpoint URL. Then update your Supabase secrets (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:

Shell
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 fieldTabsy fieldNotes
order.idexternal_idUnique per order — prevents duplicates.
order.total_pricetotalString — cast to float.
order.currencycurrencyAlready ISO 4217.
order.created_atreceipt_dateISO 8601 — pass through.
order.line_items[].titleitems[].name
order.line_items[].priceitems[].priceString — cast to float.
order.line_items[].quantityitems[].qty
order.line_items[].skuitems[].sku
order.customer.emailidentifier.emailOr store member code in customer note / metafield.
order.note or metafieldidentifier.member_codeIf 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)
Member code via Shopify POS: In your Shopify POS app, add a custom sale attribute called 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 fieldTabsy fieldNotes
sale.idexternal_idUnique per sale.
sale.total_pricetotalAlready a decimal.
sale.currency or outlet defaultcurrencySet a default if not on the payload.
sale.sale_datereceipt_dateISO 8601.
sale.line_items[].product.nameitems[].name
sale.line_items[].priceitems[].price
sale.line_items[].quantityitems[].qty
sale.customer.emailidentifier.emailOr 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)
Member code via Lightspeed: In Lightspeed go to Setup → Customer fields and add a custom field named 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}')
Need help with your specific POS? Email contact@tabsyapp.com or request an integration call — we'll map your fields and have you live same day.