ZiroWait
Sign In
Jump to a section
API v1 · 1.1.0

ZiroWait API

A server-to-server REST API for your queue. Read queues, entries, services and appointments, and create or cancel queue tokens, from your own website, kiosk, billing or display software.

Base URL
https://api.zirowait.com/v1
Format
JSON over HTTPS
Authentication
Bearer API key
Rate limit
120 requests a minute per key
Server-side only. Your key is a secret. The API sends no CORS headers, so a browser cannot call it, on purpose. Call it from your own server and keep the key in a secret manager.

All endpoints

MethodPathWhat it doesScope
GET/v1/meCheck your keyany
GET/v1/servicesList servicesqueue:read
GET/v1/queuesList queuesqueue:read
GET/v1/queues/{queueId}Get a queue with today's countsqueue:read
GET/v1/queues/{queueId}/entriesList entries in a queuequeue:read
POST/v1/queues/{queueId}/entriesCreate a queue tokenqueue:write
GET/v1/entries/{entryId}Get an entryqueue:read
POST/v1/entries/{entryId}/cancelCancel a waiting entryqueue:write
GET/v1/appointmentsList appointmentsappointments:read
GET/v1/appointments/{appointmentId}Get an appointmentappointments:read

Everything is scoped to the one business that owns the key. An id from any other business looks exactly like an id that does not exist.

Quickstart

From nothing to your first queue token in about five minutes.

  1. Get access

    The Developer API is switched on per business. Request access; we switch it on and tell you.

  2. Create a key

    The business owner opens Dashboard → Settings → Developer API and creates a key. Give it a name, only the scopes it needs, an expiry, and, if you can, your server's IP address. The key is shown once and cannot be shown again, so copy it straight into your secret manager.

  3. Keep it out of your code

    Store it as an environment variable for the examples below:

    shell
    export ZIROWAIT_KEY="zwk_live_..."
  4. Check it works

    curl "https://api.zirowait.com/v1/me" \
      -H "Authorization: Bearer $ZIROWAIT_KEY"

    You should see your business, the key's scopes and its limits:

    JSON · 200
    {
      "data": {
        "business": {
          "name": "Sharma Service Center",
          "slug": "sharma-service",
          "category": "service",
          "timezone": "Asia/Kolkata"
        },
        "key": {
          "id": "ab12cd34ef56",
          "name": "Reception kiosk",
          "scopes": [
            "queue:read",
            "queue:write"
          ],
          "expiresAt": "2026-12-19T00:00:00.000Z"
        },
        "limits": {
          "requestsPerMinute": 120,
          "writesPerMinute": 30,
          "maxBodyBytes": 32768
        }
      }
    }

    A 401 means the key is wrong, expired or revoked. A 403 api_access_disabled means step 1 is not done yet.

  5. Find your queue

    curl "https://api.zirowait.com/v1/queues" \
      -H "Authorization: Bearer $ZIROWAIT_KEY"

    Copy the id of the queue you want and keep it as QUEUE_ID.

  6. Create your first token

    curl -X POST "https://api.zirowait.com/v1/queues/$QUEUE_ID/entries" \
      -H "Authorization: Bearer $ZIROWAIT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{"name":"Priya","phone":"+919876500001"}'

    The response has the customer's token (for example A12) and their position. The token appears on the business's dashboard straight away. Read it back at any time with Get an entry.

Authentication

Send your key in the Authorization header on every request:

HTTP
Authorization: Bearer zwk_live_<id>_<secret>
  • Never put a key in a URL. A key in a query string is refused with 400 even if it is valid, because URLs end up in logs and browser history.
  • Never use a key in a browser or mobile app. Anyone could read it. Keep it on your server.
  • Every kind of key failure returns the same 401, so nothing can be learned about which keys exist.

Scopes

A key can do only what its scopes allow. Give each key the fewest it needs.

ScopeAllows
queue:readRead queues, entries and services.
queue:writeCreate queue entries and cancel waiting ones.
appointments:readRead appointments.

