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
- 1immediately
- 21 min
- 35 min
- 430 min
- 52 h
- 66 h
- 712 h
- 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
Enter the address
An
httpsaddress, reachable from the internet, at most 500 characters. Flavoso does not accept addresses in private networks.Console › App › Webhooks
- 2
Choose the events
Only what your app needs. A POS needs
order.*, oftenmenu.*andstore.*, alwaysapp.disconnected. - 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
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:
| Field | Meaning |
|---|---|
id | The event's id (evt_…). That is how you detect duplicates. |
object | Always event. |
type | The type, for example order.created. See Event types. |
created_at | When the change happened. |
api_version | The version date, as in Flavoso-Api-Version. |
livemode | false for the test restaurant. |
tenant_id | The restaurant. |
connection_id | Your app's connection to this restaurant. Use it to find the matching tokens. |
data | The 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:
| Header | Meaning |
|---|---|
Flavoso-Signature | The signature, see Verifying signatures. |
Flavoso-Event-Id | The same as id in the envelope. |
Flavoso-Event-Type | The same as type. |
Flavoso-Delivery-Attempt | The attempt number of this delivery, starting at 1. Every send counts, whether automatic or resent by hand. |
Content-Type | application/json |
User-Agent | Flavoso-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
idof 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.
| Attempt | Wait since the previous |
|---|---|
| 1 | immediately |
| 2 | 1 min |
| 3 | 5 min |
| 4 | 30 min |
| 5 | 2 h |
| 6 | 6 h |
| 7 | 12 h |
| 8 | 24 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_atis the time of the change the event reports, so keep the state with the latestupdated_atand ignore an older one. - Duplicates: possible, for example when your response gets lost on the way. The event's
iddecides. - State in the envelope:
data.objectis built on the first send and frozen after that; for orders and reservations,statusandupdated_atare those of the change the event reports. If you need the current state for sure, read the object withGET. - Your app's fields:
external_idandacknowledged_atare alwaysnullin 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.