Skip to content

Queue

When every seat is taken, POST /api/v1/sessions answers 202 with a queue_ticket. Re-POSTing the mint with that ticket is how you keep your place and eventually claim a seat — that loop is unchanged. This page covers the rest of the queue contract: the two fields the 202 now carries, a cheap status read for the ticket, and the cancel route.

The extended 202

{
"status": "queued",
"queue_ticket": "8a6b2d10-51c4-4f7e-b0a9-71d3e9c4f882",
"position": 2,
"eta_seconds": 96,
"retry_after": 3,
"state": "claimable",
"claim_deadline": 1755600045
}
FieldTypeNotes
statestringAlways present. waiting = in line. claimable = a seat is reserved for this ticket
claim_deadlinenumberEpoch seconds the reservation lapses at. Only when state is claimable

A seat that frees up is not raced for — it is reserved for one specific ticket, and only that ticket’s next mint re-POST can take it. On state: "claimable", re-POST immediately instead of sleeping retry_after: the reservation is held for a limited window (45s by default), and an unclaimed window sends the seat to the next waiter.

Both fields are additive. A client that ignores them keeps working: its next scheduled re-POST lands as the claim well inside the window.

eta_seconds is now measured, not a constant — it tracks real session lengths and how long the current holders have been talking, and it is recomputed on every poll and status read. It stays advisory. position can move backwards when a higher-priority waiter joins, so lead with the ETA in your UI (“about 2 min”) and treat position as flavor.

Queue status

GET /api/v1/sessions/queue/{ticket}

Required scope: sessions:write or session:connect — the same credential family that can mint. The ticket carries a queue position, not a session, so no extra grant is needed.

A lightweight read for status widgets: no avatar resolution, no quota checks, no chance of minting. It counts as a liveness touch — the ticket holder is kept in line — but it can never claim. Only the mint re-POST does that.

{ "state": "waiting", "position": 3, "eta_seconds": 84, "retry_after": 3 }
{ "state": "claimable", "position": 1, "eta_seconds": 12, "claim_deadline": 1755600045, "retry_after": 3 }
{ "state": "expired", "retry_after": 3 }
FieldTypeNotes
statestringwaiting, claimable, or expired
positionnumber1-based place in line. Absent when expired
eta_secondsnumber | nullLive estimate, recomputed per read. Absent when expired
claim_deadlinenumberEpoch seconds. Only when claimable

A claimable ticket still shows a nonzero eta_seconds — the estimate prices the reserved seat conservatively until it is actually claimed. Key your UI on state, not on the ETA reaching zero. | retry_after | number | Suggested seconds until the next read. Always present |

expired means the ticket is unknown here: it aged out, was cancelled, lapsed too many claim windows, or was minted under a workspace your credential cannot act for. All of those read the same on purpose — the route answers 200 {"state": "expired"} rather than 404, so ticket ids cannot be probed across workspaces. On expired, re-POST the mint without a ticket to rejoin at the back of the line.

If your secret key is not workspace-bound (or you authenticate with a trusted-issuer id_token), pass the workspace the mint named in its body as a query parameter:

GET /api/v1/sessions/queue/{ticket}?workspace_id=019...

Reading status keeps the ticket alive, but a waiter that stops touching the platform entirely — no status reads, no mint polls — is dropped after about 12 seconds. A status widget polling at retry_after cadence stays well inside that.

Leave the queue

DELETE /api/v1/sessions/queue/{ticket}

Required scope: same family as above. Answers 204 with no body — always, including for unknown or already-expired tickets, so it is safe to fire from retries, abort handlers, and page-unload beacons.

The ticket leaves the line immediately. A seat reserved for it goes to the next compatible waiter right away instead of idling until the claim window lapses. Call this whenever your user stops waiting — closes the tab, taps cancel, navigates away:

window.addEventListener('pagehide', () => {
fetch(`https://api.casola.ai/api/v1/sessions/queue/${ticket}`, {
method: 'DELETE',
headers: { authorization: `Bearer ${key}` },
keepalive: true,
});
});

An uncancelled ticket is not a disaster — the liveness reaper drops it once it stops polling — but cancelling frees the seat seconds earlier for whoever is next.