Skip to content

Queue webhooks

When the fleet is full, POST /api/v1/sessions answers 202 with a queue_ticket and your client re-POSTs to poll — see Sessions. A server-side caller (a BFF holding a secret key) can additionally register a webhook for its place in line, cut the polling cadence, and keep its spot for minutes rather than seconds of silence.

One rule before anything else: webhooks reduce polling, they do not replace the claim; on queue.claimable, immediately re-POST the mint.

Registering

Add a notify block to the mint body. It rides the same request that enrolls you in the queue:

POST /api/v1/sessions
{
"persona": "mei",
"notify": {
"url": "https://your-bff.example.com/hooks/queue",
"secret": "whsec_9f2c…"
}
}
  • Secret keys only. Any other credential (publishable, device, issuer, browser session) gets 400 notify_not_allowed. The URL receives signed platform traffic and buys a longer liveness window — that is a server capability, never a browser one.
  • https only. Anything else is 400 invalid_notify_url.
  • secret is optional. When present, every delivery to your URL is signed with it (see below).
  • Re-POSTing the same body on each poll keeps the subscription fresh; changing the notify block mid-wait takes effect on the next poll.

Liveness: the notify lane

A plain polling waiter is dropped after ~12 seconds of silence. A notify-armed waiter lives for QUEUE_NOTIFY_IDLE_S (300 seconds) without polling — the webhook replaces the heartbeat. Two things bound the cost of an absent subscriber:

  • a reserved seat you never claim lapses after the claim window (45s), and two lapses drop your ticket (queue.dropped, reason lapsed);
  • when your user leaves, cancel. The cancel route is part of the notify contract — a subscriber that abandons tickets without cancelling is holding seats other people are waiting for.

Events

Every delivery is a POST with this envelope:

{
"event": "queue.claimable",
"workspace_id": "ws_abc123",
"data": { "ticket": "", "position": 1, "claim_deadline": 1755600045, "pod_host": "" },
"timestamp": 1755600000123
}
EventWhendata
queue.claimableA seat was reserved for your ticketticket, position, claim_deadline (epoch seconds the reservation lapses), pod_host
queue.positionYour position changed — throttled to at most one delivery per 30s per ticket, and only when the value movedticket, position, eta_seconds (may be null)
queue.droppedYour ticket left the line without seatingticket, reason: lapsed (unclaimed reservations), idle (liveness window expired), or drained (operator drained the queue)

On queue.claimable, re-POST the mint with your queue_ticket right away — the re-POST is the claim, and the reservation is only held until claim_deadline. After queue.dropped, a re-POST with the dead ticket transparently re-admits you at the back of the line.

Delivery is best-effort: a short in-process retry (immediately, then 2s, then 5s for the per-intent URL), no delivery log, no replay. A missed webhook costs you nothing but latency, because the poll loop remains the source of truth — keep a slow poll running even when subscribed.

Verifying signatures

Deliveries signed with a secret carry x-signature-256: sha256=<hex> — an HMAC-SHA256 of the raw request body. Per-intent deliveries use your notify.secret; workspace endpoint deliveries use that endpoint’s signing_secret. A notify block without a secret is delivered unsigned.

async function verify(rawBody: string, header: string, secret: string): Promise<boolean> {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(rawBody));
const hex = Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return header === `sha256=${hex}`;
}

Compute the HMAC over the body bytes exactly as received — re-serializing the parsed JSON can reorder keys and break the comparison.

Workspace-level endpoints

Independent of any single mint, a webhook endpoint subscribed to queue.* (or to an individual event name) receives every queue event for the workspace, signed with the endpoint’s own secret. Use this for ops-style visibility; use the per-intent notify block for the seat-claiming loop.