Przejdź do treści
Na tej stronie

Dla programistów · Wersja 1.2.0

Dokumentacja API Tavolify

Wysyłaj zamówienia z własnej strony na kasę i do kuchni, czytaj menu, śledź zamówienia i zapisuj płatności.

Bazowy adres URL
https://your-restaurant.tavolify.com/api/v1
OpenAPI 3.1
openapi.json
Format
JSON · HTTPS · Bearer

Quick start

The Tavolify REST API lets your own systems work with the till: send orders from your website, read the menu, follow orders through the kitchen and record payments. It speaks JSON over HTTPS. Every request is made for one business and is authenticated with a Bearer token.

  1. Create a token. In Tavolify, open Settings → API, enter a name (for example “Website”), tick the permissions it needs and press Create token. Copy the token at once: it is shown only once.
  2. Keep it secret. Store it in an environment variable on your server, for example TAVOLIFY_TOKEN. Never put it in browser JavaScript or a mobile app: anyone could read it there.
  3. Make your first call. GET /me tells you who the token belongs to and what it may do.

Base URL: https://your-restaurant.tavolify.com/api/v1 (use your own business address instead of your-restaurant).

curl

curl "https://your-restaurant.tavolify.com/api/v1/me" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/me', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/me');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);

Authentication and scopes

Send the token in the Authorization header of every request: Authorization: Bearer 12|AbCd…. Add Accept: application/json so errors come back as JSON too. Requests without a valid token get 401.

A token has scopes (abilities) and can never do more than the staff member who created it: each call needs both the scope and that person’s staff permission in the branch. If the person is deactivated, the token stops at once. Tokens can expire after 30, 90 or 365 days, or never.

ScopeAllows
menu:readRead the menu (GET /menu)
menu:writeMark items sold out or back on sale
orders:readList and read orders, list order sources
orders:writeCreate orders
orders:statusAccept, mark ready, complete or cancel orders
payments:writeRecord payments
branches:readList branches
loyalty:readRead the loyalty programme, cards, balances and history (staff permission loyalty.view)
loyalty:writeCreate loyalty cards and link orders to them (staff permission loyalty.manage)

Give each system its own token with only the scopes it needs. A website that only sends orders needs orders:write (and orders:read if it shows the order status). Revoke a token in Settings → API and it stops working immediately.

Orders from your website

An order source is one of your own sales channels outside Tavolify: your website (for example mybistro.pl), another ordering app or a partner’s kiosk. Its orders arrive on the till’s Online orders screen, the kitchen display and the kitchen ticket with the source’s name and colour, and admin Orders and the sales report can be filtered by it.

Step 1 — add the source

In Settings → API → Order sources press Add order source. Enter a name (up to 40 characters, shown to your staff), pick a colour and, if you like, a short code such as WEB. Choose whether its orders go straight to the kitchen or wait for staff to accept them on the till (accepting on the till also prints the kitchen ticket). Every source gets a key like src_k3m9x2p7q4wz that never changes.

Step 2 — create a token for it

Create a token with the orders:write scope (plus orders:read to read statuses) and choose the source under Order source. Orders sent with this token come from that source, so your website does not even need to know the key. A request can still name another source with "source": "src_…".

Step 3 — send each order from your website

When a guest checks out on your website, send the order to POST /orders from your server. Use your own order number as external_id: if the request fails half-way and you send it again, you get the same order back instead of a second one. Item and choice ids come from GET /menu (map them once when you build your shop). Money is in minor units: 6280 is 62.80 zł.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "WEB-1001",
    "branch_id": 1,
    "type": "delivery",
    "note": "Ring twice, please",
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "delivery_address": {
        "street": "ul. Puławska 12/4",
        "postcode": "02-515",
        "city": "Warszawa",
        "note": "Door code 1234"
    },
    "requested_time": "2026-11-12T18:30:00+01:00",
    "delivery_fee": 900,
    "payment": {
        "status": "paid",
        "amount": 6280,
        "reference": "p24-778812"
    },
    "lines": [
        {
            "item_id": 12,
            "quantity": 2000,
            "note": "extra crispy",
            "modifiers": [
                {
                    "id": 31
                }
            ]
        }
    ]
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "external_id": "WEB-1001",
      "branch_id": 1,
      "type": "delivery",
      "note": "Ring twice, please",
      "customer": {
          "name": "Anna Nowak",
          "phone": "+48 600 100 200",
          "email": "anna@example.com"
      },
      "delivery_address": {
          "street": "ul. Puławska 12/4",
          "postcode": "02-515",
          "city": "Warszawa",
          "note": "Door code 1234"
      },
      "requested_time": "2026-11-12T18:30:00+01:00",
      "delivery_fee": 900,
      "payment": {
          "status": "paid",
          "amount": 6280,
          "reference": "p24-778812"
      },
      "lines": [
          {
              "item_id": 12,
              "quantity": 2000,
              "note": "extra crispy",
              "modifiers": [
                  {
                      "id": 31
                  }
              ]
          }
      ]
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'external_id' => 'WEB-1001',
    'branch_id' => 1,
    'type' => 'delivery',
    'note' => 'Ring twice, please',
    'customer' => [
        'name' => 'Anna Nowak',
        'phone' => '+48 600 100 200',
        'email' => 'anna@example.com',
    ],
    'delivery_address' => [
        'street' => 'ul. Puławska 12/4',
        'postcode' => '02-515',
        'city' => 'Warszawa',
        'note' => 'Door code 1234',
    ],
    'requested_time' => '2026-11-12T18:30:00+01:00',
    'delivery_fee' => 900,
    'payment' => [
        'status' => 'paid',
        'amount' => 6280,
        'reference' => 'p24-778812',
    ],
    'lines' => [
        [
            'item_id' => 12,
            'quantity' => 2000,
            'note' => 'extra crispy',
            'modifiers' => [
                [
                    'id' => 31,
                ],
            ],
        ],
    ],
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);

The answer is the order (201 Created, or 200 if this external_id was already sent), with its id, number, totals as Tavolify priced them and fulfilment.status = pending (or accepted when the source goes straight to the kitchen). Prices always come from the menu in Tavolify; compare total with your own total to spot a menu that is out of date.

Step 4 — follow the order

Subscribe to the order.status_changed webhook (Settings → Webhooks) to tell your guest “accepted, ready at 18:30”, “ready” or “sorry, we cannot take it”. Without webhooks, poll GET /orders?source=…&updated_since=… once a minute. The statuses are:

  • pending — waiting for staff on the till.
  • accepted — in the kitchen; fulfilment.ready_at is the promised time.
  • ready — ready for pickup or for the driver.
  • completed — handed over; the bill is closed.
  • rejected — turned down or cancelled; fulfilment.reject_reason says why.

If your website took the money online, send payment.status = paid with the amount and your payment reference: staff see the order as paid. If an order is rejected, refund the guest through your own payment provider; Tavolify never holds that money. Leave payment out when the guest pays on pickup or delivery.

Your kitchen can also be driven from your own system with /orders/{order}/accept, /ready, /complete and /cancel (scope orders:status). The same rules as on the till apply: you cannot mark a pending order ready, or hand over an order with something left to pay.

Loyalty on your website

Your website can use the restaurant’s own loyalty programme instead of keeping points of its own: the guest has one card for the till, the kiosk and your website, and online orders earn exactly like orders at the counter. The programme (for example “every 200 zł spent earns 30 zł credit”) is set up in Loyalty → Programme; the API never changes its rules.