Expiry, IP allowlist and rotation

  • Keys expire after 1 to 365 days (90 by default). A business can have up to 10 active keys.
  • An optional IP allowlist takes IPv4 addresses, IPv4 ranges such as 203.0.113.0/24, and exact IPv6 addresses. From any other address the key is refused with 403 ip_not_allowed.
  • To rotate a key: create the new key, deploy it, then revoke the old one. The dashboard shows when each key was last used and from where. Revoking takes effect on the next request.
  • Only the business owner can create or revoke keys, with their normal login. A key can never create, list or revoke keys.

Requests and responses

TopicHow it works
FormatJSON in and out. Send Content-Type: application/json on requests with a body. A body can be at most 32 KB.
SuccessThe result is wrapped as { "data": ... }. Lists add nextCursor (see Pagination).
TimestampsISO 8601 in UTC, for example 2026-09-21T05:41:02.000Z.
DatesA date parameter is a calendar day, YYYY-MM-DD, in the business's timezone (shown by GET /me). Omit it for today.
Phone numbersYou may send +91 98765 00001 or 9876500001. They are stored and returned as digits with the country code: 919876500001.
IDsOpaque strings. Store them as strings and do not parse them.
Strict inputUnknown fields in a body and unknown query parameters are rejected with 400, never silently ignored. That catches typos early.
Unknown response fieldsIgnore fields you do not recognise. We add new fields without a new version.
CachingResponses are Cache-Control: no-store.

Headers

HeaderDirectionMeaning
AuthorizationrequestBearer <key>. Required.
Idempotency-KeyrequestOn POST. Makes a retry safe. See Idempotency.
X-Request-IdresponseA unique id for the request. Quote it to support.
API-VersionresponseThe major version that answered, currently v1.
RateLimit-Limit / -Remaining / -ResetresponseYour allowance in the current window, what is left, and seconds until it resets.
Retry-AfterresponseOn 429 and on a busy idempotent request: seconds to wait.
LocationresponseOn 201: the URL of the new entry.
Idempotent-Replayresponsetrue when the response is the stored result of an earlier identical request.

Pagination

Lists use cursors. Each page returns up to limit items (1 to 100, default 50) and a nextCursor. Pass it back as cursor to get the next page. When nextCursor is null you have everything. Do not build a cursor yourself.

Node.js
async function allEntries(queueId, status) {
  const entries = [];
  let cursor = null;
  do {
    const url = new URL(`https://api.zirowait.com/v1/queues/${queueId}/entries`);
    url.searchParams.set("limit", "100");
    if (status) url.searchParams.set("status", status);
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ZIROWAIT_KEY}` } });
    const page = await res.json();
    if (!res.ok) throw new Error(page.error.code);
    entries.push(...page.data);
    cursor = page.nextCursor;
  } while (cursor);
  return entries;
}
Python
def all_entries(queue_id, status=None):
    entries, cursor = [], None
    while True:
        params = {"limit": 100}
        if status: params["status"] = status
        if cursor: params["cursor"] = cursor
        res = requests.get(f"https://api.zirowait.com/v1/queues/{queue_id}/entries", params=params,
                           headers={"Authorization": f"Bearer {os.environ['ZIROWAIT_KEY']}"}, timeout=10)
        page = res.json()
        if not res.ok: raise RuntimeError(page["error"]["code"])
        entries += page["data"]
        cursor = page["nextCursor"]
        if not cursor: return entries

Idempotency

Networks fail. If a request to create a token times out, you do not know whether it worked, and sending it again could create a second token. An Idempotency-Key removes the doubt.

  • Send a unique key (8 to 64 characters: letters, digits, _ or -) with each POST. A UUID is ideal.
  • Retry with the same key and the same body: you get the original result back, with Idempotent-Replay: true. No duplicate is created.
  • The same key with a different body is a bug in your code: 422 idempotency_key_reuse.
  • If the first request is still running, a retry gets 409 request_in_progress. Wait a second and try again.
  • Only successful results are stored, for 24 hours. A failed attempt releases the key so you can retry it.
Node.js
async function createToken(queueId, name, phone) {
  const key = crypto.randomUUID();           // one key per logical action
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const res = await fetch(`https://api.zirowait.com/v1/queues/${queueId}/entries`, {
        method: "POST",
        headers: { Authorization: `Bearer ${process.env.ZIROWAIT_KEY}`, "Idempotency-Key": key, "Content-Type": "application/json" },
        body: JSON.stringify({ name, phone }),
      });
      if (res.status < 500 && res.status !== 429) return await res.json();   // done, or a mistake retrying cannot fix
    } catch { /* network error: retry with the same key */ }
    await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
  }
  throw new Error("Gave up after 3 attempts");
}

