API and webhooks reference
Last updated: 19 October 2026
1. Overview
An account's owners connect QR Wait to their own systems on the admin page's Integrations screen: webhooks push queue events out, API keys let a POS or CRM read the queues and act on them. Managers and attendants can't see or change either.
Connecting without code, through Zapier or Make: the webhook recipe.
2. Webhook endpoints
Up to 5 endpoints per account. An endpoint is an https:// address on a public host name: IP literals, localhost, names without a dot, .local / .internal / .lan / .home / .arpa names and QR Wait's own domain are refused.
3. Events
| Type | When |
|---|---|
ticket.joined | A ticket is taken: QR join, kiosk, added by an attendant or the API, or an appointment checked in |
ticket.called | A ticket is called (by hand, "Call next", or automatically when places free up) |
ticket.served | A called ticket is marked "Arrived" |
ticket.no_show | A called ticket is marked "No-show", or its time to arrive runs out |
ticket.cancelled | The visitor leaves the queue, or an attendant removes a waiting ticket |
appointment.booked | An appointment is booked (by the visitor or the attendant) |
ping | "Send test event" on the Integrations screen |
Undoing an action in the attendant panel doesn't send anything; a ticket moved in from another queue of the account sends no ticket.joined there.
4. Payload
POST with content-type: application/json:
queue.slugis left out for a hidden queue (its address is a secret).ticket.source:qr,manual(attendant or API),kioskorappointment.service,counter,tableandzoneappear when the queue uses them.appointment.bookedcarriesappointment: { at, size, service? }instead ofticket.- Personal data only when the owner ticked "Include names, join form answers and booking codes" for the endpoint:
visitor: { name?, answers? }(answers keyed by the question) andappointment.code. Off by default: payloads are numbers and times only.
{
"id": "d_3f9a1c0b7e2d4a61",
"type": "ticket.called",
"created": "2026-10-10T09:41:07.512Z",
"account": "antalyabb",
"queue": { "id": "a1b2c3d4e5", "name": "Konyaaltı Beach", "slug": "konyaalti" },
"ticket": {
"no": 42, "size": 2, "source": "qr",
"joined_at": "2026-10-10T09:12:30.004Z", "called_at": "2026-10-10T09:41:07.511Z",
"service": "Payments", "counter": "3"
}
}5. Headers and signature
| Header | Meaning |
|---|---|
X-QRWait-Event | The event type |
X-QRWait-Delivery | The delivery id (also id in the body); the same on every retry, so a receiver can drop duplicates |
X-QRWait-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> of the string <t>.<raw body>, keyed with the endpoint's secret |
import { createHmac, timingSafeEqual } from "node:crypto";
const [, t, v1] = req.headers["x-qrwait-signature"].match(/^t=(\d+),v1=([a-f0-9]{64})$/);
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const ok = timingSafeEqual(want, Buffer.from(v1, "hex")) && Math.abs(Date.now() / 1000 - Number(t)) < 300;The secret (whsec_…) is shown once when the endpoint is created. To verify: compute the HMAC over t + "." + body with the secret, compare it with v1 in constant time, and reject a t more than a few minutes old.
6. Delivery and retries
Events are usually delivered within a second. A queue checks whether any endpoint is on at most once a minute, so a new endpoint can miss up to a minute of events.
- Success is any
2xxanswer within 10 seconds; redirects are not followed. - Otherwise the delivery is retried after 1 min, 5 min, 30 min, 2 h and 6 h (6 tries in about 9 hours), then dropped.
- After 15 failed attempts in a row an endpoint is turned off, its waiting deliveries are dropped and the account's email gets a notice. Turning it back on clears the count. Test events don't count.
- At most 200 deliveries wait per account; beyond that the oldest are dropped.
- The Integrations screen shows the last 50 attempts: time, event, endpoint and the answer's status (0: no answer). Bodies aren't kept.
Endpoints and their secrets are stored with the account and are in the daily backup; the delivery queue and log are not.
7. REST API: keys and limits
Base: https://qrwait.app/api/v1. Every request carries an API key:
Authorization: Bearer qrw_<12 hex>_<48 hex>Keys are made on the Integrations screen (up to 10 per account), shown once, stored as a SHA-256 hash, and revoked there. A read key may only GET; a write key may also POST. 120 requests a minute per key, and 120 a minute from one IP address whatever the key. POST bodies are JSON (content-type: application/json). Write requests may carry an Idempotency-Key header (up to 64 characters): a retried request with the same key does nothing the second time. Actions show in the queue's action log as API · <key name>.
8. REST API: endpoints
| Method and path | Scope | Body | Answer |
|---|---|---|---|
GET /queues | read | { queues: [status, …] } for every queue of the account | |
GET /queues/<id> | read | status (below) | |
POST /queues/<id>/tickets | write | { size?: 1, note?: "…", service?: "…" } | { no }: a walk-in, as the attendant's "Add" |
POST /queues/<id>/call | write | { counter?: "3" } | { no } of the group called, null if nobody waits. In a counters queue counter is required unless it has one counter; elsewhere the first waiting group is called |
POST /queues/<id>/served | write | { no: 42 } | { ok: true }: "Arrived" for that called number |
Status: { id, name, waiting, people, next, called: [{ no, at, desk?, table?, zone?, svc? }], eta, paused, open, full, services: [{ name, waiting, eta }] }. eta is the estimated wait in minutes for someone joining now (null without enough recent calls). Queue ids (10 characters) come from GET /queues.
9. Errors
Errors are JSON { error } in the language of the x-lang header (English by default): 401 a missing, wrong or revoked key, 403 a read key on a write route, 404 an unknown route or a queue of another account, 429 over the rate limit, 400 anything else the queue refuses (no tickets left, an unknown counter, a number that isn't called).