Flavoso APIWebhooks and eventsWebhooks

Webhooks

Flavoso reports every change right away with a signed POST to your server: setup, receiving, acknowledging, retries and the log.

How webhooks work

When something changes in a restaurant that connected your app, an event is created, and Flavoso sends it with a POST to your app's webhook address. Your app does not have to keep asking: a new order usually arrives within a few seconds.

Change in the restaurant

Dashboard, kitchen, phone app, receipt printer or API

Event

Envelope with id, type, data; frozen on the first send

Delivery

POST to your address, signed with Flavoso-Signature

Delivered

your server answers with 2xx within 10 seconds

Otherwise: another attempt 8 attempts over almost two days

  1. 1immediately
  2. 21 min
  3. 35 min
  4. 430 min
  5. 52 h
  6. 66 h
  7. 712 h
  8. 824 h

If at least 10 attempts in a row fail over 24 hours, the address is switched off and you get an e-mail.

Whatever got lost, you catch up with GET /events, 30 days back.

An app has exactly one webhook. It receives the events of all restaurants that connected your app, as far as the connection has the matching read scope (see Scopes and events). It also receives the events about your connections themselves (app.*).

Setting up

  1. 1

    Enter the address

    An https address, reachable from the internet, at most 500 characters. Flavoso does not accept addresses in private networks.

    Console › App › Webhooks

  2. 2

    Choose the events

    Only what your app needs. A POS needs order.*, often menu.* and store.*, always app.disconnected.

  3. 3

    Copy the signing secret

    It starts with whsec_. Your server checks every delivery with it. You can show it again or rotate it at any time in the console under Webhooks.

  4. 4

    Send a test event

    Checks the whole path: address, signature, response. The result appears in the deliveries below.

“Create new signing secret” replaces the secret; for 24 hours Flavoso then signs with both. You can switch a webhook off and on again, or delete it: then the address, the secret and all deliveries including the log are gone. The events themselves stay available for 30 days through GET /events.

The envelope

Every delivery is a JSON envelope. It is the same for webhooks and GET /events:

FieldMeaning
idThe event's id (evt_…). That is how you detect duplicates.
objectAlways event.
typeThe type, for example order.created. See Event types.
created_atWhen the change happened.
api_versionThe version date, as in Flavoso-Api-Version.
livemodefalse for the test restaurant.
tenant_idThe restaurant.
connection_idYour app's connection to this restaurant. Use it to find the matching tokens.
dataThe content: data.object is the object in the same shape the routes return, plus previous_status, changed or previous, depending on the type.
{
  "id": "evt_2bf5a9b06b75442a9244e884a1acfc57",
  "object": "event",
  "type": "order.created",
  "created_at": "2026-10-04T17:45:10.410Z",
  "api_version": "2026-10-08",
  "livemode": true,
  "tenant_id": "7d0e5b8a-3c41-4f2e-9a6b-1e2d3c4b5a69",
  "connection_id": "4b3f7c1e-9a2d-4e8b-b5f6-0c1d2e3f4a5b",
  "data": {
    "object": {
      "object": "order",
      "id": "cce01f52-5188-410f-9659-6ce26ba9b784",
      "code": "3271CD",
      "type": "delivery",
      "status": "pending",
      "source": "storefront",
      "created_at": "2026-10-04T17:44:59.942Z",
      "updated_at": "2026-10-04T17:45:10.118Z",
      "requested_for": null,
      "currency": "EUR",
      "amounts": {
        "subtotal": 2490,
        "delivery_fee": 250,
        "service_fee": 0,
        "discount": 0,
        "tip": 200,
        "total": 2940
      },
      "tax": {
        "included": true,
        "lines": [
          {
            "rate_bps": 700,
            "gross": 2421,
            "net": 2263,
            "tax": 158
          },
          {
            "rate_bps": 1900,
            "gross": 319,
            "net": 268,
            "tax": 51
          }
        ]
      },
      "payment": {
        "method": "online",
        "status": "paid",
        "paid_at": "2026-10-04T17:45:10.000Z"
      },
      "customer": {
        "name": "Alex",
        "phone": "+49 151 00000000"
      },
      "delivery": {
        "address": "Musterstraße 1",
        "postal_code": "10115",
        "city": "Berlin"
      },
      "table": null,
      "note": "Please ring twice",
      "external_id": null,
      "acknowledged_at": null,
      "items": [
        {
          "id": "e9c0ce66-7f7f-42cf-a4bf-7d66f87768c1",
          "product_id": "303b87c9-58a3-4644-9a8b-2841cc1f89d6",
          "name": "Pizza Margherita",
          "quantity": 2,
          "unit_price": 1100,
          "line_total": 2200,
          "tax_rate_bps": 700,
          "options": [
            {
              "id": "1f2e3d4c-5b6a-4798-8a7b-6c5d4e3f2a1b",
              "name": "Extra cheese",
              "price_delta": 150,
              "group_id": "8e7d6c5b-4a39-4281-b0f9-e8d7c6b5a4f3",
              "group_name": "Extras"
            }
          ]
        },
        {
          "id": "0d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
          "product_id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
          "name": "Cola 0,33 l",
          "quantity": 1,
          "unit_price": 290,
          "line_total": 290,
          "tax_rate_bps": 1900,
          "options": []
        }
      ]
    }
  }
}

