Skip to content
Developers

Ring customers from your own POS.

One call when the order is placed, one when it is ready. The customer's phone does the rest. Plain REST over HTTPS, JSON in and out, signed webhooks back.

Base URL

https://kueues.com/api/v1
Your POSorder placed
POST /tickets
Kueuesticket #27
push or text
Customer phonerings!
When the kitchen is done, your POS calls …/ready. Webhooks tell you when the customer opened the ticket and when it was picked up.

5 minutes

Quickstart

  1. 1

    Create a key

    Open Connect in your dashboard and create an API key. Start with a test key.

  2. 2

    Create a ticket per order

    POST /tickets with reference set to your invoice number. Print the qr_url on the receipt or show it on screen.

  3. 3

    Mark it ready

    POST /tickets/by-reference/{ref}/ready. The phone rings. No ids to store on your side.

Create a ticket

curl https://kueues.com/api/v1/tickets \
  -H "Authorization: Bearer $KUEUES_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-INV-1043" \
  -d '{"reference": "INV-1043", "customer_name": "Sam"}'
Response
HTTP/1.1 201 Created
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1792000980

{
  "ticket": {
    "object": "ticket",
    "id": "q7Rk2LmP9sXc4VbN",
    "number": 27,
    "status": "preparing",
    "reference": "INV-1043",
    "customer_name": "Sam",
    "note": null,
    "source": "api",
    "test": false,
    "claimed": false,
    "alerts_on": false,
    "customer_coming": false,
    "claim_url": "https://kueues.com/t/7KQ2MXR9PA",
    "qr_url": "https://kueues.com/api/v1/qr/7KQ2MXR9PA",
    "business_day": "2026-10-14",
    "queue": { "id": "Mq3vT8kLp2Wd", "name": "Pickup counter", "room": null },
    "pos": null,
    "created_at": "2026-10-14T17:02:11Z",
    "claimed_at": null,
    "ready_at": null,
    "collected_at": null,
    "cancelled_at": null,
    "expired_at": null
  },
  "created": true
}

Mark it ready by reference

curl -X POST https://kueues.com/api/v1/tickets/by-reference/INV-1043/ready \
  -H "Authorization: Bearer $KUEUES_KEY"
Response
HTTP/1.1 200 OK

{
  "ticket": {
    "object": "ticket",
    "id": "q7Rk2LmP9sXc4VbN",
    "number": 27,
    "status": "ready",
    "reference": "INV-1043",
    "ready_at": "2026-10-14T17:09:03Z",
    ...
  },
  "changed": true
}

Authentication

Every request except GET /qr/{token} carries a location API key as a bearer token: Authorization: Bearer kq_live_…. Each key belongs to one location and can be limited to specific queues.

Live

kq_live_…

Real tickets that ring real phones. Needs an active subscription, otherwise requests return 402 payment_required.

Test

kq_test_…

Creates test tickets (test: true) and works without a subscription. Perfect for wiring things up during the trial.

Keys are secrets. Call the API from your server or POS backend only, never from a browser or a customer-facing app. Revoke a leaked key from the Connect page and it stops working right away.

References & idempotency

A reference is your own order or invoice number. Send it on create and you can act on the ticket later without storing our id.

  • Up to 64 characters: letters, digits, space and _ - . / # :. Matching ignores case for Latin letters, so inv-1043 finds INV-1043.
  • Creating a ticket with a reference that is still open in that queue returns the existing ticket with 200 and created: false. A new ticket returns 201 and created: true.
  • Lookups by reference prefer an open ticket, then one from today's business day (days start at 05:00 location time), then the most recent.
  • Percent-encode references in paths. # becomes %23 and / becomes %2F, so A/12#3 is A%2F12%233.

Idempotency-Key

For creates without a reference (or for extra safety on flaky networks), send an Idempotency-Key header: up to 255 visible ASCII characters, remembered for 24 hours. A retry with the same key and body returns the original result. The same key with a different body returns 409 idempotency_conflict.