Rate limits

LimitValueApplies to
Requests120 per minuteEach key, all endpoints
Writes30 per minuteEach key, POST requests (they also count toward the 120)
Failed sign-ins30 per 10 minutesEach IP address. Only 401 responses count; a typo elsewhere never locks you out.

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Past the limit you get 429 rate_limited with a Retry-After header. GET /v1/me tells you your key's exact limits.

To stay well inside the limits, poll no faster than every 5 to 10 seconds. A lobby display that reads one queue summary and one entries page every 10 seconds makes 12 requests a minute.
Node.js
async function call(url, init, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url, init);
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("retry-after") || 2 ** i);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("still rate limited");
}

Errors

Every error has the same shape and never contains internal detail. Branch on code, not on message, and quote requestId when you contact support.

JSON · 404
{
  "error": {
    "code": "not_found",
    "message": "Entry not found.",
    "requestId": "0c8e3f5a-6b1d-4c1e-9a55-2f5d8e1b7c90",
    "docUrl": "https://www.zirowait.com/developers/docs#errors"
  }
}

Validation errors

A 400 invalid_request includes details: which fields were wrong and why. It never repeats the values you sent.

JSON · 400
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid body: name (1-120 chars), phone (8-20 digits) and an optional serviceId are accepted; no other fields.",
    "requestId": "0c8e3f5a-6b1d-4c1e-9a55-2f5d8e1b7c91",
    "docUrl": "https://www.zirowait.com/developers/docs#errors",
    "details": [
      {
        "field": "phone",
        "message": "Invalid format."
      },
      {
        "field": "nickname",
        "message": "Unknown field. Remove it."
      }
    ]
  }
}

Error codes

StatusCodeMeaningWhat to do
400invalid_requestA field, query parameter or header is invalid or unknown. error.details says which and why.Fix the fields listed in details. Unknown fields must be removed.
400invalid_jsonThe body is not valid JSON.Send valid JSON with Content-Type: application/json.
401unauthorizedThe key is missing or invalid. Every cause (malformed, unknown, wrong secret, revoked, expired) looks the same on purpose.Check the Authorization header is exactly Bearer <key>. If it worked before, the key may have expired or been revoked: create a new one.
403insufficient_scopeThe key lacks a scope this endpoint needs. error.required lists them.Create a key with the needed scope.
403api_access_disabledDeveloper API access is switched off for this business.Ask ZiroWait to switch it on.
403ip_not_allowedThe key has an IP allowlist and your server's address is not on it.Call from an allowed address, or add your server's IP to the key.
403plan_limit_reachedThe business's plan limit was reached, for example the daily token limit.The business owner can upgrade the plan.
404not_foundNo such resource in your business. An id from another business looks the same.Check the id, and that it came from this business's key.
409queue_pausedThe business has paused new tokens for this queue.Try again later. Show your customer a friendly message.
409invalid_stateThe action is not possible in the entry's current state, for example canceling one that is already in service.Read the entry to see its status first.
409request_in_progressA request with the same Idempotency-Key is still running.Wait a second and retry with the same key.
413payload_too_largeThe body is over 32 KB.Send less.
415unsupported_media_typeThe body was not sent as application/json.Set Content-Type: application/json.
422idempotency_key_reuseThat Idempotency-Key was already used with a different request.Use a fresh key for each new request. Reuse a key only to retry the same one.
429rate_limitedToo many requests. See the Retry-After header.Wait Retry-After seconds, then retry with backoff.
429too_many_failed_attemptsToo many failed sign-ins from your IP address.Fix the key and wait. It clears after a while.
500internal_errorSomething went wrong on our side.Retry with the same Idempotency-Key. If it keeps happening, contact support with the requestId.

Troubleshooting

