Payments
Connect your point-of-sale (POS) system to Tablescale for in-venue payments when staff complete orders.
How it works
Staff taps Pay & Complete on an order in Tablescale.
Tablescale sends a payment.requested webhook to your URL with the order total and line items.
Your POS charges the customer on the local terminal.
Your system reports the result back to Tablescale via the callback API.
On success, the order is marked paid and completed. Include payment_method to record cash or card on the session.
Setup checklist
Webhook events
Tablescale POSTs JSON to your webhook URL. All requests include signed headers (see Signature verification).
payment.requested
Sent when staff initiates payment or you trigger a test webhook. Contains the amount and line items to charge.
payment.expired
Sent when the payment session times out without a result.
payment.cancelled
Sent when a payment session is cancelled before completion.
Example payload
{
"event": "payment.requested",
"id": "evt_abc123",
"created_at": "2026-06-13T12:00:00Z",
"data": {
"payment_session_id": "clx...",
"order_id": "clx...",
"order_display_id": "42",
"amount_cents": 2450,
"price": "24.50",
"currency": "EUR",
"line_items": [
{
"name": "Margherita Pizza",
"quantity": 2,
"unit_price_cents": 850,
"options": [{ "name": "Extra cheese", "price_cents": 150 }]
}
],
"table": "Table 5",
"expires_at": "2026-06-13T12:02:00Z"
}
}Signature verification
Verify that webhooks came from Tablescale using the webhook secret from Settings → Payments.
The snippet below is exemplary JavaScript for decoding and verifying the X-Tablescale-Signature header. Adapt it to your stack — Node.js crypto, Deno, browser Web Crypto, or your language's HMAC utilities.
const crypto = require("crypto");
function verifyTablescaleSignature(payload, signatureHeader, secret) {
const parts = signatureHeader.split(",");
const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2);
const signature = parts.find((p) => p.startsWith("v1="))?.slice(3);
if (!timestamp || !signature) return false;
const signed = `${timestamp}.${payload}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}Headers: X-Tablescale-Signature, X-Tablescale-Event-Id, X-Tablescale-Timestamp, X-Tablescale-Test-Mode
Reporting payment results
POST to /api/integrations/payments/callback when your POS finishes processing. Authenticate with your API key in the Authorization header.
Example request
POST /api/integrations/payments/callback
Authorization: Bearer ts_live_...
Content-Type: application/json
{
"payment_session_id": "clx...",
"status": "success",
"payment_method": "CARD",
"external_ref": "pos-txn-12345"
}Request body fields
payment_session_idRequiredstringThe payment session ID from the webhook payload or test integration panel.
statusRequiredsuccessfailWhether the POS payment succeeded or failed.
external_refOptionalstringYour POS transaction or receipt reference, if you want it stored on the session.
failure_reasonOptionalstringHuman-readable reason when status is fail. Ignored for success callbacks.
payment_methodOptionalCASHCARDHow the customer paid. Stored on the payment session when status is success. Defaults to CASH. Session method values: CASH, CARD (from callback), MANUAL_CASH, MANUAL_CARD (staff override via UI).
Duplicate callbacks with the same session and status are idempotent and return the existing result. All payment activity appears in Settings → Payments → Payment logs, including outbound webhooks, inbound callbacks (callback.success / callback.fail), and internal state changes (payment.success / payment.fail). The Mode column in Payment logs shows Live or Test. Error codes: 400, 401, 404, 409.
Polling session status
If your POS receives the payment.requested webhook but prefers not to call the callback endpoint — or wants to confirm the final status after reporting — poll the session using the payment_session_id from the webhook payload:
GET /api/integrations/payments/sessions/{payment_session_id}
Authorization: Bearer ts_live_...Polling does not replace webhooks: Tablescale has no list-or-discover endpoint, so your system must receive the initial webhook (or use a relay that forwards the session ID) to learn which session to poll.
Cancelling a payment
POST /api/integrations/payments/sessions/{payment_session_id}/cancel
Authorization: Bearer ts_live_...Test mode
Test mode sends dummy payment sessions that do not affect live orders. All test webhooks include X-Tablescale-Test-Mode: true.
From Settings → Payments → Test integration: click Send test webhook, then POST the result to /api/integrations/payments/callback using your API key and the returned payment_session_id.
# After sending a test webhook, report success:
curl -X POST https://your-tablescale-domain.com/api/integrations/payments/callback \
-H "Authorization: Bearer ts_test_..." \
-H "Content-Type: application/json" \
-d '{"payment_session_id":"SESSION_ID","status":"success"}'Rate limits
Test tools in Settings → Payments are rate limited per organization (10 sends per minute). Live production endpoints (/callback, /sessions/*) are not rate limited. When exceeded, you receive 429 Too Many Requests with a Retry-After header.
{ "error": "Rate limit exceeded" }Tip: reuse the test session ID from a single webhook instead of sending repeated test webhooks in a loop.
Best practices
- Respond to webhooks within 10 seconds (return 2xx quickly, process async if needed).
- Verify webhook signatures before processing.
- Use the
payment_session_idto correlate requests — Tablescale may retry failed deliveries. - Handle idempotent callbacks — your endpoint may receive the same result more than once.
Security
Webhook URLs must use HTTPS in production.
Keep your API key and webhook secret confidential. Rotate them if compromised.
Tablescale does not handle card data — your POS system processes payments directly. You remain responsible for PCI compliance on your side.