Step 1 — a token with the loyalty scopes

Create a token with loyalty:read and loyalty:write (next to orders:write for your orders). The person who creates it needs the staff permissions loyalty.view and loyalty.manage; owners and managers have them. The loyalty calls answer 403 loyalty_unavailable while the programme is switched off or the plan does not include loyalty.

Step 2 — the guest’s card

When a signed-in guest opens “My card” on your website, call POST /loyalty/members with their phone number. You get the card back (200), or a new card is made (201) — the phone number is the key, so the call is safe to repeat and two calls at once still make one card. A guest the till already knows by that number keeps their history. If the business has programme terms, show them (GET /loyalty/program → terms) and send "terms": true once the guest accepted them; marketing consent is its own checkbox, never pre-ticked.

Check that the guest owns the number first — with a text code or your own sign-in. The API trusts your server, not the browser: never call it with a number typed by someone who has not proved it is theirs, and never call the API from the browser (the token is a secret).

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/loyalty/members" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+48 600 100 200",
    "name": "Anna Nowak",
    "email": "anna@example.com",
    "terms": true,
    "marketing": false
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/loyalty/members', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "phone": "+48 600 100 200",
      "name": "Anna Nowak",
      "email": "anna@example.com",
      "terms": true,
      "marketing": false
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'phone' => '+48 600 100 200',
    'name' => 'Anna Nowak',
    'email' => 'anna@example.com',
    'terms' => true,
    'marketing' => false,
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/loyalty/members');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);

The answer carries the card_number, the card_url and the balance. Show the card number and a QR code of card_url: staff scan it at the till, and the guest can open the same page on the phone. It never carries the guest’s name, phone or e-mail.

Step 3 — balance and history

GET /loyalty/members/{card} returns the balance (credit in minor units, points, progress and next_reward: how much is still to spend for the next reward). GET /loyalty/members/{card}/entries lists the history newest first, paged: earned, paid with at the till, reversed after a refund, expired, welcome and birthday rewards. {card} is the card number or the phone number (write the plus as %2B).

Step 4 — orders that earn

Add "loyalty": {"card": "HKQ4M8T2XN7BK"} (or {"phone": "+48 600 100 200"}) to POST /orders. The order is linked to the card’s customer, and when the bill closes — paid, or completed for an order of your order source — Tavolify writes the earning itself, with the same rules as the till: no deposits, no excluded categories, nothing twice. A refund or a cancelled receipt takes it back. An unknown or blocked card refuses the order (422 loyalty_member_unknown / loyalty_inactive), so nothing is created half-way. Forgot the card? POST /orders/{order}/loyalty links it while the order is still open.

Paying with loyalty credit stays at the till, where staff see the guest. To keep your website in step without polling, subscribe to the loyalty.entry_created webhook: it carries the card number, the new line and the balance after it.

Idempotency

Networks fail. A request can time out after Tavolify already created the order. To make retries safe:

  • Send external_id (your own order number, up to 80 characters). The same external_id of the same source never creates a second order: the first order is returned with 200 OK instead of 201 Created. The same number from another source is another order.
  • Without an external_id, send an Idempotency-Key header (up to 100 characters, e.g. a UUID you keep for the checkout). The same key returns the order created the first time.

This also holds when two identical requests arrive at the same moment. Retry with the same body after a timeout, a 5xx answer or 429; never retry a 422 unchanged.

Webhooks

Webhooks push events to your server so you do not have to poll. Add an endpoint in Settings → Webhooks (HTTPS only), choose the events and copy the signing secret. Tavolify sends a POST with a JSON body:

{
  "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
  "event": "order.status_changed",
  "business_id": 7,
  "created_at": "2026-11-12T17:05:00+00:00",
  "data": { …the order, plus "status_change": {"from": "pending", "to": "accepted"} }
}

Headers: Tavolify-Event, Tavolify-Delivery (the same on every retry: use it to ignore duplicates) and Tavolify-Signature: t=<unix seconds>,v1=<hex>. The signature is the HMAC-SHA256 of <t>.<raw body> with your secret. Check it on the raw body (before any JSON parsing), compare in constant time and reject timestamps older than 5 minutes.

Answer with any 2xx within 5 seconds and do the work afterwards. Other answers and timeouts are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h; after 10 deliveries in a row that failed every retry the webhook is switched off (you see it in Settings). Events can arrive out of order: use updated_at of the order to keep the newest state.

Node.js

import crypto from 'node:crypto';
import express from 'express';

const app = express();
// Keep the raw body: the signature is computed over the exact bytes.
app.post('/tavolify/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Tavolify-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.TAVOLIFY_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid = parts.v1 && parts.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!fresh || !valid) return res.status(400).send('bad signature');

  const event = JSON.parse(req.body);
  res.sendStatus(204); // answer fast, work afterwards
  if (event.event === 'order.status_changed') {
    console.log(event.data.external_id, event.data.status_change.to);
  }
});

PHP