SymptomLikely cause and fix
401 and the key looks rightThe header must be exactly Authorization: Bearer <key>, with no quotes or extra spaces. Or the key expired or was revoked: create a new one.
403 api_access_disabledThe Developer API is not switched on for this business yet.
403 ip_not_allowedYour server's public IP is not on the key's allowlist. Check what address your server calls from (a cloud NAT address is often different from the machine's own).
A CORS error in the browserExpected. The API is not callable from browsers. Move the call to your server.
400 with detailsRead details: each item names the field. Unknown fields must be removed.
409 queue_pausedThe business paused new tokens. Try again later.
429Slow down and honour Retry-After. Do not retry in a tight loop.

Versioning and changes

The URL carries the major version: /v1. Within a version we only make changes that do not break a correct client:

  • New endpoints, new optional parameters, new response fields, and new error codes.
  • So your client must ignore response fields it does not know and treat an unknown error code as a generic failure for that status.
  • A change that could break a correct client would ship as a new major version, announced in the changelog before it takes effect.
  • The API-Version response header tells you which major version answered.

Machine-readable description: https://api.zirowait.com/v1/openapi.json (OpenAPI 3.1, no key needed). Import it into Postman (Import → Link) or Insomnia, open it in Swagger UI, or generate a client library from it.

API reference · Account

Everything below needs the key in the Authorization header. The scope each endpoint needs is shown on its card.

Check your key

GET/v1/meany valid key

Confirms your key works and shows the business it belongs to, the key's scopes and expiry, and the rate limits that apply. Works with any valid key, so it is the first call to make.

Parameters

This endpoint takes no parameters.

Example request

curl "https://api.zirowait.com/v1/me" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": {
    "business": {
      "name": "Sharma Service Center",
      "slug": "sharma-service",
      "category": "service",
      "timezone": "Asia/Kolkata"
    },
    "key": {
      "id": "ab12cd34ef56",
      "name": "Reception kiosk",
      "scopes": [
        "queue:read",
        "queue:write"
      ],
      "expiresAt": "2026-12-19T00:00:00.000Z"
    },
    "limits": {
      "requestsPerMinute": 120,
      "writesPerMinute": 30,
      "maxBodyBytes": 32768
    }
  }
}

Errors this endpoint can return

Queues

List queues

GET/v1/queuesscope: queue:read

The business's queues. Most businesses have one. You need a queue id for everything else in this group.

Parameters

This endpoint takes no parameters.

Example request

curl "https://api.zirowait.com/v1/queues" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": [
    {
      "id": "clx8f2a1c0001abc123def456",
      "name": "Main Queue",
      "isActive": true,
      "isTokenCreationPaused": false
    }
  ]
}

Errors this endpoint can return

Get a queue with today's counts

GET/v1/queues/{queueId}scope: queue:read

The queue plus how many entries are waiting, in service, completed, canceled or no-show for the day, and the average estimated wait of the people waiting now. It is one cheap call, ideal for a lobby display.

Parameters

NameInTypeDescription
queueIdrequiredpathstringThe queue. An opaque id. Treat it as a string; do not parse it.
datequeryYYYY-MM-DDThe day to count, in the business's timezone. Defaults to today.

Example request

curl "https://api.zirowait.com/v1/queues/$QUEUE_ID" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": {
    "id": "clx8f2a1c0001abc123def456",
    "name": "Main Queue",
    "isActive": true,
    "isTokenCreationPaused": false,
    "date": "2026-09-21",
    "counts": {
      "waiting": 6,
      "inService": 1,
      "completed": 24,
      "canceled": 2,
      "noShow": 1
    },
    "averageWaitMinutes": 18
  }
}

Errors this endpoint can return

List entries in a queue

GET/v1/queues/{queueId}/entriesscope: queue:read

The day's entries in token order (today by default). Filter by status to get, for example, only the people still waiting. Results are paginated with a cursor.

Parameters

NameInTypeDescription
queueIdrequiredpathstringThe queue. An opaque id. Treat it as a string; do not parse it.
statusqueryenumWAITING, IN_SERVICE, COMPLETED, CANCELED or NO_SHOW. Omit for all.
datequeryYYYY-MM-DDThe day to list, in the business's timezone. Defaults to today.
limitqueryintegerPage size, 1 to 100. Default 50.
cursorquerystringThe nextCursor from the previous page.

