Queue Join

Reference

API documentation

A REST API for reading your queues and bookings, adding people to a queue from your own website or till system, and receiving webhooks when anything changes.

Available on the Business plan. Create a key in Account settings → Integrations.

Authentication

Every request needs an API key in the Authorization header. Keys start qj_live_ and are shown once when created — we store only a hash, so a lost key must be revoked and replaced rather than recovered.

curl https://queuejoin.com/api/v1/me \
-H "Authorization: Bearer qj_live_xxxxxxxxxxxx"

Keys carry scopes:

  • read — Read queues, bookings, customers and locations
  • write — Add people to queues, make bookings, and move visits and bookings on

A missing, unknown or revoked key gets 401 with the code unauthorized. A valid key for an account Queue Join support has suspended gets 403 with account_suspended, and one for an account whose plan doesn’t include the API (it’s on Business and above) gets 403 with plan_required.

Treat a key like a password. It authenticates as your whole organisation, so never put one in browser JavaScript, a mobile app, or a public repository.

Conventions

  • Base URL: https://queuejoin.com/api/v1. All requests must use HTTPS.
  • Successful responses return { "data": … }. Lists also return meta with the limit and offset used.
  • Errors return { "error": { "code", "message" } }. Branch on code, which is stable; message may be reworded.
  • Lists accept limit (default 50, max 200) and offset.
  • Timestamps are ISO 8601 in UTC. Locations return their own timezone so you can render local times correctly.
  • Each key may make 120 requests a minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Past the limit you get 429 rate_limited with a Retry-After header.
{
"error": {
  "code": "insufficient_scope",
  "message": "This key does not have the \"write\" scope.",
  "required_scope": "write"
}
}

Endpoints

GET /v1/me

Confirms which organisation a key belongs to and what it can do. The quickest way to check a key works.

GET /v1/locations

Your locations, with addresses, coordinates and timezones.

GET /v1/queues

Your queues and how many people are waiting in each. Filter with location_id.

curl "https://queuejoin.com/api/v1/queues?location_id=LOCATION_ID" \
-H "Authorization: Bearer $QJ_KEY"

GET /v1/queue-entries

Visits, newest first. Filter with status (waiting, called, served, no_show, cancelled), location_id or queue_id.

POST /v1/queue-entries

Add somebody to a queue — from your own booking page, your till, or a kiosk. Requires the write scope. They are notified exactly as if they had scanned your QR code.

curl -X POST https://queuejoin.com/api/v1/queue-entries \
-H "Authorization: Bearer $QJ_KEY" \
-H "Content-Type: application/json" \
-d '{
  "name": "Sam Okafor",
  "mobile_number": "+447911123456",
  "location_id": "LOCATION_ID",
  "party_size": 2
}'

name, mobile_number and location_id are required. mobile_number is in international format, or a national number with its two-letter country code in mobile_number_country ("GB"). email, queue_id, service_id, party_size and notes are optional; without a queue we use the location’s default, and an email address is stored on the customer without replacing one already on file with a blank.

Like a member of staff adding a walk-in, the API is not subject to the limits on the public join form — but a blocked number is refused. Returns 201 with the new entry, or:

  • 400 invalid_body, missing_field or invalid_field — the body isn’t JSON, a required field is missing, or a number or email address can’t be read.
  • 402 plan_limit_reached — the plan’s visit limit is reached.
  • 402 location_locked — the location is beyond what the plan covers.
  • 403 number_blocked — you, or Queue Join, have blocked the number there.
  • 404 not_found — no such location in your organisation.
  • 409 no_queue — the location has no active queue.

GET /v1/queue-entries/{id}

A single visit.

PATCH /v1/queue-entries/{id}

Move a visit on: called, served, no_show or cancelled. Requires the write scope. called sends the customer the message that it is their turn, and cancelled and no_show send theirs, so your customer hears about it either way.

curl -X PATCH https://queuejoin.com/api/v1/queue-entries/ENTRY_ID \
-H "Authorization: Bearer $QJ_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "called"}'

Returns 409 already_closed if the visit is already served, cancelled or marked a no-show — closed visits are not reopened through the API. Staff can undo a serve or a call by mistake on the dashboard, which sends queue_entry.unserved or queue_entry.uncalled.

GET /v1/appointments

Bookings, latest start first. Filter with from and to (ISO dates, on the start time), location_id, or status (booked, confirmed, completed, cancelled, no_show).

POST /v1/appointments

Make a booking — from your own website, phone system or till. Requires the write scope. It is checked against exactly the rules a booking made in the dashboard is, and the customer is told about it the same way.

curl -X POST https://queuejoin.com/api/v1/appointments \
-H "Authorization: Bearer $QJ_KEY" \
-H "Content-Type: application/json" \
-d '{
  "name": "Sam Okafor",
  "mobile_number": "+447911123456",
  "location_id": "LOCATION_ID",
  "service_id": "SERVICE_ID",
  "start_at": "2026-08-29T14:00:00Z"
}'

name, location_id and service_id are required, with a start time given one of two ways: start_at, an ISO 8601 instant that includes Z or an offset; or date (YYYY-MM-DD) and time (HH:MM) in the location’s own time zone. The end time comes from the service’s duration. mobile_number (or a national number with mobile_number_country), email and notes are optional — without a mobile number the customer isn’t messaged.

