Webhooks
Quiqqy pushes events to your HTTPS endpoint — orders never require polling. Every product uses the same transport: the same headers, the same signature scheme, the same retry ladder. Per-event payloads are documented in Online Ordering events and Hub events.
The request you receive
Section titled “The request you receive”Deliveries are POST requests with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Quiqqy-Signature | HMAC-SHA256 of the raw request body, keyed with your webhook_secret, as lowercase hex |
X-Quiqqy-Event | The event type, e.g. order.created |
X-Quiqqy-Delivery-Id | Unique delivery id (UUID) — your idempotency key |
X-Quiqqy-Location-Id | Your external_store_id for the store, when set |
X-Quiqqy-Action | Only on action_required — equals data.action (data.request_type is a legacy alias) |
Every event shares one envelope shape — event_type, event_id, timestamp and a data object. The tenant fields vary by product: Online Ordering events carry store_id and location_id, Hub events carry community_id. An Online Ordering example:
{ "event_type": "order.created", "event_id": "9b3e6a1c-4f2d-4e8a-b1c9-7d5e2f8a0c4b", "store_id": "st_8fk2c1p9", "location_id": "LOC-001", "data": { "…": "event-specific payload" }, "timestamp": "2026-07-30T12:34:56.000Z"}event_id always equals X-Quiqqy-Delivery-Id for the original delivery and
stays the same across retries.
Verify the signature
Section titled “Verify the signature”Compute HMAC-SHA256(webhook_secret, raw_body) and compare it with
X-Quiqqy-Signature using a constant-time comparison. Use the exact raw
body bytes as received — re-serializing the parsed JSON changes whitespace
and key order and the signature will not match. Reject requests that fail
verification; the signature, not the payload shape, is what makes a delivery
trustworthy.
Your webhook_secret is returned once by
POST /pos/integrate-store
(or shown once in the console for other products). Store it server-side.
import crypto from 'node:crypto';import express from 'express';
const app = express();
app.post( '/webhooks/quiqqy', express.raw({ type: 'application/json' }), // keep the raw bytes (req, res) => { const signature = req.get('X-Quiqqy-Signature') ?? ''; const expected = crypto .createHmac('sha256', process.env.QUIQQY_WEBHOOK_SECRET) .update(req.body) // Buffer of the raw body — never JSON.stringify(parsed) .digest('hex');
const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(401).end();
res.status(200).end(); // acknowledge first …
const event = JSON.parse(req.body); // … then process enqueue(event); // heavy work belongs off the request path },);import hashlibimport hmacimport os
from flask import Flask, abort, request
app = Flask(__name__)SECRET = os.environ["QUIQQY_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/quiqqy")def quiqqy_webhook(): raw = request.get_data() # exact raw bytes, before any JSON parsing expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() signature = request.headers.get("X-Quiqqy-Signature", "")
if not hmac.compare_digest(signature, expected): abort(401)
event = request.get_json() enqueue(event) # heavy work belongs off the request path return "", 200Delivery, timeouts and retries
Section titled “Delivery, timeouts and retries”Quiqqy waits 10 seconds for a response. A 2xx acknowledges the delivery.
Anything else — a non-2xx status, a timeout, a connection error, or a 3xx
redirect (redirects are treated as failure; register a direct HTTPS URL) —
schedules a retry:
| Attempt | Delay after the previous failure |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
Six attempts in total. After the sixth failure the delivery is marked failed and is visible in the console’s delivery logs, where you can inspect the payload, your response status and the first part of your response body.
Idempotency
Section titled “Idempotency”Retries mean your endpoint will see the same event more than once. Dedupe
on X-Quiqqy-Delivery-Id (equal to event_id): record the id when you accept
a delivery and return 200 immediately for any id you have already seen.
Ordering is not guaranteed across events — use timestamp and the resource’s
own state, not arrival order.
Event catalogue
Section titled “Event catalogue”| Event | Fires when | Product |
|---|---|---|
order.created | A new order is placed for an integrated store | Online Ordering |
order.updated | An order’s status advanced | Online Ordering |
order.cancelled | An order was cancelled by either side | Online Ordering |
order.delivered | An order reached its terminal delivered state | Online Ordering |
menu.updated | The store’s menu changed on the Quiqqy side | Online Ordering |
menu.item_availability_changed | An item was 86’d or restored on the Quiqqy side | Online Ordering |
store.status_changed | The store went live or was paused | Online Ordering |
action_required | Quiqqy asks your integration to do something (e.g. push the menu) | Online Ordering |
order.created | A community order is placed | Hub Ordering |
order.status_changed | A community order’s status advanced | Hub Ordering |
Build your handler to switch on event_type and ignore unknown events and
unknown fields. New event types and payload fields are added without notice;
that is not a breaking change.
Handler checklist
Section titled “Handler checklist”- Verify
X-Quiqqy-Signatureagainst the raw body — reject on mismatch. - Dedupe on
X-Quiqqy-Delivery-Id— deliveries repeat. - Return
2xxwithin 10 seconds; do heavy work asynchronously. - Dispatch on
event_type; foraction_required, ondata.action. - Ignore unknown event types and unknown fields.
- Use a direct HTTPS URL — no redirects, valid TLS certificate.