Example request

curl "https://api.zirowait.com/v1/queues/$QUEUE_ID/entries?status=WAITING&limit=20" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": [
    {
      "id": "clx8f2a1c0002abc123def456",
      "token": "A12",
      "tokenSeries": "A",
      "tokenNumber": 12,
      "status": "WAITING",
      "priority": false,
      "serviceId": null,
      "customer": {
        "name": "Priya",
        "phone": "919876500001"
      },
      "issuedAt": "2026-09-21T05:41:02.000Z",
      "startedAt": null,
      "completedAt": null,
      "canceledAt": null,
      "predictedWaitMinutes": 18
    }
  ],
  "nextCursor": null
}

Errors this endpoint can return

Entries

Create a queue token

POST/v1/queues/{queueId}/entriesscope: queue:write

Adds a customer to the queue and returns their token and position. The token appears on the business's dashboard straight away.

  • This never sends a WhatsApp message to the customer. Show them their token yourself.
  • Send an Idempotency-Key. If your request times out you can retry it safely: you get the original token back instead of a second one.
  • Nothing but name, phone and serviceId is accepted. An unknown field is rejected, not ignored.

Parameters

NameInTypeDescription
queueIdrequiredpathstringThe queue. An opaque id. Treat it as a string; do not parse it.
Idempotency-Keyheaderstring8 to 64 characters: letters, digits, underscore or dash. Recommended on every POST.
namerequiredbodystringThe customer's name, 1 to 120 characters.
phonerequiredbodystring8 to 20 digits. Spaces, dashes, brackets and a leading + are allowed. Stored with the country code.
serviceIdbodystringOptional. Must be an active service of this business.

Example request

curl -X POST "https://api.zirowait.com/v1/queues/$QUEUE_ID/entries" \
  -H "Authorization: Bearer $ZIROWAIT_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"Priya","phone":"+919876500001"}'

Response · 201

JSON · 201
{
  "data": {
    "id": "clx8f2a1c0002abc123def456",
    "token": "A12",
    "tokenSeries": "A",
    "tokenNumber": 12,
    "status": "WAITING",
    "priority": false,
    "serviceId": null,
    "customer": {
      "name": "Priya",
      "phone": "919876500001"
    },
    "issuedAt": "2026-09-21T05:41:02.000Z",
    "startedAt": null,
    "completedAt": null,
    "canceledAt": null,
    "predictedWaitMinutes": 18,
    "position": 6
  }
}

Errors this endpoint can return

Get an entry

GET/v1/entries/{entryId}scope: queue:read

One entry, plus its live position in the queue while it is waiting (1 means next). Poll this to keep a customer's screen up to date.

Parameters

NameInTypeDescription
entryIdrequiredpathstringThe entry. An opaque id. Treat it as a string; do not parse it.

Example request

curl "https://api.zirowait.com/v1/entries/$ENTRY_ID" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": {
    "id": "clx8f2a1c0002abc123def456",
    "token": "A12",
    "tokenSeries": "A",
    "tokenNumber": 12,
    "status": "WAITING",
    "priority": false,
    "serviceId": null,
    "customer": {
      "name": "Priya",
      "phone": "919876500001"
    },
    "issuedAt": "2026-09-21T05:41:02.000Z",
    "startedAt": null,
    "completedAt": null,
    "canceledAt": null,
    "predictedWaitMinutes": 18,
    "position": 6
  }
}

Errors this endpoint can return

Cancel a waiting entry

POST/v1/entries/{entryId}/cancelscope: queue:write

Cancels an entry that is still waiting and returns it with the status CANCELED. An entry that is already in service, completed, canceled or a no-show cannot be canceled.

Parameters

NameInTypeDescription
entryIdrequiredpathstringThe entry. An opaque id. Treat it as a string; do not parse it.

Example request

curl -X POST "https://api.zirowait.com/v1/entries/$ENTRY_ID/cancel" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": {
    "id": "clx8f2a1c0002abc123def456",
    "token": "A12",
    "tokenSeries": "A",
    "tokenNumber": 12,
    "status": "CANCELED",
    "priority": false,
    "serviceId": null,
    "customer": {
      "name": "Priya",
      "phone": "919876500001"
    },
    "issuedAt": "2026-09-21T05:41:02.000Z",
    "startedAt": null,
    "completedAt": null,
    "canceledAt": "2026-09-21T05:52:10.000Z",
    "predictedWaitMinutes": 18
  }
}

