{"openapi":"3.1.0","info":{"title":"Juryza API","version":"1.0.0","description":"The REST API behind every screen in Juryza — anything the app does, a script can do.\n\n## Authentication\nCreate a personal token at **/settings/tokens** (or `POST /api/me/tokens`) and send it on every request:\n\n    Authorization: Bearer jz_…\n\nBrowsers use the Better Auth session cookie instead (`POST /api/auth/sign-in/email`). Authorization is enforced by the server on every call, whatever the UI shows: each operation lists who may call it.\n\n## Errors\nFailures return JSON `{ \"error\": \"Human-readable message\" }` with the HTTP status. Validation failures (400) add `issues`, the Zod issue list with the offending `path`.\n\n## Rate limits\nVoting, commenting, voter verification and sign-in are rate limited. A 429 carries `retryAfterSeconds`.\n\n## Webhooks\nOrganizers subscribe with `POST /api/events/{event}/webhooks`. Each delivery is a POST of `{ type, sentAt, data }` with headers `X-Juryza-Event` and `X-Juryza-Signature: sha256=<hex>` — an HMAC-SHA256 of the raw body keyed with the subscription secret. Compare it in constant time before trusting the payload. Types: `event.created`, `event.updated`, `team.created`, `team.joined`, `project.submitted`, `project.updated`, `judge.invited`, `judge.joined`, `assignments.generated`, `score.saved`, `vote.cast`, `comment.posted`, `results.published`, `certificate.issued`.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"/","description":"This Juryza instance"}],"tags":[{"name":"Auth","description":"Better Auth session endpoints (cookie sessions for browsers). API clients use a personal token instead."},{"name":"Events","description":"Hackathons: details, lifecycle dates, configuration, registration and announcements."},{"name":"Tracks & rubric","description":"Tracks (categories), weighted scoring criteria and the prize table."},{"name":"Teams","description":"Team formation: create, join by invite link, leave, disband."},{"name":"Projects","description":"Submissions: the public gallery, drafts, editing and similarity."},{"name":"Comments","description":"The public discussion thread on each submitted project."},{"name":"Judging","description":"The judging panel, assignments and a judge's own rubric scores."},{"name":"Pairwise","description":"Gavel-style pairwise judging fitted with Bradley–Terry."},{"name":"Voting","description":"Quadratic community voting: ballots, votes and email verification."},{"name":"Results","description":"The leaderboard (hidden until published) and side-by-side comparison."},{"name":"Organizer data","description":"Organizer console data: dashboard, audit trail, exports, bundles and duplicate detection."},{"name":"Certificates","description":"Signed participation and winner certificates, publicly verifiable."},{"name":"Webhooks","description":"Signed HTTP callbacks for event activity."},{"name":"Me","description":"The caller's own account, dashboard and personal API tokens."},{"name":"Users","description":"Public profiles."},{"name":"Admin","description":"Platform administration (admins only)."},{"name":"System","description":"Operational endpoints for monitors and tooling."}],"paths":{"/api/auth/sign-up/email":{"post":{"operationId":"postAuthSignUpEmail","tags":["Auth"],"summary":"Create an account","description":"Better Auth email + password sign-up. New accounts get the `participant` role and a generated username, and are signed in (session cookie set). Rate limited to 5 per minute. Send an `Origin` header matching the app.","security":[],"x-access":"public","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"email":{"type":"string","format":"email","description":"Email address (case-insensitive)."},"password":{"type":"string","minLength":8,"description":"At least 8 characters."}},"required":["name","email","password"]}}}},"responses":{"200":{"description":"`{ token, user }` and a `Set-Cookie` session.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"429":{"$ref":"#/components/responses/E429"}}}},"/api/auth/sign-in/email":{"post":{"operationId":"postAuthSignInEmail","tags":["Auth"],"summary":"Sign in","description":"Better Auth email + password sign-in; sets the session cookie used by `cookieAuth`. Rate limited to 10 per minute. Browsers and cookie-jar clients must send an `Origin` header matching the app (CSRF protection).","security":[],"x-access":"public","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Email address (case-insensitive)."},"password":{"type":"string","minLength":8},"rememberMe":{"description":"Keep the session beyond the browser session.","type":"boolean"}},"required":["email","password"]}}}},"responses":{"200":{"description":"`{ redirect, token, user }` and a `Set-Cookie` session.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"429":{"$ref":"#/components/responses/E429"}}}},"/api/auth/sign-out":{"post":{"operationId":"postAuthSignOut","tags":["Auth"],"summary":"Sign out","description":"Ends the cookie session. Personal API tokens are unaffected — revoke them with `DELETE /api/me/tokens`.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","responses":{"200":{"description":"`{ success: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"}}}},"/api/auth/get-session":{"get":{"operationId":"getAuthGetSession","tags":["Auth"],"summary":"Current session","description":"The Better Auth session behind the cookie, or `null` when signed out. Does not accept bearer tokens — use `GET /api/me`.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","responses":{"200":{"description":"`{ session, user }` or `null`.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/events":{"get":{"operationId":"getEvents","tags":["Events"],"summary":"List events","description":"Published events, plus the caller's own drafts (admins see every event). Newest deadline first.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","responses":{"200":{"description":"`{ events: (Event & { projectCount, participantCount })[] }`.","content":{"application/json":{"schema":{"type":"object"}}}}}},"post":{"operationId":"postEvents","tags":["Events"],"summary":"Create an event","description":"Requires the `organizer` platform role (403 otherwise). Starts as a draft unless `visibility: \"published\"`, owned by the caller, with a default four-criterion rubric and the given `tracks`. 409 if the slug is taken.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":80},"slug":{"type":"string","minLength":3,"maxLength":48,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"tagline":{"anyOf":[{"type":"string","maxLength":140},{"type":"null"}]},"content":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"rules":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"mode":{"default":"online","type":"string","enum":["online","in-person","hybrid"]},"location":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"hue":{"default":250,"type":"integer","minimum":0,"maximum":360},"visibility":{"default":"draft","type":"string","enum":["draft","published"]},"submissionsOpen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"submissionsClose":{"type":"string","format":"date-time"},"judgingClose":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"votingOpen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"votingClose":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"maxTeamSize":{"default":4,"type":"integer","minimum":1,"maximum":20},"reviewsPerProject":{"default":3,"type":"integer","minimum":1,"maximum":10},"votingAccess":{"default":"authenticated","type":"string","enum":["open","email","authenticated"]},"votingEmailDomains":{"default":[],"maxItems":20,"type":"array","items":{"type":"string","minLength":3}},"voteBudget":{"default":16,"type":"integer","minimum":1,"maximum":400},"votingMode":{"default":"quadratic","type":"string","enum":["quadratic","writeup"]},"votingShortlistSize":{"default":0,"type":"integer","minimum":0,"maximum":100},"votingElectorateLockAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"tracks":{"default":[],"maxItems":30,"type":"array","items":{"type":"string","minLength":1,"maxLength":60}}},"required":["name","slug","submissionsClose"]}}}},"responses":{"201":{"description":"`{ id, slug }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"}}}},"/api/events/import":{"post":{"operationId":"postEventsImport","tags":["Events"],"summary":"Import an event bundle","description":"Requires the `organizer` role. Creates a NEW draft event owned by the caller from a Juryza bundle (`GET /api/events/{event}/bundle`) or any file in the DOGFOOD fixtures shape. Ids are kept when free and remapped when they collide; people are matched by email and unknown emails get a fresh account.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"format":{"type":"string"},"event":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","minLength":1},"slug":{"type":"string"},"tagline":{"type":["string","null"]},"description":{"type":["string","null"]},"submissions_open":{"type":["string","null"]},"submissions_close":{"type":"string"},"judging_close":{"type":["string","null"]},"voting_open":{"type":["string","null"]},"voting_close":{"type":["string","null"]},"max_team_size":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reviews_per_project":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"voting_access":{"type":"string","enum":["open","email","authenticated"]},"vote_budget":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"hue":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"is_fixture":{"type":"boolean"}},"required":["name","submissions_close"]},"tracks":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}},"required":["id","name"]}},"rubric":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"weight":{"type":"number","exclusiveMinimum":0},"description":{"type":["string","null"]}},"required":["key","label","weight"]}},"prizes":{"default":[],"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"kind":{"default":"overall","type":"string","enum":["overall","track","community"]},"track":{"type":["string","null"]},"amount":{"type":["string","null"]},"rank":{"default":1,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":{"type":["string","null"]}},"required":["name"]}},"judges":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"tracks":{"default":[],"type":"array","items":{"type":"string"}}},"required":["id","name","email"]}},"teams":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"members":{"default":[],"type":"array","items":{"type":"string"}}},"required":["id","name"]}},"projects":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"team":{"type":["string","null"]},"track":{"type":["string","null"]},"title":{"type":"string"},"tagline":{"type":["string","null"]},"summary":{"type":["string","null"]},"content":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"repo_url":{"type":["string","null"]},"live_url":{"type":["string","null"]},"video_url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"tech_tags":{"default":[],"type":"array","items":{"type":"string"}},"status":{"default":"submitted","type":"string","enum":["draft","submitted"]},"submitted_at":{"type":["string","null"]}},"required":["id","title"]}},"scores":{"default":[],"type":"array","items":{"type":"object","properties":{"judge":{"type":"string"},"project":{"type":"string"},"criteria":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"number"}},"comment":{"type":["string","null"]}},"required":["judge","project","criteria"]}},"pairwise":{"default":[],"type":"array","items":{"type":"object","properties":{"judge":{"type":"string"},"winner":{"type":"string"},"loser":{"type":"string"}},"required":["judge","winner","loser"]}}},"required":["event"]}}}},"responses":{"201":{"description":"`{ eventId, slug, created: { users, tracks, teams, projects, judges, scores, comparisons }, skipped: string[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/api/events/{event}":{"get":{"operationId":"getEventsByEvent","tags":["Events"],"summary":"Get an event","description":"The event with tracks, prizes, rubric, judging panel, the latest five announcements (pinned first) and counts. Draft events 404 for anyone but their organizers. Signed-in callers also get `viewer`.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ event, tracks, prizes, criteria, judges, announcements, counts: { projects, participants, teams }, viewer: { registered, team, isJudge, canManage } | null }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"patchEventsByEvent","tags":["Events"],"summary":"Update an event","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Only the fields sent are changed; 409 if a new slug is taken.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":80},"slug":{"type":"string","minLength":3,"maxLength":48,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"tagline":{"anyOf":[{"type":"string","maxLength":140},{"type":"null"}]},"content":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"rules":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"mode":{"type":"string","enum":["online","in-person","hybrid"]},"location":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"hue":{"type":"integer","minimum":0,"maximum":360},"visibility":{"type":"string","enum":["draft","published"]},"submissionsOpen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"submissionsClose":{"type":"string","format":"date-time"},"judgingClose":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"votingOpen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"votingClose":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"maxTeamSize":{"type":"integer","minimum":1,"maximum":20},"reviewsPerProject":{"type":"integer","minimum":1,"maximum":10},"votingAccess":{"type":"string","enum":["open","email","authenticated"]},"votingEmailDomains":{"maxItems":20,"type":"array","items":{"type":"string","minLength":3}},"voteBudget":{"type":"integer","minimum":1,"maximum":400},"votingMode":{"type":"string","enum":["quadratic","writeup"]},"votingShortlistSize":{"type":"integer","minimum":0,"maximum":100},"votingElectorateLockAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}}},"responses":{"200":{"description":"`{ event }` — the updated event.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}},"delete":{"operationId":"deleteEventsByEvent","tags":["Events"],"summary":"Delete an event","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Deletes the event and everything in it (teams, projects, scores, votes). Irreversible.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/registration":{"post":{"operationId":"postEventsByEventRegistration","tags":["Events"],"summary":"Register for an event","description":"Registers the caller. 403 once submissions have closed. Idempotent.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ registered: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteEventsByEventRegistration","tags":["Events"],"summary":"Withdraw registration","description":"400 while the caller is still on a team — leave it first.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ registered: false }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/announcements":{"get":{"operationId":"getEventsByEventAnnouncements","tags":["Events"],"summary":"List announcements","description":"Organizer updates, pinned first, then newest.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ announcements: Announcement[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventAnnouncements","tags":["Events"],"summary":"Post an announcement","description":"**Organizers only:** 403 unless the caller created this event (or is an admin).","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":2,"maxLength":120},"body":{"type":"string","minLength":1,"maxLength":4000},"pinned":{"default":false,"description":"Pinned announcements are listed first.","type":"boolean"}},"required":["title","body"]}}}},"responses":{"201":{"description":"`{ id }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/announcements/{id}":{"delete":{"operationId":"deleteAnnouncementsById","tags":["Events"],"summary":"Delete an announcement","description":"**Organizers only:** 403 unless the caller manages the announcement's event.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Announcement id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/tracks":{"get":{"operationId":"getEventsByEventTracks","tags":["Tracks & rubric"],"summary":"List tracks","description":"The event's tracks in display order.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ tracks: Track[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"putEventsByEventTracks","tags":["Tracks & rubric"],"summary":"Replace tracks","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Rows with an existing `id` are updated in place (projects keep their track), rows without one are created, and tracks left out are deleted (their projects become untracked).","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tracks":{"maxItems":30,"type":"array","items":{"type":"object","properties":{"id":{"description":"Existing track id to update in place; omit to create.","type":"string"},"name":{"type":"string","minLength":1,"maxLength":60},"description":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}]}},"required":["name"]}}},"required":["tracks"]}}}},"responses":{"200":{"description":"`{ tracks: Track[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/rubric":{"get":{"operationId":"getEventsByEventRubric","tags":["Tracks & rubric"],"summary":"Get the rubric","description":"The weighted scoring criteria in display order.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ criteria: RubricCriterion[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"putEventsByEventRubric","tags":["Tracks & rubric"],"summary":"Replace the rubric","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Rows are matched by `key` (what judges' marks are stored against). Weights are relative and results recompute on read, so re-weighting never invalidates marks. 400 on duplicate keys.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,31}$","description":"Stable lowercase identifier marks are stored against, e.g. `code_quality`."},"label":{"type":"string","minLength":1,"maxLength":60},"description":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}]},"weight":{"type":"number","exclusiveMinimum":0,"maximum":100,"description":"Relative weight; normalized to sum to 1 when scoring."}},"required":["key","label","weight"]}}},"required":["criteria"]}}}},"responses":{"200":{"description":"`{ criteria: RubricCriterion[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/prizes":{"get":{"operationId":"getEventsByEventPrizes","tags":["Tracks & rubric"],"summary":"List prizes","description":"The prize table. Winners are derived from results, never stored.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ prizes: Prize[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"putEventsByEventPrizes","tags":["Tracks & rubric"],"summary":"Replace prizes","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). A prize is awarded by overall judged `rank`, by `rank` within one track (`kind: \"track\"`), or by community vote (`kind: \"community\"`).","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prizes":{"maxItems":40,"type":"array","items":{"type":"object","properties":{"kind":{"default":"overall","type":"string","enum":["overall","track","community"]},"trackId":{"description":"Required for `kind: \"track\"`, ignored otherwise.","type":["string","null"]},"name":{"type":"string","minLength":1,"maxLength":80},"description":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}]},"amount":{"description":"Free text, e.g. \"$1,000\".","anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"rank":{"default":1,"description":"Which place wins it (1 = first).","type":"integer","minimum":1,"maximum":50}},"required":["name"]}}},"required":["prizes"]}}}},"responses":{"200":{"description":"`{ prizes: Prize[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/teams":{"get":{"operationId":"getEventsByEventTeams","tags":["Teams"],"summary":"List teams","description":"Every team with its members (invite links are never included).","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ maxTeamSize, teams: (Team & { projectId, members })[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventTeams","tags":["Teams"],"summary":"Create a team","description":"The caller becomes the owner and is registered for the event. 403 once submissions have closed; 409 if the caller is already on a team in this event.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":60},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"lookingForMembers":{"default":false,"type":"boolean"}},"required":["name"]}}}},"responses":{"201":{"description":"`{ id, inviteUrl }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}}},"/api/teams/{id}":{"get":{"operationId":"getTeamsById","tags":["Teams"],"summary":"Get a team","description":"The team, its members and project. Members and the event's organizers also get `inviteUrl`, and see a draft project.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"id","in":"path","required":true,"description":"Team id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ team, event, members, project, viewer: { role, canManage, open } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"patchTeamsById","tags":["Teams"],"summary":"Update a team","description":"403 unless the caller is the team owner or an organizer of the event.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Team id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":60},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"lookingForMembers":{"type":"boolean"}}}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteTeamsById","tags":["Teams"],"summary":"Disband a team","description":"Team owner (or an organizer of the event). For owners: 403 after the submission deadline, and 403 while the team still has a project.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Team id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true, eventSlug }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/teams/{id}/invite":{"post":{"operationId":"postTeamsByIdInvite","tags":["Teams"],"summary":"Reset the invite link","description":"Team owner only (403 otherwise). The old link stops working immediately.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Team id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ inviteUrl }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/teams/{id}/members/{userId}":{"delete":{"operationId":"deleteTeamsByIdMembersByUserId","tags":["Teams"],"summary":"Leave or remove a member","description":"Anyone may remove themselves; removing someone else needs the team owner or an organizer (403). Rosters lock at the submission deadline (403, organizers excepted). When the owner leaves, ownership passes to the longest-serving member; the last member leaving disbands the team.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Team id.","schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"description":"The member to remove — the caller's own id to leave.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true, disbanded: boolean }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/teams/join":{"get":{"operationId":"getTeamsJoin","tags":["Teams"],"summary":"Preview an invite link","description":"The team behind an invite token. 404 if the token is invalid or was reset.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"token","in":"query","required":true,"description":"Invite token.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ team, event, members, open, full }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postTeamsJoin","tags":["Teams"],"summary":"Join a team","description":"The invite link is the capability — no accept round-trip. Idempotent for current members. 409 if the caller is on another team in the event; 403 after the submission deadline or when the team is full.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":1,"description":"The token from the team's invite link."}},"required":["token"]}}}},"responses":{"200":{"description":"`{ teamId, eventSlug, alreadyMember }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}}},"/api/events/{event}/projects":{"get":{"operationId":"getEventsByEventProjects","tags":["Projects"],"summary":"Project gallery","description":"Submitted projects only — public, no auth needed. At most 500.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Search title, tagline and write-up.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Filter by track id.","schema":{"type":"string"}},{"name":"tag","in":"query","required":false,"description":"Filter by tech tag.","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"`newest` (default) or `title`.","schema":{"type":"string","enum":["newest","title"]}}],"responses":{"200":{"description":"`{ event: { id, slug, name }, count, projects: GalleryProject[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventProjects","tags":["Projects"],"summary":"Start or submit a project","description":"Creates the caller's team project (draft, or submitted with `submit: true`). **403 once the submission deadline has passed** — enforced by the server whatever the UI shows. 403 if the caller has no team; 409 if the team already has a project.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":100},"tagline":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}]},"trackId":{"type":["string","null"]},"content":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"thumbnailUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"videoUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"repoUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"liveUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"techTags":{"maxItems":12,"type":"array","items":{"type":"string","minLength":1,"maxLength":30}},"submit":{"type":"boolean"}},"required":["title"]}}}},"responses":{"201":{"description":"`{ id, status: \"draft\" | \"submitted\" }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}}},"/api/projects/{id}":{"get":{"operationId":"getProjectsById","tags":["Projects"],"summary":"Get a project","description":"Submitted projects of published events are public. Drafts are visible only to the team and the event's organizers — anyone else gets 404 (no existence leak).","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"id","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ project, event, track, team: { id, name, members } | null, viewer: { manager, member, owner, canEdit, submissionsOpen } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"patchProjectsById","tags":["Projects"],"summary":"Edit a project","description":"Team members until the submission deadline; the event's organizers at any time. 403 for anyone else, and 403 for members after the deadline. Only the fields sent change. `submit: true` submits, `submit: false` withdraws back to draft.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":100},"tagline":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}]},"trackId":{"type":["string","null"]},"content":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"doc"},"content":{"type":"array","items":{}}},"required":["type"]},{"type":"null"}]},"thumbnailUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"videoUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"repoUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"liveUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"techTags":{"maxItems":12,"type":"array","items":{"type":"string","minLength":1,"maxLength":30}},"submit":{"type":"boolean"}}}}}},"responses":{"200":{"description":"`{ project }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteProjectsById","tags":["Projects"],"summary":"Delete a project","description":"The team owner before the deadline, or an organizer of the event. 403 otherwise.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/projects/{id}/similar":{"get":{"operationId":"getProjectsByIdSimilar","tags":["Projects"],"summary":"Similar projects","description":"The four submissions in the same event most similar by content (TF-IDF cosine).","security":[],"x-access":"public","parameters":[{"name":"id","in":"path","required":true,"description":"A submitted project's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ similar: { id, title, tagline, techTags, thumbnailUrl, trackName, score }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}}},"/api/projects/{id}/comments":{"get":{"operationId":"getProjectsByIdComments","tags":["Comments"],"summary":"List comments","description":"The public discussion thread, oldest first.","security":[],"x-access":"public","parameters":[{"name":"id","in":"path","required":true,"description":"A submitted project's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ comments: { id, body, createdAt, authorId, authorName, authorUsername, authorImage }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postProjectsByIdComments","tags":["Comments"],"summary":"Post a comment","description":"Signed-in users only, so every comment has an accountable author. Rate limited to 10 per minute per user (429).","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"A submitted project's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000}},"required":["body"]}}}},"responses":{"201":{"description":"`{ id }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/api/comments/{id}":{"delete":{"operationId":"deleteCommentsById","tags":["Comments"],"summary":"Delete a comment","description":"The author, or an organizer of the event (moderation). 403 otherwise.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Comment id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/judges":{"get":{"operationId":"getEventsByEventJudges","tags":["Judging"],"summary":"The judging panel","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Each judge's tracks and progress (assigned vs scored), plus pending invitations with their links.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ judges: { id, name, email, username, image, trackIds, joinedAt, assigned, scored, lastScoredAt }[], invites: { id, email, trackIds, createdAt, inviteUrl }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventJudges","tags":["Judging"],"summary":"Invite a judge","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). An existing account joins the panel immediately (a participant is promoted to judge); otherwise an invitation link is created for the organizer to share.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Email address (case-insensitive)."},"trackIds":{"default":[],"description":"Empty = all tracks.","maxItems":30,"type":"array","items":{"type":"string"}}},"required":["email"]}}}},"responses":{"201":{"description":"`{ status: \"added\", userId, name }` or `{ status: \"invited\", inviteUrl }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/judges/{userId}":{"patch":{"operationId":"patchEventsByEventJudgesByUserId","tags":["Judging"],"summary":"Change a judge's tracks","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). 404 if the user is not on the panel.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"description":"The judge's user id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"trackIds":{"maxItems":30,"type":"array","items":{"type":"string"},"description":"Track ids this judge covers; empty = all tracks."}},"required":["trackIds"]}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteEventsByEventJudgesByUserId","tags":["Judging"],"summary":"Remove a judge","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Unscored assignments are released; scores already given are kept as part of the record.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"description":"The judge's user id, or a pending invitation id (`inv_…`) to revoke it.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/invites/{token}":{"get":{"operationId":"getInvitesByToken","tags":["Judging"],"summary":"Preview a judging invitation","description":"404 if the invitation is invalid or was revoked.","security":[],"x-access":"public","parameters":[{"name":"token","in":"path","required":true,"description":"The invitation token from the invite link.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ email, accepted, event: { slug, name, tagline, hue } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postInvitesByToken","tags":["Judging"],"summary":"Accept a judging invitation","description":"The signed-in account joins the panel (and gains the judge role). Bound to the invited email: 403 for any other account, and 403 if already used by someone else.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"token","in":"path","required":true,"description":"The invitation token from the invite link.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ eventSlug }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/assignments":{"get":{"operationId":"getEventsByEventAssignments","tags":["Judging"],"summary":"List assignments","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Every assignment with judge, project and whether it has been scored, plus coverage.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ reviewsPerProject, coverage: { projects, fullyCovered, underCovered }, assignments: { id, batch, judgeId, judgeName, projectId, projectTitle, trackId, scoredAt, scored }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventAssignments","tags":["Judging"],"summary":"Generate assignments","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Plans judge→project pairs honouring track eligibility and conflicts of interest (a judge never reviews their own team). Re-running tops up coverage and never duplicates. `dryRun` previews without writing.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"strategy":{"default":"balanced","description":"`balanced`: each project gets N reviews from the least-loaded eligible judges. `batch`: contiguous chunks, one review each.","type":"string","enum":["balanced","batch"]},"reviewsPerProject":{"description":"Defaults to the event's setting.","type":"integer","minimum":1,"maximum":10},"dryRun":{"default":false,"description":"Plan and report coverage without writing anything.","type":"boolean"}}}}}},"responses":{"200":{"description":"`{ strategy, reviewsPerProject, judges, pairs, inserted?, dryRun?, coverage }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteEventsByEventAssignments","tags":["Judging"],"summary":"Release unscored assignments","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Deletes every assignment that has no score yet.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ removed }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/judge/queue":{"get":{"operationId":"getJudgeQueue","tags":["Judging"],"summary":"My judging queue","description":"Without `event`: the events the caller judges, with progress. With `event`: the caller's assigned projects there (with their own score, if any) and the rubric — 403 if the caller is not on that event's panel. Scoped to the caller; there is no parameter to see anyone else's queue.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"query","required":false,"description":"Event slug or id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ events }` or `{ event, locked, criteria, total, scored, items }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/judge/scores":{"get":{"operationId":"getJudgeScores","tags":["Judging"],"summary":"My scores","description":"The caller's own scores with their weighted value. **Role isolation:** `judge` may only name the caller — asking for another judge is a 403 decided before any row is read (and audited). Callers who are neither a judge nor on any panel (e.g. participants) get 403.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"query","required":false,"description":"Limit to one event (slug or id).","schema":{"type":"string"}},{"name":"judge","in":"query","required":false,"description":"Username or id; must be the caller's own.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ judge: { id, username, name }, scores: { id, eventId, eventSlug, projectId, projectTitle, criteria, comment, updatedAt, weighted }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postJudgeScores","tags":["Judging"],"summary":"Save a score","description":"Creates or updates the caller's score. 403 unless the project is assigned to the caller, and 403 after `judgingClose` or once results are published. 400 unless there is a mark for every rubric key and no unknown keys.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","minLength":1},"criteria":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":1,"maximum":5},"description":"A 1–5 mark for every rubric key, e.g. `{ \"impact\": 4 }`."},"comment":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}]}},"required":["projectId","criteria"]}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/api/events/{event}/pairwise":{"get":{"operationId":"getEventsByEventPairwise","tags":["Pairwise"],"summary":"Next pair to compare","description":"403 unless the caller is on the event's panel. Picks, among the caller's assigned projects, the unseen pair that carries the most information (least-compared projects, closest current strengths). `pair` is null once every pair has been compared.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ event: { slug, name }, pair: [Project, Project] | null, progress: { compared, possible } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventPairwise","tags":["Pairwise"],"summary":"Record a verdict","description":"403 unless the caller is on the panel and both projects are assigned to them; 403 once results are published. 400 if `winnerId` equals `loserId`.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"winnerId":{"type":"string","minLength":1},"loserId":{"type":"string","minLength":1}},"required":["winnerId","loserId"]}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteEventsByEventPairwise","tags":["Pairwise"],"summary":"Undo last verdict","description":"403 unless the caller is on the panel; 403 once results are published. Deletes the judge's most recent pairwise comparison in this event.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true, undone: { id, winnerId, loserId } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/ballot":{"get":{"operationId":"getEventsByEventBallot","tags":["Voting"],"summary":"My ballot","description":"Submitted projects in an order shuffled per voter (stable for one voter, different between voters). Carries only the caller's own allocations and remaining credits — never anyone's totals. The caller's own team's projects are flagged `ownTeam`. In `open` access mode an anonymous voter cookie is issued.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ event, enabled, open, access, allowedDomains, voter, budget, spent, maxVotesPerProject, projects: { id, title, tagline, thumbnailUrl, trackName, techTags, ownTeam, myVotes }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/votes":{"post":{"operationId":"postEventsByEventVotes","tags":["Voting"],"summary":"Cast votes","description":"Sets how many votes the caller gives one project (0 removes them). Quadratic: `votes` costs votes² credits and the total may not exceed the event's `voteBudget` (**400** when exceeded, or above ⌊√budget⌋ per project). 403 outside the voting window; 401 when the access mode needs a sign-in or a verified email; **403 for your own team's project**; 404 for an unknown or unsubmitted project. Rate limited per IP (60/min) and per voter (30/min); at most 3 anonymous voters per network (429).","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","minLength":1},"votes":{"type":"integer","minimum":0,"maximum":50,"description":"Votes for this project (0 removes them). Costs votes² credits."}},"required":["projectId","votes"]}}}},"responses":{"200":{"description":"`{ ok: true, votes, spent, remaining }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}},"get":{"operationId":"getEventsByEventVotes","tags":["Voting"],"summary":"Live tally and abuse signals","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Never public while voting runs. Includes voters per network and the busiest minutes.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ open, budget, voters, byKind: { user, email, anon }, tally, signals: { sharedNetworks, busiestMinutes }, recent }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/voters":{"post":{"operationId":"postEventsByEventVoters","tags":["Voting"],"summary":"Email voter verification","description":"Only for events with `votingAccess: \"email\"` (400 otherwise); 403 if the address is outside the allowed domains. Send `{ email }` to receive a 6-digit code (written to the server log offline), then `{ email, code }` to verify — that sets the voter cookie for this event. Codes expire after 15 minutes and allow five attempts (403 after). Rate limited: 5 codes per IP and 3 per address per 10 minutes, 20 verifications per IP (429).","security":[],"x-access":"public","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Email address (case-insensitive)."},"code":{"description":"Omit to request a code; send the 6-digit code to verify.","type":"string","pattern":"^\\d{6}$"}},"required":["email"]}}}},"responses":{"200":{"description":"`{ sent: true }` or `{ verified: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/api/events/{event}/results":{"get":{"operationId":"getEventsByEventResults","tags":["Results"],"summary":"Leaderboard","description":"Normalized judged ranking, per-criterion means, track ranks, community votes, pairwise strength and prize winners. **Hidden until published:** everyone but the event's organizers gets `{ published: false, event }` with no numbers at all. Organizers see a `preview` before publishing, and are the only ones who get per-judge calibration (`judges`).","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ published: false, event }` or `{ published, preview, event, criteria, projects, judges?, … }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventResults","tags":["Results"],"summary":"Publish or unpublish results","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Publishing is refused (403) while community voting is still open, unless `closeVoting: true` ends the window now. Publishing locks scores and pairwise verdicts.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"published":{"type":"boolean"},"closeVoting":{"default":false,"description":"End a still-open voting window now, so results can be published.","type":"boolean"}},"required":["published"]}}}},"responses":{"200":{"description":"`{ published }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/compare":{"get":{"operationId":"getEventsByEventCompare","tags":["Results"],"summary":"Compare projects","description":"Side by side: the projects, a content-similarity matrix and (when scores are visible) head-to-head pairwise records. With one id the server picks rivals — the most similar submissions and, once results are visible, the neighbours in the ranking. Scores appear only when results are published or the caller organizes the event. 400 without ids.","security":[{},{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"optional","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"ids","in":"query","required":true,"description":"Comma-separated project ids (1–4).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ auto, showScores, criteria, projects: (Project & { result })[], similarity: number[][], headToHead }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/overview":{"get":{"operationId":"getEventsByEventOverview","tags":["Organizer data"],"summary":"Organizer dashboard","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Headline counts, submissions per day, projects per track, per-judge progress (least progress first) and the latest audit entries.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ event, phase, totals, submissionsPerDay, projectsPerTrack, judgeProgress, activity }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/audit":{"get":{"operationId":"getEventsByEventAudit","tags":["Organizer data"],"summary":"Audit trail","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Newest first. Page backwards with `before` = the previous response's `nextBefore`.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"action","in":"query","required":false,"description":"Filter by action prefix, e.g. `score` or `vote.`.","schema":{"type":"string"}},{"name":"before","in":"query","required":false,"description":"ISO timestamp; only entries older than this.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200 (default 50).","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"`{ entries: AuditEntry[], nextBefore: string | null }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/export":{"get":{"operationId":"getEventsByEventExport","tags":["Organizer data"],"summary":"Export a dataset","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Spreadsheet-safe CSV by default (formula-looking cells are neutralized), JSON records with `format=json`. Sent as an attachment; every download is audited. 400 for an unknown dataset.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"dataset","in":"query","required":false,"description":"Which dataset (default `results`).","schema":{"type":"string","enum":["participants","teams","projects","assignments","scores","results","votes","comments","audit"],"default":"results"}},{"name":"format","in":"query","required":false,"description":"`csv` (default) or `json`.","schema":{"type":"string","enum":["csv","json"],"default":"csv"}}],"responses":{"200":{"description":"`text/csv` or a JSON array of records.","content":{"text/csv":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/bundle":{"get":{"operationId":"getEventsByEventBundle","tags":["Organizer data"],"summary":"Download the event bundle","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). The whole event as one portable, fixtures-compatible JSON file for backup or migration. Re-import with `POST /api/events/import`.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The bundle (same shape as the import body), as an attachment.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/similarity":{"get":{"operationId":"getEventsByEventSimilarity","tags":["Organizer data"],"summary":"Duplicate detection","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Every pair of submissions at or above `threshold`, with reasons (same repository, identical title, overlapping write-up) and the distinctive terms they share.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}},{"name":"threshold","in":"query","required":false,"description":"0.1–1 (default 0.5).","schema":{"type":"number","minimum":0.1,"maximum":1,"default":0.5}}],"responses":{"200":{"description":"`{ threshold, scanned, pairs: { a, b, score, reasons, sharedTerms }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/certificates":{"get":{"operationId":"getEventsByEventCertificates","tags":["Certificates"],"summary":"List issued certificates","description":"**Organizers only:** 403 unless the caller created this event (or is an admin).","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ certificates: Certificate[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventCertificates","tags":["Certificates"],"summary":"Issue certificates","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). `judges`: one per judge with at least one review, stating how many they completed. `winners`: one per team member per prize — 403 until results are published. Re-issuing skips people who already hold the same certificate.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["judges","winners"]}},"required":["kind"]}}}},"responses":{"201":{"description":"`{ issued, serials }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/certificates/{serial}":{"get":{"operationId":"getCertificatesBySerial","tags":["Certificates"],"summary":"Verify a certificate","description":"Public verification. Recomputes the HMAC-SHA256 over the stored record; `valid: false` means it was altered after issue.","security":[],"x-access":"public","parameters":[{"name":"serial","in":"path","required":true,"description":"Certificate serial (case-insensitive).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ serial, kind, subjectName, subjectUsername, statement, reviewsCompleted, event: { name, slug }, issuedAt, signature, algorithm, valid }`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}}},"/api/events/{event}/webhooks":{"get":{"operationId":"getEventsByEventWebhooks","tags":["Webhooks"],"summary":"List subscriptions","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). Secrets are redacted to a hint. Each subscription carries its 20 most recent deliveries.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ types: string[], webhooks: (Webhook & { secretHint, deliveries })[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postEventsByEventWebhooks","tags":["Webhooks"],"summary":"Subscribe","description":"**Organizers only:** 403 unless the caller created this event (or is an admin). The signing secret (`whsec_…`) is returned once, in this response only.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"event","in":"path","required":true,"description":"Event slug or id (e.g. `sample-hack-2026`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"http(s) endpoint that receives POSTs."},"events":{"default":[],"description":"Event types to receive; empty = all.","type":"array","items":{"type":"string","enum":["event.created","event.updated","team.created","team.joined","project.submitted","project.updated","judge.invited","judge.joined","assignments.generated","score.saved","vote.cast","comment.posted","results.published","certificate.issued"]}}},"required":["url"]}}}},"responses":{"201":{"description":"`{ id, secret }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/webhooks/{id}":{"patch":{"operationId":"patchWebhooksById","tags":["Webhooks"],"summary":"Update a subscription","description":"Organizers of the webhook's event (platform-wide hooks: admins). 403 otherwise.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Webhook id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"active":{"type":"boolean"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["event.created","event.updated","team.created","team.joined","project.submitted","project.updated","judge.invited","judge.joined","assignments.generated","score.saved","vote.cast","comment.posted","results.published","certificate.issued"]}}}}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"deleteWebhooksById","tags":["Webhooks"],"summary":"Delete a subscription","description":"Organizers of the webhook's event (platform-wide hooks: admins). 403 otherwise.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Webhook id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"postWebhooksById","tags":["Webhooks"],"summary":"Send a test ping","description":"Delivers a signed `ping` now and reports the receiver's answer. Same permissions as updating.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"Webhook id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ status, ok, error }` — the receiver's HTTP status, or the network error.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/me":{"get":{"operationId":"getMe","tags":["Me"],"summary":"My account","description":"The caller's account and profile.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","responses":{"200":{"description":"`{ user: { id, name, email, role, username, image, headline, bio, location, websiteUrl, githubUrl, skills, lookingForTeam, createdAt } }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"}}},"patch":{"operationId":"patchMe","tags":["Me"],"summary":"Edit my profile","description":"Only the fields sent change. 409 if the username is taken.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"username":{"description":"3–32 characters: letters, numbers, dashes, underscores.","type":"string","pattern":"^[a-z0-9](?:[a-z0-9-_]{1,30}[a-z0-9])$"},"headline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"bio":{"anyOf":[{"type":"string","maxLength":50000},{"type":"null"}]},"location":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]},"websiteUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"githubUrl":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"image":{"description":"Avatar URL.","anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"skills":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":30}},"lookingForTeam":{"type":"boolean"}}}}}},"responses":{"200":{"description":"`{ user }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"409":{"$ref":"#/components/responses/E409"}}}},"/api/me/overview":{"get":{"operationId":"getMeOverview","tags":["Me"],"summary":"My dashboard","description":"Events joined (with team), projects, judging queues with progress, events organized and certificates held.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","responses":{"200":{"description":"The dashboard overview object.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"}}}},"/api/me/tokens":{"get":{"operationId":"getMeTokens","tags":["Me"],"summary":"List API tokens","description":"The caller's personal API tokens — never the secret itself.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","responses":{"200":{"description":"`{ tokens: { id, label, prefix, createdAt, lastUsedAt }[] }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"}}},"post":{"operationId":"postMeTokens","tags":["Me"],"summary":"Create an API token","description":"Mints a `jz_…` token. It is shown once; only its SHA-256 hash is stored. Use it as `Authorization: Bearer <token>`.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":60}},"required":["label"]}}}},"responses":{"201":{"description":"`{ id, token }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"}}},"delete":{"operationId":"deleteMeTokens","tags":["Me"],"summary":"Revoke an API token","description":"404 unless the token belongs to the caller.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"query","required":true,"description":"Token id.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/users/{username}":{"get":{"operationId":"getUsersByUsername","tags":["Users"],"summary":"Public profile","description":"Bio, skills, links, submitted projects, judging panels and certificates. No email address is exposed.","security":[],"x-access":"public","parameters":[{"name":"username","in":"path","required":true,"description":"The person's username.","schema":{"type":"string"}}],"responses":{"200":{"description":"The public profile object.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/E404"}}}},"/api/admin/users":{"get":{"operationId":"getAdminUsers","tags":["Admin"],"summary":"List accounts","description":"Requires the `admin` role (403 otherwise). 50 per page, newest first.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"q","in":"query","required":false,"description":"Search name, email and username.","schema":{"type":"string"}},{"name":"role","in":"query","required":false,"description":"Filter by role.","schema":{"type":"string","enum":["participant","judge","organizer","admin"]}},{"name":"page","in":"query","required":false,"description":"Zero-based page.","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"`{ users, total, page, pageSize }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/api/admin/users/{id}":{"patch":{"operationId":"patchAdminUsersById","tags":["Admin"],"summary":"Change role or suspend","description":"Requires the `admin` role. Suspending signs the user out everywhere. 400 when targeting yourself, so an instance always keeps an admin.","security":[{"bearerAuth":[]},{"cookieAuth":[]}],"x-access":"user","parameters":[{"name":"id","in":"path","required":true,"description":"User id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["participant","judge","organizer","admin"]},"banned":{"description":"true suspends the account and signs it out everywhere.","type":"boolean"}}}}}},"responses":{"200":{"description":"`{ ok: true }`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/api/health":{"get":{"operationId":"getHealth","tags":["System"],"summary":"Health check","description":"Liveness and database readiness, for container healthchecks and uptime monitors.","security":[],"x-access":"public","responses":{"200":{"description":"`{ ok: true, time }`.","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"$ref":"#/components/responses/E503"}}}},"/api/openapi.json":{"get":{"operationId":"getOpenapiJson","tags":["System"],"summary":"This document","description":"The OpenAPI 3.1 description of the API, served with `Access-Control-Allow-Origin: *` so any tool can load it.","security":[],"x-access":"public","responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"webhooks":{"ping":{"post":{"summary":"Sent by `POST /api/webhooks/{id}`.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"ping"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"ping"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"message":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"event.created":{"post":{"summary":"An event was created.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"event.created"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"event.created"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"name":{"type":"string"},"slug":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"event.updated":{"post":{"summary":"Event settings changed.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"event.updated"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"event.updated"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"team.created":{"post":{"summary":"A team was formed.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"team.created"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"team.created"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"teamId":{"type":"string"},"name":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"team.joined":{"post":{"summary":"Someone joined a team.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"team.joined"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"team.joined"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"teamId":{"type":"string"},"userId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"project.submitted":{"post":{"summary":"A project was submitted.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"project.submitted"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"project.submitted"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"projectId":{"type":"string"},"title":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"project.updated":{"post":{"summary":"A project was edited.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"project.updated"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"project.updated"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"projectId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"judge.invited":{"post":{"summary":"A judging invitation link was created.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"judge.invited"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"judge.invited"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"email":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"judge.joined":{"post":{"summary":"A judge joined the panel.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"judge.joined"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"judge.joined"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"userId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"assignments.generated":{"post":{"summary":"Judge assignments were generated.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"assignments.generated"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"assignments.generated"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"strategy":{"type":"string"},"reviewsPerProject":{"type":"integer"},"judges":{"type":"integer"},"pairs":{"type":"integer"},"inserted":{"type":"integer"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"score.saved":{"post":{"summary":"A judge saved a score.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"score.saved"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"score.saved"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"projectId":{"type":"string"},"judgeId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"vote.cast":{"post":{"summary":"A community vote changed.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"vote.cast"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"vote.cast"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"projectId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"comment.posted":{"post":{"summary":"A comment was posted.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"comment.posted"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"comment.posted"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"projectId":{"type":"string"},"commentId":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"results.published":{"post":{"summary":"Results were published.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"results.published"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"results.published"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"name":{"type":"string"},"slug":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}},"certificate.issued":{"post":{"summary":"Certificates were issued.","description":"Verify `X-Juryza-Signature` before trusting the body. Answer with any 2xx within 5 seconds.","parameters":[{"name":"X-Juryza-Event","in":"header","required":true,"schema":{"const":"certificate.issued"}},{"name":"X-Juryza-Signature","in":"header","required":true,"description":"`sha256=<hex>` — HMAC-SHA256 of the raw body keyed with the subscription secret.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","sentAt","data"],"properties":{"type":{"const":"certificate.issued"},"sentAt":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"eventId":{"type":["string","null"]},"kind":{"type":"string"},"serials":{"type":"array","items":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"jz_…","description":"Personal API token from /settings/tokens: `Authorization: Bearer jz_…`."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Better Auth session cookie set by `POST /api/auth/sign-in/email`."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable message."},"issues":{"type":"array","description":"Validation issues (400 only).","items":{"type":"object","properties":{"path":{"type":"array","items":{}},"message":{"type":"string"},"code":{"type":"string"}}}}}},"RateLimitError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"retryAfterSeconds":{"type":"integer","description":"Seconds until the window resets."}}}},"responses":{"E400":{"description":"Invalid request — the body failed validation or broke a rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E401":{"description":"No valid credential: sign in or send a bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E403":{"description":"Authenticated, but not allowed to do this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E404":{"description":"Not found (or not visible to the caller).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E409":{"description":"Conflicts with existing state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E429":{"description":"Rate limited. Retry after `retryAfterSeconds`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"E503":{"description":"A dependency (the database) is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}