Tavoloo

    ← POS integration

    Developer docs

    Tavoloo Integrations API

    For whoever connects a till system (POS) to Tavoloo. The reference below mirrors the OpenAPI contract.

    Download the OpenAPI contract (YAML)

    On this page

    Loading the reference…

    Verifying the signature

    Verify Tavoloo-Signature against the raw body bytes before parsing, compare in constant time, reject a t more than 300 seconds from your clock, and accept the request if any v1 matches. Answer a non-2xx status (for example 401) when it does not verify: Tavoloo retries.

    Node (crypto + Express)

    import crypto from 'node:crypto';
    import express from 'express';
    
    const SECRET = process.env.TAVOLOO_WEBHOOK_SECRET; // whsec_...
    const TOLERANCE_SEC = 300;
    
    export function verifyTavolooSignature(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
      if (!secret || !header) return false;
      let t = null;
      const sigs = [];
      for (const part of header.split(',')) {
        const i = part.indexOf('=');
        if (i < 0) continue;
        const k = part.slice(0, i).trim();
        const v = part.slice(i + 1).trim();
        if (k === 't') t = v;
        else if (k === 'v1') sigs.push(v);
      }
      if (!t || !/^\d{1,12}$/.test(t) || Math.abs(now - Number(t)) > TOLERANCE_SEC) return false;
      const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
      return sigs.some(
        (s) => /^[0-9a-f]{64}$/.test(s) && crypto.timingSafeEqual(Buffer.from(s, 'hex'), expected),
      );
    }
    
    const app = express();
    const seen = new Set(); // in production: a table or Redis keyed by (connection, event id)
    
    // express.raw hands over the body as a Buffer: the exact bytes Tavoloo signed.
    app.post('/tavoloo/webhook', express.raw({ type: 'application/json', limit: '512kb' }), (req, res) => {
      if (!verifyTavolooSignature(SECRET, req.get('Tavoloo-Signature'), req.body)) {
        return res.sendStatus(401); // non-2xx: Tavoloo retries
      }
      const event = JSON.parse(req.body.toString('utf8'));
      if (seen.has(event.id)) return res.sendStatus(200); // duplicate: already handled
      seen.add(event.id);
    
      switch (event.type) {
        case 'order.created':
          // send event.data to the kitchen or bar printer
          break;
        case 'order.updated':
        case 'waiter_call.created':
        case 'waiter_call.updated':
        case 'table_session.opened':
        case 'table_session.closed':
        case 'integration.test':
          break;
        default:
          break; // new type: ignore it, do not fail
      }
      return res.sendStatus(200);
    });
    
    app.listen(3000);

    PHP (hash_hmac + hash_equals)

    <?php
    const TAVOLOO_TOLERANCE_SEC = 300;
    
    function verify_tavoloo_signature(string $secret, ?string $header, string $rawBody, int $tolerance = TAVOLOO_TOLERANCE_SEC): bool
    {
        if ($secret === '' || $header === null || $header === '') {
            return false;
        }
        $t = null;
        $sigs = [];
        foreach (explode(',', $header) as $part) {
            $pos = strpos($part, '=');
            if ($pos === false) {
                continue;
            }
            $k = trim(substr($part, 0, $pos));
            $v = trim(substr($part, $pos + 1));
            if ($k === 't') {
                $t = $v;
            } elseif ($k === 'v1') {
                $sigs[] = $v;
            }
        }
        if ($t === null || !preg_match('/^\d{1,12}$/', $t) || abs(time() - (int) $t) > $tolerance) {
            return false;
        }
        $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
        foreach ($sigs as $s) {
            if (hash_equals($expected, $s)) {
                return true;
            }
        }
        return false;
    }
    
    $rawBody = file_get_contents('php://input');           // exact bytes, before json_decode
    $header  = $_SERVER['HTTP_TAVOLOO_SIGNATURE'] ?? null; // the Tavoloo-Signature header
    $secret  = getenv('TAVOLOO_WEBHOOK_SECRET') ?: '';     // whsec_...
    
    if (!verify_tavoloo_signature($secret, $header, $rawBody)) {
        http_response_code(401); // non-2xx: Tavoloo retries
        exit;
    }
    
    $event = json_decode($rawBody, true);
    // de-duplicate on $event['id'] (for example an INSERT with a unique key), then:
    // if ($event['type'] === 'order.created') { /* print in the kitchen */ }
    
    http_response_code(200);