Base URL: your deployment (default http://localhost:4025). Machine-readable
spec: openapi.json.
Only one route is paid — joining. Seeing the wait, checking your position,
claiming your refund and leaving are all free. The paid route returns
402 Payment Required until called with a valid X-PAYMENT header, and its
successful response carries an X-PAYMENT-RESPONSE settlement receipt header.
Every queue with its live length and current wait.
200
{
"venue": { "name": "402 Walk-ins", "timezone": "America/New_York", "address": "402 Payment Ave" },
"holdPolicy": { "joinPrice": "$0.01", "refundIfNotServedMinutes": 30, "description": "…" },
"generatedAt": "2026-08-07T18:00:00.000Z",
"queues": [
{
"id": "dining-room",
"name": "Dining room walk-in list",
"description": "Tables for parties of 1–6…",
"open": true,
"avgServeMinutes": 12,
"parallelServers": 2,
"maxLength": 40,
"graceMinutes": 5,
"waiting": 7,
"beingServed": 2,
"spaceLeft": 33,
"estimatedWaitMinutes": 36,
"acceptingJoins": true
}
]
}estimatedWaitMinutes = floor(waiting / parallelServers) × avgServeMinutes.
acceptingJoins is false when the queue is closed or at maxLength.
The live board for one queue. Names are reduced to an initial — the board is public, the identities are not.
{
"queueId": "dining-room",
"name": "Dining room walk-in list",
"open": true,
"waiting": 3,
"estimatedWaitMinutes": 12,
"generatedAt": "2026-08-07T18:00:00.000Z",
"entries": [
{ "position": 1, "holder": "A.", "party": 2, "status": "waiting", "joinedAt": "…" },
{ "position": null, "holder": "B.", "party": 4, "status": "called", "joinedAt": "…", "calledAt": "…" }
]
}Errors: 404 UNKNOWN_QUEUE.
Body
{ "queue": "dining-room", "name": "Ada Lovelace", "party": 2, "contact": "+15550402402" }| Field | Required | Notes |
|---|---|---|
queue |
yes | Queue id from /queues. |
name |
yes | Shown to the operator when your party is called. |
party |
no | Positive integer, defaults to 1. |
contact |
no | Only surfaced to the operator at call time. |
200 — the purchased artifact
{
"ticketId": "tkt_1a2b3c4d5e6f",
"token": "<base64url payload>.<hex HMAC>",
"position": 8,
"ahead": 7,
"party": 2,
"holder": "Ada Lovelace",
"queue": { "id": "dining-room", "name": "Dining room walk-in list", "description": "…" },
"venue": "402 Walk-ins",
"eta": { "minutes": 36, "at": "2026-08-07T18:36:00.000Z", "basis": "7 ahead, 2 server(s), ~12 min each" },
"graceMinutes": 5,
"holdPolicy": { "joinPrice": "$0.01", "refundIfNotServedMinutes": 30, "description": "…" },
"refundClaim": {
"claimId": "clm_9f8e7d6c5b4a",
"ticketId": "tkt_1a2b3c4d5e6f",
"amount": "$0.01",
"reason": "auto-refund if not served within 30 minutes of the quoted ETA",
"claimableAfter": "2026-08-07T19:06:00.000Z",
"claimEndpoint": "POST /claim/<token>",
"signature": "hex HMAC-SHA256"
},
"positionUrl": "https://queue.example.com/position/<token>",
"positionEndpoint": "GET /position/<token>",
"leaveEndpoint": "POST /leave/<token>",
"joinedAt": "2026-08-07T18:00:00.000Z",
"signature": "hex HMAC-SHA256"
}| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_QUEUE / INVALID_NAME / INVALID_PARTY |
malformed body |
| 402 | — | payment missing/invalid |
| 404 | UNKNOWN_QUEUE |
no such queue id |
| 409 | QUEUE_CLOSED |
the queue is not accepting anyone |
| 409 | QUEUE_FULL |
at maxLength waiting |
None of the 4xx cases charge the caller: settlement is deferred until the
handler returns 2xx.
Live position, serve status and refund status. Free and pollable — you paid once for the place in line, not for the right to look at it.
{
"ticketId": "tkt_1a2b3c4d5e6f",
"queue": { "id": "dining-room", "name": "Dining room walk-in list" },
"venue": "402 Walk-ins",
"holder": "Ada Lovelace",
"party": 2,
"status": "waiting",
"position": 3,
"ahead": 2,
"eta": { "minutes": 12, "at": "2026-08-07T18:24:00.000Z", "quotedAtJoin": "2026-08-07T18:36:00.000Z" },
"calledAt": null,
"servedAt": null,
"closedAt": null,
"closedReason": null,
"overdueByMinutes": 0,
"refund": {
"issued": false,
"due": false,
"reason": "still in line and inside the promised window",
"claimableAfter": "2026-08-07T19:06:00.000Z",
"amount": "$0.01"
},
"holdPolicy": { … },
"checkedAt": "2026-08-07T18:12:00.000Z"
}status is one of:
| Status | Meaning | Hold |
|---|---|---|
waiting |
in line | held |
called |
your turn — graceMinutes to appear |
held |
served |
seen | returned |
left |
you gave up your place | returned |
no-show |
called and never appeared | forfeited |
refunded |
claim exercised while still in line | returned |
position and eta are null once you are no longer waiting.
Errors: 400 BAD_TOKEN (malformed, altered, or forged), 404 TICKET_NOT_FOUND.
Exercise the refund claim. Idempotent, and authenticated by the token itself — the instrument was handed over at join time.
The hold is due back when you were served, when you left before being called,
or when the venue missed the quoted ETA by more than refundIfNotServedMinutes.
It is forfeited only when you were called and did not show up.
{
"ticketId": "tkt_1a2b3c4d5e6f",
"queueId": "dining-room",
"refunded": true,
"amount": "$0.01",
"reason": "not served within 30 minutes of the quoted ETA — the hold is returned",
"wallet": "0x…",
"issuedAt": "2026-08-07T19:07:00.000Z",
"signature": "…"
}A repeat call returns the same record with alreadyIssued: true.
Errors: 409 REFUND_NOT_DUE (message carries the reason), 400 BAD_TOKEN,
404 TICKET_NOT_FOUND.
Give up your place. The hold is returned and the queue closes up behind you.
Errors: 409 ALREADY_CLOSED, 400 BAD_TOKEN, 404 TICKET_NOT_FOUND.
Front of house, or your POS integration.
| Route | Effect |
|---|---|
POST /call-next/:queueId |
Calls the oldest waiting party. Returns their name, party size, contact, and graceMinutes. 409 QUEUE_EMPTY if nobody is waiting. |
POST /serve/:ticketId |
Marks a called party served; the hold is returned. 409 ALREADY_SERVED / 409 ALREADY_CLOSED. |
POST /no-show/:ticketId |
Called but never appeared; the hold is forfeited. 409 NOT_CALLED if they were never called, and 409 GRACE_PERIOD until the full graceMinutes has elapsed — nobody can be written off early. |
| Route | Returns |
|---|---|
GET /info |
venue profile, hold policy, prices, payment rails |
GET /health |
liveness |
GET /.well-known/x402 |
x402 discovery manifest (resources, prices, schemas, both rails) |
The paid route answers an unpaid request with a 402 whose accepts array
carries both payment rails. Pick one, sign it, retry with X-PAYMENT.
{
"x402Version": 1,
"error": "X-PAYMENT header is required",
"accepts": [
{
"scheme": "exact",
"network": "base-sepolia",
"maxAmountRequired": "10000",
"resource": "http://localhost:4025/join",
"description": "Join a live waitlist with a refundable hold…",
"mimeType": "application/json",
"payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"maxTimeoutSeconds": 300,
"extra": { "name": "USDC", "version": "2" }
},
{
"scheme": "exact",
"network": "solana",
"maxAmountRequired": "10000",
"resource": "http://localhost:4025/join",
"description": "Join a live waitlist with a refundable hold…",
"mimeType": "application/json",
"payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"maxTimeoutSeconds": 300,
"extra": { "rpcUrl": "https://api.mainnet-beta.solana.com" }
}
]
}| Field | Meaning |
|---|---|
network |
base-sepolia/base = EVM rail; solana/solana-devnet = SVM rail |
maxAmountRequired |
price in atomic USDC units (6 decimals) — 10000 = $0.01 |
asset |
USDC contract address (EVM) or SPL mint (Solana) |
payTo |
merchant receive address on that network |
extra |
EVM: the EIP-712 domain to sign against. Solana: the RPC to build against. |
Configure the rails with NETWORK / PAY_TO_ADDRESS / FACILITATOR_URL (EVM)
and SOLANA_NETWORK / SOLANA_PAY_TO_ADDRESS / SOLANA_RPC_URL /
SOLANA_FACILITATOR_URL (Solana). Each rail settles through its own facilitator
because no public one handles both chains. Drop an address and that rail is
omitted from every challenge.
A successful paid call returns X-PAYMENT-RESPONSE: base64 JSON of
{ success, transaction, network, payer }. network tells you which rail
settled. Settlement is deferred until the handler returns 2xx — an error
response (e.g. 409 QUEUE_FULL) never moves funds.