<?php
$body = file_get_contents('php://input'); // the raw body
$header = $_SERVER['HTTP_TAVOLIFY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, getenv('TAVOLIFY_WEBHOOK_SECRET'));
$fresh = abs(time() - (int) ($parts['t'] ?? 0)) < 300;
if (! $fresh || ! hash_equals($expected, (string) ($parts['v1'] ?? ''))) {
    http_response_code(400);
    exit('bad signature');
}

$event = json_decode($body, true);
http_response_code(204); // answer fast, work afterwards
if ($event['event'] === 'order.status_changed') {
    error_log($event['data']['external_id'] . ' → ' . $event['data']['status_change']['to']);
}

Errors

Tavolify uses ordinary HTTP status codes. Errors always have a JSON body.

StatusMeaning
200 / 201OK / created. 200 on POST /orders means the order already existed (idempotency).
401Missing, wrong, expired or revoked token.
403The token lacks the scope, or its owner lacks the staff permission (permission names it).
404No such record in this business (records of other businesses are never visible).
422Validation errors per field (errors), or an order rule with a stable error code.
429Too many requests; wait for Retry-After seconds.
5xxSomething went wrong on our side; retry later with the same external_id.

Order rule codes you may meet: item_unavailable, variant_required, variant_unknown, modifiers_too_few, modifiers_too_many, modifiers_unknown_modifier, branch_inactive, payment_exceeds_due, invalid_amount, source_unknown, status_not_allowed, order_not_paid, order_has_payments, reason_required, order_not_open. Program against the error code; message is a translated text for people and may change.

Rate limits

Each token may make 120 requests per minute. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining; above the limit you get 429 Too Many Requests with Retry-After (seconds). Wait that long, then continue. Poll at most once a minute and prefer webhooks; cache the menu and refresh it when item.sold_out / item.back_on_sale arrive.

Pagination

Lists are paginated. Pass per_page (1–100, default 50) and page. The answer has a meta object with current_page, last_page, per_page and total; ask for the next page until current_page equals last_page.

To keep a copy in sync, use updated_since: orders changed at or after that time come oldest change first. Remember the newest updated_at you processed and use it as the next updated_since. Because the boundary is inclusive you may see an order twice; use its id to skip it.

Versioning and changelog

The API is versioned in the path (/api/v1). Within v1 we only add things: new endpoints, new optional fields in requests and new fields in answers. Ignore fields you do not know. Anything that would break a working integration comes as /api/v2, with v1 kept running long enough to move. The machine-readable description is always at https://tavolify.com/api/v1/openapi.json (OpenAPI 3.1).

Changelog

  • 1.2.0 — November 2026. Loyalty: GET /loyalty/program, POST /loyalty/members (find or create a card by phone), GET /loyalty/members/{card} and /entries (by card number or phone), loyalty on POST /orders and POST /orders/{order}/loyalty (the order earns when it closes); loyalty on a single order; new scopes loyalty:read and loyalty:write; the loyalty.entry_created webhook.
  • 1.1.0 — November 2026. Order sources (GET /order-sources, source on POST /orders, tokens tied to a source), external_id idempotency, customer, delivery address, requested time, delivery fee and “already paid” on new orders; order_source, external_id, fulfilment, note, updated_at on orders; source, fulfilment_status and updated_since filters; accept, ready, complete and cancel (new scope orders:status); the order.status_changed webhook; order.created of API orders now carries the lines.
  • 1.0.0 — November 2026. Account, branches, menu with availability, orders, payments, signed webhooks.

Opis API

Account

Who the token belongs to.

The token owner, business and scopes

GET /api/v1/me

Any valid token. A good first call to check your setup.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/me" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/me', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/me');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": {
        "user": {
            "id": 1,
            "name": "Halil"
        },
        "business": {
            "id": 7,
            "name": "Demo Bistro",
            "country": "PL",
            "currency": "PLN",
            "timezone": "Europe/Warsaw",
            "locale": "pl"
        },
        "scopes": [
            "orders:write",
            "orders:read"
        ]
    }
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Opis API

Branches

Locations of the business. Every order belongs to one branch.

List branches

GET /api/v1/branches Zakres: branches:read

Scope: branches:read. Use the id as branch_id when you create orders.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/branches" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/branches', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/branches');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": [
        {
            "id": 1,
            "name": "Mokotów",
            "slug": "mokotow",
            "city": "Warszawa",
            "country": "PL",
            "timezone": "Europe/Warsaw",
            "is_active": true
        }
    ]
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Opis API

Order sources

Your own sales channels (website, app, kiosk) that send orders through the API.

List order sources

GET /api/v1/order-sources Zakres: orders:read

Scope: orders:read. Sources are created by the business in Settings → API → Order sources; the key never changes.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/order-sources" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/order-sources', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/order-sources');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": [
        {
            "key": "src_k3m9x2p7q4wz",
            "name": "mybistro.pl",
            "color": "#BE123C",
            "code": "WEB",
            "auto_accept": false,
            "last_order_at": "2026-11-12T17:02:11+00:00",
            "created_at": "2026-11-01T09:00:00+00:00"
        }
    ]
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Opis API

Orders

Create orders, read them, follow them and move them through accepted → ready → completed.

List orders

GET /api/v1/orders Zakres: orders:read

Scope: orders:read. Newest first; with updated_since oldest change first. Paginated (see meta).

Parametry

PoleTypOpis
status query string open, closed or voided One of: open, closed, voided.
branch_id query integer
from query string First business day (Y-m-d)
to query string Last business day (Y-m-d)
source query string Only orders of this order source (key)
fulfilment_status query string One of: pending, accepted, ready, completed, rejected.
updated_since query string ISO 8601 time: only orders changed at or after it, oldest change first (for syncing). Keep the last updated_at you saw as your next value.
per_page query integer 1–100, default 50 From 1 to 100. Default 50.
page query integer Default 1.

Odpowiedzi

  • 200A page of orders (without lines)
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/orders?source=src_k3m9x2p7q4wz&updated_since=2026-11-12T17:00:00Z" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders?source=src_k3m9x2p7q4wz&updated_since=2026-11-12T17:00:00Z', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders?source=src_k3m9x2p7q4wz&updated_since=2026-11-12T17:00:00Z');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
            "number": 42,
            "branch_id": 1,
            "business_date": "2026-11-12",
            "type": "takeaway",
            "channel": "online",
            "source": "pos",
            "status": "open",
            "currency": "PLN",
            "prices_include_tax": true,
            "subtotal": 5380,
            "discount_total": 0,
            "net_total": 4981,
            "tax_total": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "created_at": "2026-11-12T17:02:11+00:00",
            "closed_at": null,
            "updated_at": "2026-11-12T17:02:11+00:00",
            "order_source": {
                "key": "src_k3m9x2p7q4wz",
                "name": "mybistro.pl",
                "color": "#BE123C",
                "code": "WEB"
            },
            "external_id": "WEB-1001",
            "note": "Ring twice",
            "fulfilment": {
                "status": "pending",
                "requested_for": "2026-11-12T18:00:00+00:00",
                "ready_at": null,
                "accepted_at": null,
                "reject_reason": null
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 50,
        "total": 1
    }
}
Odpowiedź 422
{
    "error": "source_unknown",
    "message": "This order source does not exist. Check its key in Settings → API."
}

Create an order

POST /api/v1/orders Zakres: orders:write

Scope: orders:write; the token owner needs orders.create in the branch. With an order source (source, or the token's own) the order waits on the till's Online orders screen (or goes straight to the kitchen when the source accepts automatically) and shows the source's name and colour everywhere. external_id (or an Idempotency-Key header) makes retries safe.

Parametry

PoleTypOpis
Idempotency-Key header string Used when no external_id is sent: the same key returns the order created the first time. At most 100 characters.

Treść żądania

PoleTypOpis
source string Order source key. Optional when the token is tied to a source. With a source the order is an online order of that channel. At most 40 characters.
external_id string Your own order number. The same external_id of a source never creates a second order: the first one is returned with 200. At most 80 characters.
branch_idwymagane integer
typewymagane string Usually takeaway (collect) or delivery. One of: takeaway, delivery, pickup, dine_in, qr, online, kiosk.
note string Note for the whole order, shown on the till and the kitchen ticket At most 500 characters.
customer object
customer.name string At most 80 characters.
customer.phone string At most 32 characters.
customer.email string At most 190 characters.
delivery_address object Required (street) for delivery orders with a source.
delivery_address.street string At most 200 characters.
delivery_address.postcode string At most 16 characters.
delivery_address.city string At most 80 characters.
delivery_address.note string Floor, door code … At most 200 characters.
requested_time string When the guest wants it. Omit, or a time in the past, for as soon as possible.
delivery_fee integer Added as a fee line (minor units)
payment object Money you already took (e.g. your own online payment). Omit or status=unpaid: the guest pays on pickup or delivery.
payment.status string One of: paid, unpaid.
payment.amount integer Required when paid; at most the order total
payment.reference string Your payment id At most 100 characters.
lineswymagane array of object
lines[].item_idwymagane integer
lines[].variant_id integer Required for items with sizes
lines[].quantity integer Milli-units, default 1000 (= 1)
lines[].note string At most 300 characters.
lines[].modifiers array of object
lines[].modifiers[].idwymagane integer
lines[].modifiers[].quantity integer From 0 to 50. Default 1.
loyalty object Link the order to a loyalty card: send the card number or the phone, exactly one. Needs the loyalty:write scope too. The order earns on the card when it closes (paid, or completed for an order source). An unknown or blocked card refuses the whole order (422 loyalty_member_unknown / loyalty_inactive); the website's own customer details stay, missing ones come from the card's customer.
loyalty.card string At most 64 characters.
loyalty.phone string At most 32 characters.

Odpowiedzi

  • 201Created
  • 200Already created with this external_id or Idempotency-Key (the first order)
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "src_k3m9x2p7q4wz",
    "external_id": "WEB-1001",
    "branch_id": 1,
    "type": "takeaway",
    "note": "Ring twice",
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "requested_time": "2026-11-12T18:00:00+00:00",
    "payment": {
        "status": "paid",
        "amount": 5380,
        "reference": "p24-778812"
    },
    "lines": [
        {
            "item_id": 12,
            "quantity": 2000,
            "note": "extra crispy",
            "modifiers": [
                {
                    "id": 31
                }
            ]
        }
    ],
    "loyalty": {
        "card": "HKQ4M8T2XN7BK"
    }
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "source": "src_k3m9x2p7q4wz",
      "external_id": "WEB-1001",
      "branch_id": 1,
      "type": "takeaway",
      "note": "Ring twice",
      "customer": {
          "name": "Anna Nowak",
          "phone": "+48 600 100 200",
          "email": "anna@example.com"
      },
      "requested_time": "2026-11-12T18:00:00+00:00",
      "payment": {
          "status": "paid",
          "amount": 5380,
          "reference": "p24-778812"
      },
      "lines": [
          {
              "item_id": 12,
              "quantity": 2000,
              "note": "extra crispy",
              "modifiers": [
                  {
                      "id": 31
                  }
              ]
          }
      ],
      "loyalty": {
          "card": "HKQ4M8T2XN7BK"
      }
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'source' => 'src_k3m9x2p7q4wz',
    'external_id' => 'WEB-1001',
    'branch_id' => 1,
    'type' => 'takeaway',
    'note' => 'Ring twice',
    'customer' => [
        'name' => 'Anna Nowak',
        'phone' => '+48 600 100 200',
        'email' => 'anna@example.com',
    ],
    'requested_time' => '2026-11-12T18:00:00+00:00',
    'payment' => [
        'status' => 'paid',
        'amount' => 5380,
        'reference' => 'p24-778812',
    ],
    'lines' => [
        [
            'item_id' => 12,
            'quantity' => 2000,
            'note' => 'extra crispy',
            'modifiers' => [
                [
                    'id' => 31,
                ],
            ],
        ],
    ],
    'loyalty' => [
        'card' => 'HKQ4M8T2XN7BK',
    ],
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 201
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "pending",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "pending",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}