State changes are idempotent by design. Calling ready, collect or cancel on a ticket already in that state returns 200 with changed: false. collect is also allowed straight from preparing, for orders handed over without a ring.

Queues

A location can run several queues: departments, pickup windows, the bar and the kitchen. GET /shop lists the queues your key can use. When there is more than one, send queue with the queue id on create.

curl https://kueues.com/api/v1/shop \
  -H "Authorization: Bearer $KUEUES_KEY"
Response
HTTP/1.1 200 OK

{
  "shop": {
    "code": "H7K2QM",
    "name": "Lucky Noodle Bar",
    "open": true,
    "timezone": "America/Chicago",
    "business_day": "2026-10-14",
    "subscription_active": true
  },
  "queues": [
    { "id": "Mq3vT8kLp2Wd", "name": "Pickup counter", "room": null },
    { "id": "Zp8cR2nVx4Ht", "name": "Bar", "room": "Patio" }
  ],
  "key": { "id": "k9Wm2Qx7Lr4T", "test": false, "all_queues": true }
}
SituationResult
400 queue_requiredSeveral queues are available to the key and the request did not say which.
400 invalid_queueThe queue id does not exist at this location, or it was archived.
403 queue_forbiddenThe queue exists but the key is limited to other queues.
404 ticket_not_foundThe ticket lives in a queue the key cannot see. We do not confirm it exists.
409 ambiguous_referenceThe reference is open in more than one queue. Repeat the call with ?queue=.
  • Numbers restart at 1 in each queue every business day, so the number alone is not unique. Join on id or reference.
  • There is one webhook endpoint per location. Route events by data.ticket.queue.

Endpoints

All paths are relative to https://kueues.com/api/v1. Request bodies are JSON with Content-Type: application/json, up to 16 KB. The OpenAPI 3.1 spec is the source of truth.

MethodPathWhat it does
POST/ticketsCreate a ticket. Body: reference, customer_name (≤ 40), note (≤ 140, staff only), lang (en, es, fr), queue.
GET/ticketsToday's tickets plus any still open, newest first. Filters: status (comma list), reference, queue, limit (≤ 300, default 100). No pagination.
GET/tickets/{id}One ticket by id.
POST/tickets/{id}/{action}ready rings the customer, collect marks it picked up, cancel cancels it.
GET/tickets/by-reference/{reference}One ticket by your reference. Accepts ?queue=.
POST/tickets/by-reference/{reference}/{action}Same actions, addressed by your reference. Accepts ?queue=.
GET/shopThe location and the queues this key can use. Handy as a connection check.
GET/qr/{token}QR image for a claim link. PNG by default, ?format=svg for SVG, ?size= 64 to 1024. No key needed.

List open tickets

curl "https://kueues.com/api/v1/tickets?status=preparing,ready&queue=Mq3vT8kLp2Wd&limit=50" \
  -H "Authorization: Bearer $KUEUES_KEY"
Response
HTTP/1.1 200 OK

{
  "object": "list",
  "business_day": "2026-10-14",
  "tickets": [
    { "object": "ticket", "id": "q7Rk2LmP9sXc4VbN", "number": 27, "status": "ready", ... },
    { "object": "ticket", "id": "Hn4pW8sKq2ZtR6vY", "number": 26, "status": "preparing", ... }
  ]
}

The ticket object