Errors this endpoint can return

Services

List services

GET/v1/servicesscope: queue:read

The active services customers can queue for. Use a service id when you create an entry.

Parameters

This endpoint takes no parameters.

Example request

curl "https://api.zirowait.com/v1/services" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": [
    {
      "id": "clx8f2a1c0000abc123def456",
      "name": "Screen repair",
      "price": 1500,
      "baseTimeMinutes": 45
    }
  ]
}

Errors this endpoint can return

Appointments

List appointments

GET/v1/appointmentsscope: appointments:read

The appointments booked for one day, in time order. Notes are never returned. Results are paginated with a cursor.

Parameters

NameInTypeDescription
datequeryYYYY-MM-DDThe day, in the business's timezone. Defaults to today.
limitqueryintegerPage size, 1 to 100. Default 50.
cursorquerystringThe nextCursor from the previous page.

Example request

curl "https://api.zirowait.com/v1/appointments?date=2026-09-24" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": [
    {
      "id": "clx8f2a1c0003abc123def456",
      "date": "2026-09-24",
      "slotTime": "10:30",
      "durationMinutes": 15,
      "status": "CONFIRMED",
      "serviceId": null,
      "customer": {
        "name": "Rahul",
        "phone": "919812345670"
      }
    }
  ],
  "nextCursor": null
}

Errors this endpoint can return

Get an appointment

GET/v1/appointments/{appointmentId}scope: appointments:read

One appointment.

Parameters

NameInTypeDescription
appointmentIdrequiredpathstringThe appointment. An opaque id. Treat it as a string; do not parse it.

Example request

curl "https://api.zirowait.com/v1/appointments/$APPOINTMENT_ID" \
  -H "Authorization: Bearer $ZIROWAIT_KEY"

Response · 200

JSON · 200
{
  "data": {
    "id": "clx8f2a1c0003abc123def456",
    "date": "2026-09-24",
    "slotTime": "10:30",
    "durationMinutes": 15,
    "status": "CONFIRMED",
    "serviceId": null,
    "customer": {
      "name": "Rahul",
      "phone": "919812345670"
    }
  }
}

Errors this endpoint can return

Objects

The shape of everything the API returns. All fields are always present; a field that has no value is null, never missing.

Entry

One customer's place in a queue: a token.

FieldTypeDescription
idstringUnique id.
tokenstringThe label the customer sees, for example A12 (tokenSeries + tokenNumber).
tokenSeriesstringThe letter of the counter's series.
tokenNumberintegerThe number within the day and series.
statusenumWAITING, IN_SERVICE, COMPLETED, CANCELED or NO_SHOW.
prioritybooleanTrue if the business marked this entry urgent.
serviceIdstring | nullThe service they queued for, if any.
customerobjectname (string or null) and phone (digits with country code, or null).
issuedAtISO 8601 UTCWhen the token was created.
startedAtISO 8601 UTC | nullWhen service started.
completedAtISO 8601 UTC | nullWhen service finished.
canceledAtISO 8601 UTC | nullWhen it was canceled.
predictedWaitMinutesinteger | nullEstimated minutes until they are served. Null when unknown.
positioninteger | nullOnly on single-entry responses. 1 means next. Null unless the status is WAITING.
JSON
{
  "id": "clx8f2a1c0002abc123def456",
  "token": "A12",
  "tokenSeries": "A",
  "tokenNumber": 12,
  "status": "WAITING",
  "priority": false,
  "serviceId": null,
  "customer": {
    "name": "Priya",
    "phone": "919876500001"
  },
  "issuedAt": "2026-09-21T05:41:02.000Z",
  "startedAt": null,
  "completedAt": null,
  "canceledAt": null,
  "predictedWaitMinutes": 18,
  "position": 6
}

Queue and queue summary

A line of customers. The summary form (GET /queues/{queueId}) adds the day's counts.