These headers come with it:

HeaderMeaning
Flavoso-SignatureThe signature, see Verifying signatures.
Flavoso-Event-IdThe same as id in the envelope.
Flavoso-Event-TypeThe same as type.
Flavoso-Delivery-AttemptThe attempt number of this delivery, starting at 1. Every send counts, whether automatic or resent by hand.
Content-Typeapplication/json
User-AgentFlavoso-Webhooks/1.0 (+https://docs.flavoso.com/en/api), so you can recognise deliveries in your logs.
POST /flavoso/webhook HTTP/1.1
Host: pos.example.com
Content-Type: application/json
User-Agent: Flavoso-Webhooks/1.0 (+https://docs.flavoso.com/en/api)
Flavoso-Signature: t=1759598710,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Flavoso-Event-Id: evt_2bf5a9b06b75442a9244e884a1acfc57
Flavoso-Event-Type: order.created
Flavoso-Delivery-Attempt: 1

{"id":"evt_2bf5a9b06b75442a9244e884a1acfc57","object":"event","type":"order.created",...}

Receiving and acknowledging

A delivery counts as delivered when your server answers with any 2xx within 10 seconds. What the body of your response says does not matter.

  • Verify the signature first, then respond, then do the work: long processing before the response runs into the timeout, and Flavoso sends the same delivery again.
  • A redirect (3xx) counts as a failure. Register the final address.
  • Remember the id of every processed event and skip duplicates.
import express from "express";
import { verifyFlavosoSignature } from "./flavoso-signature.js"; // see "Verifying signatures"

const app = express();
const SECRET = process.env.FLAVOSO_WEBHOOK_SECRET; // whsec_...

// express.raw: the body stays exactly as it arrived, which the signature needs
app.post("/flavoso/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyFlavosoSignature(raw, req.get("Flavoso-Signature") ?? "", SECRET)) {
    return res.status(400).send("invalid signature");
  }
  const event = JSON.parse(raw);
  // Answer within 10 seconds, then work. Duplicates are possible: the event id decides.
  res.sendStatus(200);
  if (await alreadyHandled(event.id)) return;
  await queue.add(event); // your job queue: book the order, update the menu, ...
});

app.listen(3000);

Retries

Anything but 2xx counts as a failure: an error code, a timeout, a redirect, an unreachable server. Flavoso then tries again, 8 attempts in total over almost two days. A POS that is off overnight still gets everything in the morning.

AttemptWait since the previous
1immediately
21 min
35 min
430 min
52 h
66 h
712 h
824 h

Each wait counts from the previous attempt. Flavoso-Delivery-Attempt counts every send, including manual ones. Every attempt sends the same envelope, byte for byte, with a fresh signature.

Disabling after persistent failures

If at least 10 attempts in a row fail and nothing gets through for 24 hours, Flavoso switches the webhook off and sends you an e-mail. Open deliveries are then skipped.

After fixing it, you switch it back on in the console and send the missed deliveries again with “Resend”, or you catch up with GET /events, 30 days back.

Order and duplicates

  • Order: not guaranteed. Events of one object can arrive out of order, for example a retry after a later event. The newest state wins by the object's updated_at: data.object.updated_at is the time of the change the event reports, so keep the state with the latest updated_at and ignore an older one.
  • Duplicates: possible, for example when your response gets lost on the way. The event's id decides.
  • State in the envelope: data.object is built on the first send and frozen after that; for orders and reservations, status and updated_at are those of the change the event reports. If you need the current state for sure, read the object with GET.
  • Your app's fields: external_id and acknowledged_at are always null in events, because one envelope goes to every recipient alike. The responses of the routes carry them.

Deliveries and resending

Under Webhooks you find every delivery to your address, from all connected restaurants, filterable by status, type, restaurant and period. The detail view of a delivery shows the envelope, the headers sent including the signature, and every attempt: time, automatic or manual, HTTP status, duration, error and the beginning of your server's response (up to 2,000 characters).

“Resend” sends a delivery again, with the same envelope and the same id. It does not count against the 8 automatic attempts. Together, “Resend” and “Send a test event” can be used at most 10 times per minute and app, including the buttons on the test restaurant's Events tab.

Test events

“Send a test event” sends a type of your choice to your address, even one you have not subscribed to, plus ping. The data comes from your test restaurant where there is some, otherwise it is sample data; livemode is always false. After that it is a delivery like any other: signed, with retries and in the log. The connection to the test restaurant needs the matching read scope for it.

Real activity in the test restaurant triggers real events: a test order order.created, a status change in the console order.status_changed, a pause store.paused.

For development on your own computer you need a public address, for example through a tunnel. Or you use GET /events, which needs none.

A restaurant's own webhooks

An owner can also set up webhooks for their restaurant themselves, without an app, for example for their own system or an automation service: in the dashboard under Settings, Integrations, up to five addresses. They have the same shape; connection_id is null there, and there are no app.* events. More in the Flavoso docs.

Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.