One order with lines, payments and the customer

GET /api/v1/orders/{order} Zakres: orders:read

Scope: orders:read

Parametry

PoleTypOpis
orderwymagane path string Order id (ULID)

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.
  • 404No such order in this business.

curl

curl "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "pending",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Accept an order

POST /api/v1/orders/{order}/accept Zakres: orders:status

Scope: orders:status; the token owner needs orders.create in the branch. Only from pending. Sends the order to the kitchen (kitchen display; printed tickets come from the till) and promises a ready time.

Parametry

PoleTypOpis
orderwymagane path string

Treść żądania

PoleTypOpis
minutes integer Minutes until ready (ignored when the guest asked for a time) From 5 to 240. Default 20.

Odpowiedzi

  • 200The order after the step
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.
  • 404No such order in this business.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/accept" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "minutes": 25
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/accept', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "minutes": 25
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'minutes' => 25,
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/accept');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "accepted",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 422
{
    "error": "status_not_allowed",
    "message": "This order cannot move to that status now."
}

Mark an order ready

POST /api/v1/orders/{order}/ready Zakres: orders:status

Scope: orders:status; the token owner needs orders.create in the branch. Only from accepted.

Parametry

PoleTypOpis
orderwymagane path string

Odpowiedzi

  • 200The order after the step
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.
  • 404No such order in this business.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/ready" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/ready', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/ready');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "ready",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 422
{
    "error": "status_not_allowed",
    "message": "This order cannot move to that status now."
}

Hand over (complete) an order

POST /api/v1/orders/{order}/complete Zakres: orders:status

Scope: orders:status; the token owner needs orders.create in the branch. Only from accepted or ready, and only when nothing is left to pay (error order_not_paid): the bill is closed. Orders with something to pay are paid and closed on the till, or record the payment first.

Parametry

PoleTypOpis
orderwymagane path string

Odpowiedzi

  • 200The order after the step
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.
  • 404No such order in this business.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/complete" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/complete', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/complete');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "completed",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 422
{
    "error": "status_not_allowed",
    "message": "This order cannot move to that status now."
}

Cancel an order

POST /api/v1/orders/{order}/cancel Zakres: orders:status

Scope: orders:status; the token owner needs orders.create in the branch. Pending: turned down (like Reject on the till). Accepted or a till bill: voided, which needs orders.cancel and no payments recorded in Tavolify. Money your website took is refunded by your website.

Parametry

PoleTypOpis
orderwymagane path string

Treść żądania

PoleTypOpis
reasonwymagane string At most 160 characters.

Odpowiedzi

  • 200The order after the step
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.
  • 404No such order in this business.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/cancel" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Guest called to cancel"
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/cancel', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "reason": "Guest called to cancel"
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'reason' => 'Guest called to cancel',
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/cancel');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
    "number": 42,
    "branch_id": 1,
    "business_date": "2026-11-12",
    "type": "takeaway",
    "channel": "online",
    "source": "pos",
    "status": "open",
    "currency": "PLN",
    "prices_include_tax": true,
    "subtotal": 5380,
    "discount_total": 0,
    "net_total": 4981,
    "tax_total": 399,
    "total": 5380,
    "taxes": [
        {
            "code": "B",
            "rate": 800,
            "net": 4981,
            "tax": 399
        }
    ],
    "created_at": "2026-11-12T17:02:11+00:00",
    "closed_at": null,
    "updated_at": "2026-11-12T17:02:11+00:00",
    "order_source": {
        "key": "src_k3m9x2p7q4wz",
        "name": "mybistro.pl",
        "color": "#BE123C",
        "code": "WEB"
    },
    "external_id": "WEB-1001",
    "note": "Ring twice",
    "fulfilment": {
        "status": "rejected",
        "requested_for": "2026-11-12T18:00:00+00:00",
        "ready_at": null,
        "accepted_at": null,
        "reject_reason": null
    },
    "customer": {
        "name": "Anna Nowak",
        "phone": "+48 600 100 200",
        "email": "anna@example.com"
    },
    "lines": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
            "item_id": 12,
            "variant_id": null,
            "name": {
                "en": "Doner wrap",
                "pl": "Kebab w bułce"
            },
            "variant_name": null,
            "unit_price": 2690,
            "quantity": 2000,
            "discount": 0,
            "net": 4981,
            "tax": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "status": "active",
            "note": "extra crispy",
            "modifiers": [
                {
                    "modifier_id": 31,
                    "name": {
                        "en": "Garlic sauce"
                    },
                    "quantity": 1,
                    "price": 0,
                    "action": null
                }
            ]
        }
    ],
    "payments": [
        {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    ]
}
Odpowiedź 422
{
    "error": "status_not_allowed",
    "message": "This order cannot move to that status now."
}

Opis API

Payments

Record payments taken outside Tavolify.

Record a payment

POST /api/v1/orders/{order}/payments Zakres: payments:write