FieldTypeDescription
idstringUnique id.
namestringFor example Main Queue.
isActivebooleanFalse if the business has switched the queue off.
isTokenCreationPausedbooleanTrue while the business is not taking new tokens. Creating one returns 409 queue_paused.
dateYYYY-MM-DDSummary only. The day the counts are for.
countsobjectSummary only. waiting, inService, completed, canceled and noShow, as integers.
averageWaitMinutesinteger | nullSummary only. The average estimated wait of the people waiting now.
JSON
{
  "id": "clx8f2a1c0001abc123def456",
  "name": "Main Queue",
  "isActive": true,
  "isTokenCreationPaused": false,
  "date": "2026-09-21",
  "counts": {
    "waiting": 6,
    "inService": 1,
    "completed": 24,
    "canceled": 2,
    "noShow": 1
  },
  "averageWaitMinutes": 18
}

Service

Something a customer can queue for.

FieldTypeDescription
idstringUnique id.
namestringFor example Screen repair.
priceinteger | nullWhole rupees.
baseTimeMinutesinteger | nullHow long it usually takes.
JSON
{
  "id": "clx8f2a1c0000abc123def456",
  "name": "Screen repair",
  "price": 1500,
  "baseTimeMinutes": 45
}

Appointment

A booked slot.

FieldTypeDescription
idstringUnique id.
dateYYYY-MM-DDThe day, in the business's timezone.
slotTimeHH:MM24-hour start time in the business's timezone.
durationMinutesintegerThe length of the slot.
statusenumCONFIRMED, CHECKED_IN, IN_SERVICE, COMPLETED, NO_SHOW or CANCELLED.
serviceIdstring | nullThe booked service, if any.
customerobjectname and phone.
JSON
{
  "id": "clx8f2a1c0003abc123def456",
  "date": "2026-09-24",
  "slotTime": "10:30",
  "durationMinutes": 15,
  "status": "CONFIRMED",
  "serviceId": null,
  "customer": {
    "name": "Rahul",
    "phone": "919812345670"
  }
}

Account (GET /me)

What your key is and what it may do.

FieldTypeDescription
businessobjectname, slug, category and timezone (an IANA name such as Asia/Kolkata).
keyobjectid, name, scopes and expiresAt.
limitsobjectrequestsPerMinute, writesPerMinute and maxBodyBytes.
JSON
{
  "business": {
    "name": "Sharma Service Center",
    "slug": "sharma-service",
    "category": "service",
    "timezone": "Asia/Kolkata"
  },
  "key": {
    "id": "ab12cd34ef56",
    "name": "Reception kiosk",
    "scopes": [
      "queue:read",
      "queue:write"
    ],
    "expiresAt": "2026-12-19T00:00:00.000Z"
  },
  "limits": {
    "requestsPerMinute": 120,
    "writesPerMinute": 30,
    "maxBodyBytes": 32768
  }
}

Recipes

Self check-in kiosk

Let a customer type their name and phone on a screen and get a token. Create the entry with an idempotency key, show the token and position, then poll the entry so the screen stays current.

Node.js
// 1. When the customer taps "Join"
const key = crypto.randomUUID();
const create = await fetch(`https://api.zirowait.com/v1/queues/${QUEUE_ID}/entries`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Idempotency-Key": key, "Content-Type": "application/json" },
  body: JSON.stringify({ name, phone }),
});
const { data: entry } = await create.json();
show(`Your token is ${entry.token}. ${entry.position - 1} people ahead.`);

// 2. Keep the screen up to date every 10 seconds
const timer = setInterval(async () => {
  const res = await fetch(`https://api.zirowait.com/v1/entries/${entry.id}`, { headers: { Authorization: `Bearer ${KEY}` } });
  const { data } = await res.json();
  show(data.status === "WAITING" ? `Position ${data.position}` : data.status);
  if (data.status !== "WAITING") clearInterval(timer);
}, 10000);

Lobby display board

Show "Now serving" and the next few tokens on a screen. One queue summary call gives the counts and average wait; one entries call gives the tokens.