FieldTypeNotes
objectstringAlways "ticket".
idstringOur id. Stable and unique.
numberintegerThe number the customer sees. Restarts at 1 per queue each business day.
statusenumpreparing → ready → collected, or cancelled. Tickets left open for 12 hours become expired.
referencestring | nullYour order or invoice number.
customer_namestring | nullOptional name shown to staff and on the ticket.
notestring | nullStaff-only note.
sourceenumapi, counter, scan, board, square or toast.
testbooleanCreated with a test key or the dashboard test button.
claimedbooleanA phone has opened the ticket.
alerts_onbooleanThe customer turned on push or text alerts.
customer_comingbooleanThe customer tapped "On my way" after the ring.
claim_urlstringThe link the customer opens. Encode it in your own QR if you like.
qr_urlstringA ready-made QR image of claim_url.
business_daystringYYYY-MM-DD in the location time zone. Days start at 05:00.
queueobject | null{ id, name, room }
posobject | null{ provider, order_id, location_id } for Square and Toast tickets.
created_at … expired_atstring | nullcreated_at, claimed_at, ready_at, collected_at, cancelled_at, expired_at. ISO 8601 in UTC.
ticket.json
{
  "object": "ticket",
  "id": "q7Rk2LmP9sXc4VbN",
  "number": 27,
  "status": "preparing",
  "reference": "INV-1043",
  "customer_name": "Sam",
  "note": null,
  "source": "api",
  "test": false,
  "claimed": true,
  "alerts_on": true,
  "customer_coming": false,
  "claim_url": "https://kueues.com/t/7KQ2MXR9PA",
  "qr_url": "https://kueues.com/api/v1/qr/7KQ2MXR9PA",
  "business_day": "2026-10-14",
  "queue": { "id": "Mq3vT8kLp2Wd", "name": "Pickup counter", "room": null },
  "pos": null,
  "created_at": "2026-10-14T17:02:11Z",
  "claimed_at": "2026-10-14T17:02:40Z",
  "ready_at": null,
  "collected_at": null,
  "cancelled_at": null,
  "expired_at": null
}

Errors

Errors share one shape: { "error": { "code", "message", ... } }. Branch on code; the message is for humans and may change.

StatusCodesMeaning
400invalid_input, invalid_json, bad_request, queue_required, invalid_queueFix the request. invalid_input includes details: [{ field, issue }]. Unknown fields are rejected.
401missing_api_key, invalid_api_keyNo key, or a wrong or revoked one.
402payment_requiredThe subscription is not active. Test keys keep working.
403queue_forbiddenThe key is limited to other queues.
404ticket_not_found, not_foundNo such ticket (or not visible to this key), or no such route.
409invalid_transition, idempotency_conflict, ambiguous_referenceThe action does not fit the ticket's state. invalid_transition includes ticket_status.
413payload_too_largeBody over 16 KB.
415unsupported_media_typeSend Content-Type: application/json.
429rate_limitedSlow down. See Retry-After.
500server_errorOur fault. Safe to retry with backoff.
Examples
HTTP/1.1 409 Conflict

{
  "error": {
    "code": "invalid_transition",
    "message": "This ticket is cancelled and cannot become ready.",
    "ticket_status": "cancelled"
  }
}

HTTP/1.1 400 Bad Request

{
  "error": {
    "code": "invalid_input",
    "message": "Unrecognized key: \"customer\"",
    "details": [{ "field": "(body)", "issue": "Unrecognized key: \"customer\"" }]
  }
}

Rate limits

120requests per minute, per key

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). Over the limit you get 429 with Retry-After.

30bad-key attempts per 10 minutes

Counted per IPv4 address or IPv6 /64. Past that, the network gets 429 until the window clears, even with a valid key.

Webhooks

Set one HTTPS endpoint per location on the Connect page, pick events, and we POST JSON to it as tickets move.

EventWhen
ticket.createdA ticket was created from any source.
ticket.claimedA customer phone opened the ticket.
ticket.readyThe ticket went ready and the phone is ringing.
ticket.collectedPicked up.
ticket.cancelledCancelled by staff or the API.
ticket.expiredLeft open for 12 hours.
webhook.testSent from the dashboard test button.
Delivery
POST /webhooks/kueues HTTP/1.1
Content-Type: application/json
User-Agent: Kueues-Webhooks/1.0
Kueues-Event: ticket.ready
Kueues-Delivery: Xc2mR8kQp4LwT7vN
Kueues-Attempt: 1
Kueues-Signature: t=1792001343,v1=6f1c0e9a4b...e27d