Scope: payments:write. Records money taken outside Tavolify (cash, a card on another terminal, other). A till bill closes when fully paid; an online order is handed over with /complete. Method loyalty pays with the credit of the loyalty card linked to the order (also needs loyalty:write and the token owner's loyalty.redeem permission); refused with 422 loyalty_member_unknown (no card on the order), loyalty_insufficient, loyalty_share_exceeded, loyalty_min_redeem or payment_exceeds_due. A rejected or cancelled order gives the credit back.

Parametry

PoleTypOpis
orderwymagane path string

Treść żądania

PoleTypOpis
methodwymagane string One of: cash, card_external, other, loyalty.
amountwymagane integer Applied to the bill, at most what is left to pay
tendered integer Cash given (change is computed)
tip integer
card_last4 string At most 4 characters.
auth_code string At most 32 characters.

Odpowiedzi

  • 201Recorded
  • 422Validation error (errors per field) or an order rule (stable error code).
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/payments" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "cash",
    "amount": 5380,
    "tendered": 6000
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/payments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "method": "cash",
      "amount": 5380,
      "tendered": 6000
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'method' => 'cash',
    'amount' => 5380,
    'tendered' => 6000,
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/orders/01JF8Z3W2K5R6T7Y8U9I0OPQRS/payments');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 201
{
    "data": {
        "payment": {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPBBB",
            "method": "cash",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 620,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:40:00+00:00"
        },
        "order": {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
            "number": 42,
            "branch_id": 1,
            "business_date": "2026-11-12",
            "type": "takeaway",
            "channel": "api",
            "source": "pos",
            "status": "closed",
            "currency": "PLN",
            "prices_include_tax": true,
            "subtotal": 5380,
            "discount_total": 0,
            "net_total": 4981,
            "tax_total": 399,
            "total": 5380,
            "taxes": [
                {
                    "code": "B",
                    "rate": 800,
                    "net": 4981,
                    "tax": 399
                }
            ],
            "created_at": "2026-11-12T17:02:11+00:00",
            "closed_at": null,
            "updated_at": "2026-11-12T17:02:11+00:00",
            "order_source": null,
            "external_id": "WEB-1001",
            "note": "Ring twice",
            "fulfilment": null
        }
    }
}
Odpowiedź 422
{
    "error": "payment_exceeds_due",
    "message": "The amount is more than what is left to pay."
}

Opis API

Loyalty

The business's loyalty programme on your own website: cards by phone, balances, history, the QR card page, and orders that earn.

Get the loyalty programme

GET /api/v1/loyalty/program Zakres: loyalty:read

Scope: loyalty:read; the token owner needs loyalty.view. The rules to explain on your website, and the current terms to show before a guest gets a card.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 429Too many requests (120 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/loyalty/program" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/loyalty/program', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/loyalty/program');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": {
        "name": "Bistro Card",
        "type": "spend",
        "currency": "PLN",
        "earn_basis": "gross",
        "spend_step": 20000,
        "spend_reward": 3000,
        "points_per_unit": 0,
        "points_block": 0,
        "points_block_value": 0,
        "stamps_every": 0,
        "stamp_reward": 0,
        "welcome_bonus": 500,
        "birthday_reward": 1000,
        "min_redeem": 0,
        "max_share_percent": 100,
        "expiry_months": 12,
        "tiers": [],
        "terms": {
            "version": 2,
            "text": {
                "en": "Credit expires 12 months after it is earned.",
                "pl": "Środki wygasają 12 miesięcy po naliczeniu."
            }
        }
    }
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Find or create a card by phone

POST /api/v1/loyalty/members Zakres: loyalty:write

Scope: loyalty:write; the token owner needs loyalty.manage. Returns the card of this phone number (200, created: false) or creates one (201): a known customer with this number gets the card, otherwise a new customer is created. Idempotent and safe to retry: the phone number is the key, two calls at once make one card. An existing card or customer is never changed by this call. Polish numbers are accepted in every usual form (600 100 200, +48 600-100-200, 0048…, 48…); other countries need the + and the calling code. When the business has terms, send terms: true after the guest accepted them; marketing consent is separate and only recorded when true. Check that the guest owns the number (a text code or your own sign-in) before you show the card.

Treść żądania

PoleTypOpis
phonewymagane string At most 32 characters.
name string Only used for a new customer. At most 120 characters.
email string Only used for a new customer. At most 190 characters.
terms boolean The guest accepted the programme terms (required when the business has terms).
marketing boolean The guest agreed to marketing messages (own checkbox, never pre-ticked).

Odpowiedzi

  • 200The existing card
  • 201A new card
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 422Invalid phone number (validation error on phone), or terms not accepted.
  • 429Too many requests (30 per minute per token). Wait for the seconds in Retry-After.

curl

curl -X POST "https://your-restaurant.tavolify.com/api/v1/loyalty/members" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+48 600 100 200",
    "name": "Anna Nowak",
    "email": "anna@example.com",
    "terms": true,
    "marketing": false
}'

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/loyalty/members', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "phone": "+48 600 100 200",
      "name": "Anna Nowak",
      "email": "anna@example.com",
      "terms": true,
      "marketing": false
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$payload = [
    'phone' => '+48 600 100 200',
    'name' => 'Anna Nowak',
    'email' => 'anna@example.com',
    'terms' => true,
    'marketing' => false,
];

$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/loyalty/members');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": {
        "card_number": "HKQ4M8T2XN7BK",
        "card_url": "https://mybistro.pl/card/5f0c3e1a9b7d4c2e8f6a1b3c5d7e9f0a2b4c6d8e",
        "status": "active",
        "enrolled_at": "2026-11-12T17:02:11+00:00",
        "programme": {
            "name": "Bistro Card",
            "type": "spend"
        },
        "balance": {
            "currency": "PLN",
            "credit": 3500,
            "points": 0,
            "progress": 7600,
            "next_reward": {
                "spend_needed": 12400,
                "reward": 3000
            },
            "tier": null,
            "bonus_percent": 0,
            "lifetime_spend": 47600,
            "expiring": {
                "amount": 3000,
                "date": "2026-12-01"
            }
        }
    },
    "created": false
}
Odpowiedź 201
{
    "data": {
        "card_number": "HKQ4M8T2XN7BK",
        "card_url": "https://mybistro.pl/card/5f0c3e1a9b7d4c2e8f6a1b3c5d7e9f0a2b4c6d8e",
        "status": "active",
        "enrolled_at": "2026-11-12T17:02:11+00:00",
        "programme": {
            "name": "Bistro Card",
            "type": "spend"
        },
        "balance": {
            "currency": "PLN",
            "credit": 500,
            "points": 0,
            "progress": 0,
            "next_reward": {
                "spend_needed": 20000,
                "reward": 3000
            },
            "tier": null,
            "bonus_percent": 0,
            "lifetime_spend": 0,
            "expiring": null
        }
    },
    "created": true
}

Get a card and its balance

GET /api/v1/loyalty/members/{card} Zakres: loyalty:read

Scope: loyalty:read; the token owner needs loyalty.view. By card number or by phone number. A blocked card is returned with status blocked (it earns and pays nothing).

Parametry

PoleTypOpis
cardwymagane path string Card number, or the member's phone number (URL-encode the plus: %2B48600100200). At most 64 characters.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 404No card with this number or phone (also: a card of another business, or an erased card).
  • 429Too many requests (60 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": {
        "card_number": "HKQ4M8T2XN7BK",
        "card_url": "https://mybistro.pl/card/5f0c3e1a9b7d4c2e8f6a1b3c5d7e9f0a2b4c6d8e",
        "status": "active",
        "enrolled_at": "2026-11-12T17:02:11+00:00",
        "programme": {
            "name": "Bistro Card",
            "type": "spend"
        },
        "balance": {
            "currency": "PLN",
            "credit": 3500,
            "points": 0,
            "progress": 7600,
            "next_reward": {
                "spend_needed": 12400,
                "reward": 3000
            },
            "tier": null,
            "bonus_percent": 0,
            "lifetime_spend": 47600,
            "expiring": {
                "amount": 3000,
                "date": "2026-12-01"
            }
        }
    }
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