Node.js
async function refresh() {
  const get = (path) => fetch(`https://api.zirowait.com/v1${path}`, { headers: { Authorization: `Bearer ${KEY}` } }).then((r) => r.json());
  const [{ data: queue }, serving, waiting] = await Promise.all([
    get(`/queues/${QUEUE_ID}`),
    get(`/queues/${QUEUE_ID}/entries?status=IN_SERVICE&limit=5`),
    get(`/queues/${QUEUE_ID}/entries?status=WAITING&limit=8`),
  ]);
  render({
    nowServing: serving.data.map((e) => e.token),
    next: waiting.data.map((e) => e.token),
    waitingCount: queue.counts.waiting,
    averageWaitMinutes: queue.averageWaitMinutes,
  });
}
setInterval(refresh, 10000);   // 3 requests every 10 seconds = 18 a minute, far below the limit

Nightly export to your own system

Read yesterday's completed entries page by page and write them to a file. Use the date parameter so the answer never depends on when the job runs.

Node.js
const day = "2026-09-20";                       // the business's calendar day
let cursor = null, rows = ["token,name,phone,completedAt"];
do {
  const url = new URL(`https://api.zirowait.com/v1/queues/${QUEUE_ID}/entries`);
  url.search = new URLSearchParams({ status: "COMPLETED", date: day, limit: "100", ...(cursor ? { cursor } : {}) });
  const page = await (await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } })).json();
  for (const e of page.data) rows.push([e.token, e.customer.name, e.customer.phone, e.completedAt].join(","));
  cursor = page.nextCursor;
} while (cursor);
await fs.promises.writeFile(`entries-${day}.csv`, rows.join("\n"));

Security

What we do

  • Keys are never stored. We keep only a keyed HMAC-SHA256 of the secret, so a database leak yields no usable keys.
  • 256-bit random secrets, verified in constant time. Unknown keys cost the same to reject as wrong ones.
  • Least privilege: scopes, expiry, an optional IP allowlist, instant revocation, at most 10 active keys per business.
  • Tenant isolation: the business always comes from the key, never from the URL or body, and every query is scoped to it.
  • Audit log: every authenticated request is recorded (method, path, status, IP, request id), never bodies or keys, and kept for 90 days.
  • Abuse controls: per-key rate limits, failed-sign-in blocking by IP, a 32 KB body cap, strict validation and no CORS.
  • Safe by default: off for every business until switched on. Error messages never contain internals or the values you sent.

What you should do

  • Keep keys on servers only, in a secret manager, not in code, apps, chat or tickets.
  • Create one key per integration with the fewest scopes, and set the IP allowlist.
  • Rotate keys on a schedule, and revoke one immediately if it may have leaked.
  • Send an Idempotency-Key on every POST.
To report a vulnerability, email support@zirowait.com. Please do not test against another business's data.

What this API does not have yet: a third-party security audit, request signing, OAuth-style consent screens and mTLS. We do not claim parity with the largest cloud providers.

Changelog

v1.1.02026-09-21

Clearer errors, a machine-readable spec and two new endpoints

  • New: GET /v1/openapi.json, an OpenAPI 3.1 description of the whole API. Import it into Postman, Insomnia or Swagger UI, or generate a client from it.
  • New: GET /v1/queues/{queueId} returns a queue with today's counts and the average wait, for lobby displays.
  • New: GET /v1/appointments/{appointmentId}.
  • Validation errors now include error.details: which fields were wrong and why (never the values you sent).
  • Every error now includes error.docUrl, a link to this page.
  • GET /v1/me now includes your key's rate limits.
  • Responses now carry an API-Version header. Creating an entry returns a Location header, also on an idempotent replay.
  • GET /v1/openapi.json and GET /v1 are public and need no key. Everything else is unchanged.
v1.0.02026-09-20

First release

  • Scoped, expiring API keys with IP allowlists.
  • Queues, entries, services and appointments (read), create and cancel tokens.
  • Idempotency keys, per-key rate limits, request ids and an audit log.

Coming next

  • Signed webhooks, so events are pushed to you instead of polled.
  • A free sandbox with test data.
  • Self-serve access with published pricing.

Until webhooks arrive, poll Get a queue for counts and List entries for changes.

Support

Stuck, or something here is unclear? Email support@zirowait.com and include the requestId from the failing response, the endpoint, and what you expected. The requestId lets us find your exact request.