{
  "id": "evt_3kX9pQ2mW7zR1vB8nC4d",
  "type": "ticket.ready",
  "created_at": "2026-10-14T17:09:03Z",
  "shop": { "code": "H7K2QM", "name": "Lucky Noodle Bar" },
  "data": {
    "ticket": { "object": "ticket", "id": "q7Rk2LmP9sXc4VbN", "number": 27, "status": "ready", "test": false, ... }
  }
}

Delivery rules

  • HTTPS on port 443 or 8443. Private and internal addresses are refused, and checked again at send time.
  • Answer with any 2xx within 10 seconds. Redirects are not followed.
  • Retries after 10s, 1m, 5m, 30m, 2h and 6h: 7 attempts in total.
  • At-least-once. Deduplicate on the payload id.
  • Ordering is not guaranteed. Compare timestamps on the ticket.

Good to know

  • Headers: Kueues-Event, Kueues-Delivery (same across retries), Kueues-Attempt, Kueues-Signature.
  • Undo and ring-again in the dashboard send no events.
  • Test tickets arrive with data.ticket.test = true.
  • Rotating the secret takes effect immediately, retries included.

Verify signatures

Kueues-Signature looks like t=1792001343,v1=6f1c…. v1 is the hex HMAC-SHA256 of ${t}.${rawBody}, keyed with your full whsec_… secret string. Compare in constant time and reject timestamps older than 5 minutes.

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

const app = express()
const SECRET = process.env.KUEUES_WEBHOOK_SECRET // the full whsec_... string
const TOLERANCE_SEC = 300

// Raw body: the signature covers the exact bytes we sent, so do not parse JSON first.
app.post('/webhooks/kueues', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Kueues-Signature') ?? ''
  const parts = Object.fromEntries(
    header.split(',').map((kv) => {
      const i = kv.indexOf('=')
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()]
    }),
  )
  const t = parts.t ?? ''
  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SEC) {
    return res.status(400).send('stale or missing timestamp')
  }

  const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(req.body).digest()
  const given = Buffer.from(parts.v1 ?? '', 'hex')
  if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
    return res.status(400).send('bad signature')
  }

  const event = JSON.parse(req.body.toString('utf8'))
  // Deliveries are at-least-once: skip event.id values you have already handled.
  // Then do the work (or queue it) and answer 2xx within 10 seconds.
  res.sendStatus(204)
})

app.listen(3000)
Verify against the raw request bytes. Parsing the JSON and serializing it again changes whitespace and key order, and the signature will not match.

POS guides

SquareOfficial integration

Connect from the dashboard Connect page with Square sign-in (OAuth). Pick the Square location to listen to.

Every Square order at that location becomes a ticket, using the Square ticket name or order number as the reference.

When the order's pickup fulfillment moves to Prepared, the ticket rings.

Pressing Ready in Kueues updates the Square fulfillment only for orders created through Square's API (online ordering, for example). Square does not let third parties edit orders rung up on Square POS, so for counter sales staff press Ready in Kueues, or mark the order prepared in Square.

ToastOfficial integration

Add Kueues from Toast's integrations, then paste your restaurant GUID on the Connect page.

Orders flow in automatically as tickets.

A ticket goes ready when every item on the order is bumped on Toast KDS.

Toast does not let third parties write ready status back. Without Toast KDS, staff press Ready in Kueues.

Other POSREST API

Create a ticket when the order is paid, with reference set to your invoice number. Print qr_url on the receipt.

When the kitchen is done, call /tickets/by-reference/{ref}/ready.

Subscribe to webhooks if you want claims and pickups back in your system.

Back to quickstart

Square is a trademark of Block, Inc. Toast is a trademark of Toast, Inc. Kueues is independent and not affiliated with or endorsed by either. Integration behavior depends on what each platform's API allows and can change when they change it.