List a card's history

GET /api/v1/loyalty/members/{card}/entries Zakres: loyalty:read

Scope: loyalty:read; the token owner needs loyalty.view. The card's ledger, newest first: earned, paid with at the till, reversed after a refund, expired, adjusted by staff (without the staff note), welcome and birthday rewards. Lines of unit spend are the progress towards the next reward.

Parametry

PoleTypOpis
cardwymagane path string Card number, or the member's phone number (URL-encode the plus: %2B48600100200). At most 64 characters.
unit query string One of: credit, points, spend.
type query string One of: earn, welcome, birthday, opening, convert, redeem, restore, reversal, expire, adjust.
page query integer
per_page query integer From 1 to 100. Default 50.

Odpowiedzi

  • 200OK
  • 401Missing, invalid or expired token.
  • 403Token scope or staff permission missing.
  • 404No card with this number or phone (also: a card of another business, or an erased card).
  • 422Unknown unit or type.
  • 429Too many requests (60 per minute per token). Wait for the seconds in Retry-After.

curl

curl "https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK/entries" \
  -H "Authorization: Bearer $TAVOLIFY_TOKEN" \
  -H "Accept: application/json"

JavaScript

const response = await fetch('https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK/entries', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.TAVOLIFY_TOKEN}`,
    Accept: 'application/json',
  },
});
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? result.message);
console.log(result.data);

PHP

<?php
$ch = curl_init('https://your-restaurant.tavolify.com/api/v1/loyalty/members/HKQ4M8T2XN7BK/entries');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TAVOLIFY_TOKEN'),
        'Accept: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
    throw new RuntimeException($result['error'] ?? $result['message'] ?? 'HTTP ' . $status);
}
print_r($result['data']);
Odpowiedź 200
{
    "data": [
        {
            "id": 981,
            "created_at": "2026-11-12T18:10:00+00:00",
            "unit": "credit",
            "type": "earn",
            "amount": 3000,
            "balance_after": 3500,
            "order_id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
            "expires_at": "2027-11-12T18:10:00+00:00"
        },
        {
            "id": 975,
            "created_at": "2026-11-01T12:00:00+00:00",
            "unit": "credit",
            "type": "welcome",
            "amount": 500,
            "balance_after": 500,
            "order_id": null,
            "expires_at": "2027-11-01T12:00:00+00:00"
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 50,
        "total": 2
    }
}
Odpowiedź 401
{
    "message": "Unauthenticated."
}

Obiekty

Order

PoleTypOpis
id string ULID
number integer Running number per branch and business day
branch_id integer
business_date string
type string One of: dine_in, takeaway, delivery, pickup, qr, online, kiosk.
channel string Where the order was taken. Orders with an order source are online; API orders without one are api. One of: pos, qr, online, kiosk, api, delivery.
source string Record origin (import = copied sales history). Not the order source: see order_source. One of: pos, import.
status string One of: open, closed, unpaid, voided, written_off.
currency string
prices_include_tax boolean
subtotal integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
discount_total integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
net_total integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
tax_total integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
total integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
taxes array of object
created_at string
closed_at string | null
updated_at string
order_source object | null The order source as it was when the order came in.
order_source.key string
order_source.name string
order_source.color string
order_source.code string | null
external_id string | null Your own order number (idempotency key per source)
note string | null
fulfilment Fulfilment Online-order flow of orders from an order source, the online shop or a delivery app (null for till bills).
fulfilment.status string One of: pending, accepted, ready, completed, rejected, awaiting_payment, awaiting_cash.
fulfilment.requested_for string | null The time the guest asked for (null = as soon as possible)
fulfilment.ready_at string | null Promised (after accept) or actual (after ready) time
fulfilment.accepted_at string | null
fulfilment.reject_reason string | null
customer object | null Only on single-order answers.
customer.name string
customer.phone string
customer.email string
customer.address string
customer.postcode string
customer.city string
customer.address_note string
lines array of OrderLine Only on single-order answers.
payments array of Payment Only on single-order answers.
loyalty object | null Single order only: the loyalty card linked to the order. The bill earns on this card when it closes.
loyalty.card_number string

OrderLine

PoleTypOpis
id string
item_id integer | null
variant_id integer | null
name object Text per language code, e.g. {"en": "Doner wrap", "pl": "Kebab w bułce"}.
variant_name object | null
unit_price integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
quantity integer Milli-units (1000 = 1)
discount integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
net integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
tax integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
total integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
taxes array of object
status string One of: active, voided.
note string | null
modifiers array of object
modifiers[].modifier_id integer
modifiers[].name object Text per language code, e.g. {"en": "Doner wrap", "pl": "Kebab w bułce"}.
modifiers[].quantity integer
modifiers[].price integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
modifiers[].action string | null add, remove, extra, less …

Fulfilment

Online-order flow of orders from an order source, the online shop or a delivery app (null for till bills).

PoleTypOpis
status string One of: pending, accepted, ready, completed, rejected, awaiting_payment, awaiting_cash.
requested_for string | null The time the guest asked for (null = as soon as possible)
ready_at string | null Promised (after accept) or actual (after ready) time
accepted_at string | null
reject_reason string | null

Payment

PoleTypOpis
id string
method string cash, card_external, card_terminal, online, other, …
applied integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
rounding integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
tip integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
change integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
refunded integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
card_last4 string | null
created_at string

OrderSource

PoleTypOpis
key string Stable key, never changes
name string At most 40 characters.
color string #RRGGBB
code string | null At most 8 characters.
auto_accept boolean Orders go straight to the kitchen instead of waiting for staff on the till
last_order_at string | null
created_at string

LoyaltyMember

A loyalty card. Never carries the member's name, phone or e-mail.

PoleTypOpis
card_number string Printed card number, e.g. HKQ4M8T2XN7BK (or a number carried over from the previous system).
card_url string The member's public card page. Put this address in the QR code you show; staff scan it at the till.
status string One of: active, blocked.
enrolled_at string
programme object | null
programme.name string
programme.type string One of: spend, points, stamps.
balance LoyaltyBalance
balance.currency string
balance.credit integer Credit to pay with at the till, minor units.
balance.points integer
balance.progress integer Towards the next reward: spent minor units (spend programme) or counted pieces (stamps).
balance.next_reward object | null spend: {spend_needed, reward}; stamps: {stamps_needed, reward}; points: null.
balance.next_reward.spend_needed integer
balance.next_reward.stamps_needed integer
balance.next_reward.reward integer
balance.tier string | null
balance.bonus_percent integer
balance.lifetime_spend integer
balance.expiring object | null The next amount that expires soon.
balance.expiring.amount integer
balance.expiring.date string

LoyaltyBalance

PoleTypOpis
currency string
credit integer Credit to pay with at the till, minor units.
points integer
progress integer Towards the next reward: spent minor units (spend programme) or counted pieces (stamps).
next_reward object | null spend: {spend_needed, reward}; stamps: {stamps_needed, reward}; points: null.
next_reward.spend_needed integer
next_reward.stamps_needed integer
next_reward.reward integer
tier string | null
bonus_percent integer
lifetime_spend integer
expiring object | null The next amount that expires soon.
expiring.amount integer
expiring.date string

LoyaltyEntry

One line of the card's ledger. Lines are never changed; a correction is a new line.

PoleTypOpis
id integer
created_at string
unit string spend = progress towards the next reward. One of: credit, points, spend.
type string One of: earn, welcome, birthday, opening, convert, redeem, restore, reversal, expire, adjust.
amount integer Signed: + added, − taken.
balance_after integer
order_id string | null
expires_at string | null

LoyaltyProgramme

The programme rules, in minor units.

PoleTypOpis
name string
type string
currency string
earn_basis string
spend_step integer
spend_reward integer
points_per_unit integer
points_block integer
points_block_value integer
stamps_every integer
stamp_reward integer
welcome_bonus integer
birthday_reward integer
min_redeem integer
max_share_percent integer
expiry_months integer | null
tiers array of object
tiers[].name string
tiers[].min_spend integer
tiers[].percent integer
terms object | null Current terms the guest accepts when a card is created: version and text per language.
terms.version integer
terms.text object

Item

PoleTypOpis
id integer
category_id integer
name object Text per language code, e.g. {"en": "Doner wrap", "pl": "Kebab w bułce"}.
description object | null
price integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
sku string | null
barcode string | null
is_active boolean
availability object | null Days and hours the item is sold (null = always).
dietary array of string
allergens array of string EU allergen codes
image_url string | null
variants array of object Sizes; when present, send variant_id with the line.
variants[].id integer
variants[].name object Text per language code, e.g. {"en": "Doner wrap", "pl": "Kebab w bułce"}.
variants[].price integer Minor units of the order currency (e.g. 2690 = 26.90 PLN).
modifier_groups array of ModifierGroup
branches array of object Per-branch price and availability.
branches[].branch_id integer
branches[].price integer | null
branches[].is_available boolean
branches[].sold_out boolean

ModifierGroup

PoleTypOpis
id integer
name object Text per language code, e.g. {"en": "Doner wrap", "pl": "Kebab w bułce"}.
min integer At least this many choices
max integer | null At most this many (null = no limit)
free integer This many choices are free
repeat boolean The same choice may be picked several times
kind string
max_extra integer | null
mix_label string | null
modifiers array of Modifier

Branch

PoleTypOpis
id integer
name string
slug string
city string | null
country string
timezone string
is_active boolean

WebhookPayload

PoleTypOpis
id string Delivery id (ULID); the same id on every retry: use it to ignore duplicates
event string
business_id integer
created_at string
data object The order (see Order) or, for payment events, {order_id, payment}.

Error

PoleTypOpis
errorwymagane string Stable code, e.g. item_unavailable, modifiers_too_few, payment_exceeds_due, source_unknown, status_not_allowed, order_not_paid, forbidden.
messagewymagane string Translated text for people (language of the token owner).
permission string Only with error=forbidden: the staff permission the token owner is missing.
scope string Only with error=forbidden from a body field: the token scope that is missing (e.g. loyalty:write for loyalty on POST /orders).

Webhooki

Events you can subscribe to in Settings → Webhooks. See Webhooks for the signature.

order.created

An order was created. API orders: sent once the lines are on (data: the order with lines and payments). Till orders: sent when the bill opens.

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "order.created",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "number": 42,
        "branch_id": 1,
        "business_date": "2026-11-12",
        "type": "takeaway",
        "channel": "online",
        "source": "pos",
        "status": "open",
        "currency": "PLN",
        "prices_include_tax": true,
        "subtotal": 5380,
        "discount_total": 0,
        "net_total": 4981,
        "tax_total": 399,
        "total": 5380,
        "taxes": [
            {
                "code": "B",
                "rate": 800,
                "net": 4981,
                "tax": 399
            }
        ],
        "created_at": "2026-11-12T17:02:11+00:00",
        "closed_at": null,
        "updated_at": "2026-11-12T17:02:11+00:00",
        "order_source": {
            "key": "src_k3m9x2p7q4wz",
            "name": "mybistro.pl",
            "color": "#BE123C",
            "code": "WEB"
        },
        "external_id": "WEB-1001",
        "note": "Ring twice",
        "fulfilment": {
            "status": "pending",
            "requested_for": "2026-11-12T18:00:00+00:00",
            "ready_at": null,
            "accepted_at": null,
            "reject_reason": null
        },
        "customer": {
            "name": "Anna Nowak",
            "phone": "+48 600 100 200",
            "email": "anna@example.com"
        },
        "lines": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
                "item_id": 12,
                "variant_id": null,
                "name": {
                    "en": "Doner wrap",
                    "pl": "Kebab w bułce"
                },
                "variant_name": null,
                "unit_price": 2690,
                "quantity": 2000,
                "discount": 0,
                "net": 4981,
                "tax": 399,
                "total": 5380,
                "taxes": [
                    {
                        "code": "B",
                        "rate": 800,
                        "net": 4981,
                        "tax": 399
                    }
                ],
                "status": "active",
                "note": "extra crispy",
                "modifiers": [
                    {
                        "modifier_id": 31,
                        "name": {
                            "en": "Garlic sauce"
                        },
                        "quantity": 1,
                        "price": 0,
                        "action": null
                    }
                ]
            }
        ],
        "payments": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
                "method": "online",
                "applied": 5380,
                "rounding": 0,
                "tip": 0,
                "change": 0,
                "refunded": 0,
                "card_last4": null,
                "created_at": "2026-11-12T17:02:11+00:00"
            }
        ]
    }
}

order.closed

An order was fully paid and closed (data: order with lines and payments).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "order.closed",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "number": 42,
        "branch_id": 1,
        "business_date": "2026-11-12",
        "type": "takeaway",
        "channel": "online",
        "source": "pos",
        "status": "closed",
        "currency": "PLN",
        "prices_include_tax": true,
        "subtotal": 5380,
        "discount_total": 0,
        "net_total": 4981,
        "tax_total": 399,
        "total": 5380,
        "taxes": [
            {
                "code": "B",
                "rate": 800,
                "net": 4981,
                "tax": 399
            }
        ],
        "created_at": "2026-11-12T17:02:11+00:00",
        "closed_at": "2026-11-12T18:05:00+00:00",
        "updated_at": "2026-11-12T17:02:11+00:00",
        "order_source": {
            "key": "src_k3m9x2p7q4wz",
            "name": "mybistro.pl",
            "color": "#BE123C",
            "code": "WEB"
        },
        "external_id": "WEB-1001",
        "note": "Ring twice",
        "fulfilment": {
            "status": "pending",
            "requested_for": "2026-11-12T18:00:00+00:00",
            "ready_at": null,
            "accepted_at": null,
            "reject_reason": null
        },
        "customer": {
            "name": "Anna Nowak",
            "phone": "+48 600 100 200",
            "email": "anna@example.com"
        },
        "lines": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
                "item_id": 12,
                "variant_id": null,
                "name": {
                    "en": "Doner wrap",
                    "pl": "Kebab w bułce"
                },
                "variant_name": null,
                "unit_price": 2690,
                "quantity": 2000,
                "discount": 0,
                "net": 4981,
                "tax": 399,
                "total": 5380,
                "taxes": [
                    {
                        "code": "B",
                        "rate": 800,
                        "net": 4981,
                        "tax": 399
                    }
                ],
                "status": "active",
                "note": "extra crispy",
                "modifiers": [
                    {
                        "modifier_id": 31,
                        "name": {
                            "en": "Garlic sauce"
                        },
                        "quantity": 1,
                        "price": 0,
                        "action": null
                    }
                ]
            }
        ],
        "payments": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
                "method": "online",
                "applied": 5380,
                "rounding": 0,
                "tip": 0,
                "change": 0,
                "refunded": 0,
                "card_last4": null,
                "created_at": "2026-11-12T17:02:11+00:00"
            }
        ]
    }
}

order.voided

An open order was voided or cancelled.

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "order.voided",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "number": 42,
        "branch_id": 1,
        "business_date": "2026-11-12",
        "type": "takeaway",
        "channel": "online",
        "source": "pos",
        "status": "voided",
        "currency": "PLN",
        "prices_include_tax": true,
        "subtotal": 5380,
        "discount_total": 0,
        "net_total": 4981,
        "tax_total": 399,
        "total": 5380,
        "taxes": [
            {
                "code": "B",
                "rate": 800,
                "net": 4981,
                "tax": 399
            }
        ],
        "created_at": "2026-11-12T17:02:11+00:00",
        "closed_at": null,
        "updated_at": "2026-11-12T17:02:11+00:00",
        "order_source": {
            "key": "src_k3m9x2p7q4wz",
            "name": "mybistro.pl",
            "color": "#BE123C",
            "code": "WEB"
        },
        "external_id": "WEB-1001",
        "note": "Ring twice",
        "fulfilment": {
            "status": "pending",
            "requested_for": "2026-11-12T18:00:00+00:00",
            "ready_at": null,
            "accepted_at": null,
            "reject_reason": null
        },
        "customer": {
            "name": "Anna Nowak",
            "phone": "+48 600 100 200",
            "email": "anna@example.com"
        },
        "lines": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
                "item_id": 12,
                "variant_id": null,
                "name": {
                    "en": "Doner wrap",
                    "pl": "Kebab w bułce"
                },
                "variant_name": null,
                "unit_price": 2690,
                "quantity": 2000,
                "discount": 0,
                "net": 4981,
                "tax": 399,
                "total": 5380,
                "taxes": [
                    {
                        "code": "B",
                        "rate": 800,
                        "net": 4981,
                        "tax": 399
                    }
                ],
                "status": "active",
                "note": "extra crispy",
                "modifiers": [
                    {
                        "modifier_id": 31,
                        "name": {
                            "en": "Garlic sauce"
                        },
                        "quantity": 1,
                        "price": 0,
                        "action": null
                    }
                ]
            }
        ],
        "payments": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
                "method": "online",
                "applied": 5380,
                "rounding": 0,
                "tip": 0,
                "change": 0,
                "refunded": 0,
                "card_last4": null,
                "created_at": "2026-11-12T17:02:11+00:00"
            }
        ]
    }
}

payment.created

A payment was recorded (data: order_id, payment).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "payment.created",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "order_id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "payment": {
            "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
            "method": "online",
            "applied": 5380,
            "rounding": 0,
            "tip": 0,
            "change": 0,
            "refunded": 0,
            "card_last4": null,
            "created_at": "2026-11-12T17:02:11+00:00"
        }
    }
}

payment.refunded

A payment was refunded (data: order_id, payment_id, amount, reason).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "payment.refunded",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "order_id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "payment_id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
        "amount": 5380,
        "reason": "Order rejected"
    }
}

item.sold_out

An item was marked sold out in a branch (data: item_id, branch_id, name).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "item.sold_out",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "item_id": 12,
        "branch_id": 1,
        "name": {
            "en": "Doner wrap",
            "pl": "Kebab w bułce"
        }
    }
}

item.back_on_sale

An item is on sale again in a branch (data: item_id, branch_id, name).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "item.back_on_sale",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "item_id": 12,
        "branch_id": 1,
        "name": {
            "en": "Doner wrap",
            "pl": "Kebab w bułce"
        }
    }
}

ping

A test delivery from Settings → Webhooks → Send test.

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "ping",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "message": "Test delivery from Tavolify."
    }
}

order.status_changed

An online, delivery-app or order-source order moved: accepted, ready, completed or rejected (data: the order plus status_change {from, to}).

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9L",
    "event": "order.status_changed",
    "business_id": 7,
    "created_at": "2026-11-12T17:05:00+00:00",
    "data": {
        "id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
        "number": 42,
        "branch_id": 1,
        "business_date": "2026-11-12",
        "type": "takeaway",
        "channel": "online",
        "source": "pos",
        "status": "open",
        "currency": "PLN",
        "prices_include_tax": true,
        "subtotal": 5380,
        "discount_total": 0,
        "net_total": 4981,
        "tax_total": 399,
        "total": 5380,
        "taxes": [
            {
                "code": "B",
                "rate": 800,
                "net": 4981,
                "tax": 399
            }
        ],
        "created_at": "2026-11-12T17:02:11+00:00",
        "closed_at": null,
        "updated_at": "2026-11-12T17:02:11+00:00",
        "order_source": {
            "key": "src_k3m9x2p7q4wz",
            "name": "mybistro.pl",
            "color": "#BE123C",
            "code": "WEB"
        },
        "external_id": "WEB-1001",
        "note": "Ring twice",
        "fulfilment": {
            "status": "accepted",
            "requested_for": "2026-11-12T18:00:00+00:00",
            "ready_at": "2026-11-12T18:00:00+00:00",
            "accepted_at": null,
            "reject_reason": null
        },
        "customer": {
            "name": "Anna Nowak",
            "phone": "+48 600 100 200",
            "email": "anna@example.com"
        },
        "lines": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPLKJ",
                "item_id": 12,
                "variant_id": null,
                "name": {
                    "en": "Doner wrap",
                    "pl": "Kebab w bułce"
                },
                "variant_name": null,
                "unit_price": 2690,
                "quantity": 2000,
                "discount": 0,
                "net": 4981,
                "tax": 399,
                "total": 5380,
                "taxes": [
                    {
                        "code": "B",
                        "rate": 800,
                        "net": 4981,
                        "tax": 399
                    }
                ],
                "status": "active",
                "note": "extra crispy",
                "modifiers": [
                    {
                        "modifier_id": 31,
                        "name": {
                            "en": "Garlic sauce"
                        },
                        "quantity": 1,
                        "price": 0,
                        "action": null
                    }
                ]
            }
        ],
        "payments": [
            {
                "id": "01JF8Z3W2K5R6T7Y8U9I0OPAAA",
                "method": "online",
                "applied": 5380,
                "rounding": 0,
                "tip": 0,
                "change": 0,
                "refunded": 0,
                "card_last4": null,
                "created_at": "2026-11-12T17:02:11+00:00"
            }
        ],
        "status_change": {
            "from": "pending",
            "to": "accepted"
        }
    }
}

loyalty.entry_created

A loyalty card changed: earned, paid with at the till, reversed after a refund, expired, adjusted, welcome or birthday reward. data: card_number, entry (see LoyaltyEntry) and the balance after it (credit, points, progress). Progress lines are not sent on their own; imported opening balances are not sent. Never carries personal data.

Przykład
{
    "id": "01JF9A0B1C2D3E4F5G6H7J8K9M",
    "event": "loyalty.entry_created",
    "business_id": 7,
    "created_at": "2026-11-12T18:10:00+00:00",
    "data": {
        "card_number": "HKQ4M8T2XN7BK",
        "entry": {
            "id": 981,
            "created_at": "2026-11-12T18:10:00+00:00",
            "unit": "credit",
            "type": "earn",
            "amount": 3000,
            "balance_after": 3500,
            "order_id": "01JF8Z3W2K5R6T7Y8U9I0OPQRS",
            "expires_at": "2027-11-12T18:10:00+00:00"
        },
        "balance": {
            "credit": 3500,
            "points": 0,
            "progress": 7600
        }
    }
}