Your booking settings all apply: whether bookings are on, booking hours, the notice and how far ahead people may book, capacity, the limit per customer, the service’s own rules and resources, and “only existing customers”. With booking approval on, a new booking is booked (a request) until it is confirmed; otherwise it starts confirmed. A booking counts towards your plan’s visit allowance, like a waitlist join. Returns 201 with the booking, or:

  • 400 invalid_body, missing_field or invalid_field — the body isn’t JSON, a required field is missing, or a time, number or email address can’t be read.
  • 402 location_locked — the location is beyond what the plan covers.
  • 402 plan_limit_reached — the plan’s visits for this month are used up.
  • 403 number_blocked — you, or Queue Join, have blocked the number there.
  • 403 existing_customers_only — the location only takes bookings from existing customers, and this isn’t one.
  • 404 not_found — no such location or service in your organisation.
  • 409 slot_unavailable — the time can’t be booked. The message says why: outside booking hours, too soon or too far ahead, full, or the customer’s limit reached.

GET /v1/appointments/{id}

A single booking.

PATCH /v1/appointments/{id}

Change a booking: move it to another time, or move it on. One change per request. Requires the write scope.

# Move it
curl -X PATCH https://queuejoin.com/api/v1/appointments/APPOINTMENT_ID \
-H "Authorization: Bearer $QJ_KEY" \
-H "Content-Type: application/json" \
-d '{"date": "2026-08-30", "time": "10:30"}'

# Move it on
curl -X PATCH https://queuejoin.com/api/v1/appointments/APPOINTMENT_ID \
-H "Authorization: Bearer $QJ_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "confirmed"}'

To move it, send start_at, or date and time, as when creating one. The new time is checked against the same rules, and the customer is told it has changed.

To move it on, send status:

  • confirmed — accept a booking request. Only for a booking that is still booked.
  • arrived and served — stamp arrived_at or served_at. The status doesn’t change.
  • completed, no_show and cancelled — close the booking.

Each sends the customer the same message the dashboard button does. Returns the booking as it now stands, or 404 not_found, 400 invalid_status, 409 already_closed (it is completed, cancelled or a no-show), 409 already_confirmed, or 409 slot_unavailable.

A booking has id, status, start_at, end_at, location_id, location_name, timezone, service_id, service_name, resource_id, notes, the customer’s customer_id, customer_name, customer_mobile and customer_email, and the created_at, confirmed_at, arrived_at, delayed_at, served_at, completed_at and cancelled_at times (null until they happen).

GET /v1/customers

Your customer directory. Filter with search, or with marketing_consent=true to get only those who have opted in and not since opted out.

Webhooks

Register an HTTPS endpoint in Account settings → Integrations and we will POST to it when something happens — whether it happened through this API or because a member of staff pressed a button.

  • appointment.cancelled — A booking was cancelled
  • appointment.created — A booking was made
  • appointment.updated — A booking was rescheduled, confirmed, arrived, served, completed or marked a no-show
  • queue_entry.called — A visitor was called forward
  • queue_entry.cancelled — A visitor left the queue
  • queue_entry.created — A visitor joined a queue
  • queue_entry.no_show — A visitor did not arrive
  • queue_entry.served — A visitor was served
  • queue_entry.uncalled — A visitor called by mistake was sent back to the queue
  • queue_entry.unserved — A visitor marked served by mistake was reopened
POST https://your-app.example.com/hooks/queuejoin
Queuejoin-Signature: t=1756377600,v1=6f2a…

{
"id": "EVENT_ID",
"type": "queue_entry.called",
"created_at": "2026-08-28T10:00:00.000Z",
"data": {
  "id": "ENTRY_ID",
  "customer_id": "CUSTOMER_ID",
  "location_id": "LOCATION_ID",
  "queue_id": "QUEUE_ID",
  "status": "called"
}
}

appointment.* events carry the booking instead:

{
"id": "EVENT_ID",
"type": "appointment.created",
"created_at": "2026-08-28T10:00:00.000Z",
"data": {
  "id": "APPOINTMENT_ID",
  "customer_id": "CUSTOMER_ID",
  "location_id": "LOCATION_ID",
  "service_id": "SERVICE_ID",
  "resource_id": "RESOURCE_ID",
  "start_at": "2026-08-29T14:00:00.000Z",
  "end_at": "2026-08-29T14:30:00.000Z",
  "status": "booked",
  "arrived_at": null,
  "cancelled_at": null,
  "completed_at": null,
  "confirmed_at": null,
  "delayed_at": null,
  "served_at": null
}
}

Events fire however the change happened: in the dashboard, through this API, by the customer on their own phone (joining, leaving, booking, confirming, moving or cancelling), or by one of your automation rules.

Verifying a webhook

Always verify the signature before acting on a webhook — otherwise anyone who learns your URL can tell your system a customer was served. The header contains a timestamp and an HMAC-SHA256 of <timestamp>.<raw body>, keyed with the signing secret shown when you created the endpoint.

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
  header.split(",").map((p) => p.split("=")),
);
const expected = crypto
  .createHmac("sha256", secret)
  .update(`${parts.t}.${rawBody}`)
  .digest("hex");

// Constant-time compare, and reject anything older than five minutes so a
// captured request cannot be replayed later.
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && crypto.timingSafeEqual(
  Buffer.from(parts.v1), Buffer.from(expected),
);
}

Use the raw request body, before any JSON parsing — re-serialising changes the bytes and the signature will not match.

Delivery and retries

  • Reply 2xx to acknowledge. Anything else is treated as a failure.
  • Reply quickly and do the work afterwards — we time out after 10 seconds.
  • Failures are retried six times over about 90 minutes with increasing gaps, then given up on. Recent attempts are listed in Account settings → Integrations.
  • Delivery is at-least-once: the same event may arrive twice. Use the event id to make your handling idempotent.
  • Disabling an endpoint stops queued events being delivered to it.
  • While an account is suspended its events are held, and delivered once it is reinstated.

Limits and support

Each key may make 120 requests a minute; see Conventions. Creating a visit or a booking counts towards your plan’s visit allowance, exactly as it would from the dashboard.

Questions, or something missing? hello@queuejoin.com. Use of the API is governed by our terms.