{"openapi":"3.1.0","info":{"title":"Support Queue API","version":"1.0.0","description":"Mock helpdesk queue for the customer support agent workshop. Each team gets its own 4-hour run of the same tickets, starting when its webhook is verified; what you write is visible only to your team. Errors are always {\"error\": \"…\"}. Queue endpoints may answer 429 (with Retry-After) or 500 at any time: retry, and reuse your Idempotency-Key for actions."},"tags":[{"name":"Onboarding","description":"Register, set up delivery, practise in the sandbox."},{"name":"Queue","description":"Read events and tickets, look up new records, act on tickets."},{"name":"Admin","description":"Facilitator only."}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"Your team's api_key."}},"schemas":{"Event":{"type":"object","properties":{"id":{"type":"string","description":"Stable across redeliveries: use it to drop duplicates.","example":"evt_TKT-015382"},"seq":{"type":"number","description":"Your team's position in its event log; the polling cursor."},"type":{"type":"string","enum":["ticket.created","ticket.message_created"]},"sandbox":{"type":"boolean"},"created_at":{"type":"string","description":"When the queue released the event (real time)."},"data":{"type":"object","properties":{"ticket_id":{"type":"string"},"message_id":{"type":"string"},"simulated_at":{"type":"string","description":"When it happened in the simulated timeline."}},"required":["ticket_id","simulated_at"]}},"required":["id","seq","type","sandbox","created_at","data"]},"TeamCredentials":{"type":"object","properties":{"team_id":{"type":"string"},"name":{"type":"string"},"api_key":{"type":"string","description":"Send as Authorization: Bearer <api_key>."},"webhook_secret":{"type":"string","description":"Used to verify webhook signatures."}},"required":["team_id","name","api_key","webhook_secret"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"RegisterTeam":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,39}$","example":"team-rocket"},"join_code":{"type":"string","description":"Given out by the facilitator."}},"required":["name","join_code"]},"SetWebhook":{"type":"object","properties":{"url":{"type":"string","format":"uri","example":"http://my-laptop:3000/hooks"}},"required":["url"]},"EventsPage":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"next_after":{"type":"number"}},"required":["events","next_after"]},"TicketView":{"type":"object","properties":{"ticket_id":{"type":"string"},"ticket":{"type":["object","null"],"properties":{"ticket_id":{"type":"string"},"customer_id":{"type":"string"},"subject":{"type":"string"},"description":{"type":"string"},"channel":{"type":"string"},"created_at":{"type":"string"},"platform":{"type":["string","null"]},"app_version":{"type":["string","null"]},"error_reference":{"type":["string","null"]}},"required":["ticket_id","customer_id","subject","description","channel","created_at","platform","app_version","error_reference"],"description":"null for tickets opened before the cutoff; their history is in lake.db."},"messages":{"type":"array","items":{"type":"object","properties":{"message_id":{"type":"string"},"sent_at":{"type":"string"},"author":{"type":"string"},"body":{"type":"string"},"sender":{"type":"string","enum":["customer"]}},"required":["message_id","sent_at","author","body","sender"]}},"status":{"type":["string","null"],"enum":["closed","replied","escalated",null],"description":"Set by your latest action."},"actions":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["ticket_id","ticket","messages","status","actions"]},"ActionResult":{"type":"object","properties":{"action_id":{"type":"string"},"ticket_id":{"type":"string"},"status":{"type":"string","enum":["closed","replied","escalated"]},"replayed":{"type":"boolean"}},"required":["action_id","ticket_id","status","replayed"]},"Action":{"type":"object","properties":{"action":{"type":"string","enum":["close","reply","escalate"],"description":"escalate = hand to a human."},"body":{"type":"string","minLength":1,"description":"Required for reply: the text for the customer. Nothing is really sent."},"handoff":{"type":"object","additionalProperties":{},"description":"Required for escalate: everything the next owner needs. The shape is yours to design.","example":{"summary":"…","evidence":["INV-…"],"open_questions":["…"],"next_owner":"billing"}},"category":{"type":"string","example":"Billing"},"subtype":{"type":"string","example":"Charged twice"},"priority":{"type":"string","enum":["Low","Medium","High","Critical"]},"flags":{"type":"array","items":{"type":"string"},"example":["churn_risk"]},"note":{"type":"string"}},"required":["action"]},"Records":{"type":"object","properties":{"records":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Current version of each record. A record that exists in lake.db and changed later (a churn, a refund processed) appears here with its new values."}},"required":["records"]}},"parameters":{}},"paths":{"/teams":{"post":{"tags":["Onboarding"],"summary":"Register a team","description":"Self-service. Needs the workshop join code. The api_key and webhook_secret are only shown here, so save them. Your 4-hour run starts once you set a webhook.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterTeam"}}}},"responses":{"201":{"description":"Team created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamCredentials"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/teams/me/webhook":{"put":{"tags":["Onboarding"],"summary":"Set or change your webhook","description":"The queue POSTs {\"type\":\"webhook.verification\",\"challenge\":\"…\"} to the URL. Answer 200 with the challenge (raw or as {\"challenge\":\"…\"}). The URL is saved only once verified. The first verified webhook starts your 4-hour run: 20 tickets a minute, delivered here and readable via GET /events. Changing the URL later does not restart it.","security":[{"bearer":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetWebhook"}}}},"responses":{"200":{"description":"Verified and saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetWebhook"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sandbox/start":{"post":{"tags":["Onboarding"],"summary":"Start (or restart) your private sandbox","description":"Sends 20 practice tickets to your team only, marked sandbox: true. Customers write back right after you act. Restarting wipes your sandbox events and actions. Not scored.","security":[{"bearer":[]}],"responses":{"200":{"description":"Sandbox started","content":{"application/json":{"schema":{"type":"object","properties":{"tickets":{"type":"number"}},"required":["tickets"]}}}}}}},"/lake.db":{"get":{"tags":["Queue"],"summary":"Download lake.db","description":"History up to the cutoff as a SQLite file. Same for every team.","security":[{"bearer":[]}],"responses":{"200":{"description":"SQLite database","content":{"application/vnd.sqlite3":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events":{"get":{"tags":["Queue"],"summary":"Poll your events","description":"Your events with seq > after, oldest first. Pass next_after back as after. Works with or without a webhook, also to catch up after a restart.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":["integer","null"],"minimum":0,"default":0},"required":false,"name":"after","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsPage"}}}}}}},"/tickets/{id}":{"get":{"tags":["Queue"],"summary":"Read a ticket","description":"The ticket as the customer sent it, the customer messages released to you, and your own actions. No labels or outcomes: deciding those is your job.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","example":"TKT-015382"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Ticket","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketView"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tickets/{id}/actions":{"post":{"tags":["Queue"],"summary":"Act on a ticket","description":"Requires an Idempotency-Key header (scoped to your team). Same key and same body returns the original result with replayed: true; same key with a different body is a 422. You can act more than once: the latest action sets the status, and every action is kept.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","example":"TKT-015382"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":200,"example":"TKT-015382:1"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Action"}}}},"responses":{"200":{"description":"Replay of an earlier request with the same key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResult"}}}},"201":{"description":"Action recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResult"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/records/{table}":{"get":{"tags":["Queue"],"summary":"Records created or changed after the cutoff","description":"lake.db holds everything up to the cutoff. New and changed customers, subscriptions, invoices, payments, refunds and account events appear here as your run passes them. Filter by exactly one field, e.g. ?customer_id=CUS-000552.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","enum":["customers","subscriptions","payment_methods","invoices","invoice_items","transactions","refunds","account_events"]},"required":true,"name":"table","in":"path"}],"responses":{"200":{"description":"Records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Records"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"ticket-event":{"post":{"summary":"Event delivered to your webhook","description":"POSTed to your verified webhook URL. Header x-signature: t=<unix seconds>,v1=<hex HMAC-SHA256(webhook_secret, t + '.' + raw_body)>. Answer 2xx within 5 s; anything else is retried, from the same event, with backoff up to once a minute. Events arrive in order, at least once.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"responses":{"200":{"description":"Accepted"}}}}}}