{"scopes":{"adminlog":{"label":"Admin Logging","description":"Game server command and spectate logging","routes":["POST /adminlog/session/start","POST /adminlog/session/match-start","POST /adminlog/session/match-end","POST /adminlog/session/discard","POST /adminlog/command","POST /adminlog/spectate/start","POST /adminlog/spectate/end"]},"api:recording-policy":{"label":"Recording Policy","description":"Notify flagged attendees when a new event is logged","routes":["POST /api/recording-policy/event"]},"api:mainframe":{"label":"Mainframe","description":"GAS proxy endpoints for mainframe sheet operations","routes":["GET /api/mainframe/*","POST /api/mainframe/*"]},"api:applications":{"label":"Applications","description":"Application review system","routes":["* /api/applications/*"]},"api:ia":{"label":"Internal Affairs","description":"IA Notion integration","routes":["* /api/ia/*"]},"api:intake":{"label":"Intake","description":"Create intakes (background checks, officer applications, bypasses) for users","routes":["POST /api/intake/create","POST /api/intake/create-direct"]},"api:verify-check":{"label":"Verification Check","description":"Read-only lookup of a user's Roblox verification and background check status","routes":["GET /api/verify/check"]}},"endpoints":{"adminlog":[{"method":"POST","path":"/adminlog/session/start","description":"Register a new server session when the Roblox server boots. Must be called before any other /adminlog endpoints.","body":{"sessionId":"string — unique per server boot","serverType":"string — e.g. \"Training\", \"Tryout\""}},{"method":"POST","path":"/adminlog/session/match-start","description":"Mark when the event formally begins. Captures the overseer team at this instant.","body":{"sessionId":"string","overseerTeam?":"string[] — usernames present at match start","timestamp?":"number — Unix seconds or ms, defaults to now"}},{"method":"POST","path":"/adminlog/session/match-end","description":"Record when the match ends. Does not post the log — that happens on server close via /session/discard.","body":{"sessionId":"string","timestamp?":"number — Unix seconds or ms, defaults to now"}},{"method":"POST","path":"/adminlog/session/discard","description":"Called on server close (game:BindToClose). If a match was started, posts the full event log to Discord then destroys the session. If no match was started, silently discards.","body":{"sessionId":"string"}},{"method":"POST","path":"/adminlog/overseer/add","description":"Add a player to the overseer team at any point during the session. Idempotent — safe to call multiple times for the same username.","body":{"sessionId":"string","username":"string"}},{"method":"POST","path":"/adminlog/command","description":"Record an admin command execution. Call this every time a player runs an Adonis command during the session.","body":{"sessionId":"string","executor":"string — Roblox username of the admin","executorTeam":"string — team name at time of command","adminLevel":"number — 0=Everyone, 1=VIP, 2=Mod, 3=Admin, 4=SuperAdmin, >100=Creator","commandName":"string — name of the Adonis command","target?":"string — target username; omit or set to \"Self\" for self-targeting commands","timestamp?":"number — Unix seconds or ms, defaults to now","duringMatch":"boolean — true if /session/match-start has already been called"}},{"method":"POST","path":"/adminlog/spectate/start","description":"Record when an admin opens the spectate or CXW UI on a target.","body":{"sessionId":"string","spectateId":"string — client-generated unique ID; used to pair with /spectate/end","spectator":"string — Roblox username of the admin spectating","target":"string — Roblox username being spectated","timestamp?":"number — Unix seconds or ms, defaults to now"}},{"method":"POST","path":"/adminlog/spectate/end","description":"Record when the spectate or CXW UI is closed.","body":{"sessionId":"string","spectateId":"string — same ID sent to /spectate/start","timestamp?":"number — Unix seconds or ms, defaults to now"}}],"api:recording-policy":[{"method":"POST","path":"/api/recording-policy/event","description":"Check an event's attendee list against the recording policy flaglist and send Discord DM notifications to any flagged attendees. Called by Google Apps Script when a new event row is logged.","body":{"eventId":"string — unique event identifier","host":"string — host Roblox username","date":"string — YYYY-MM-DD","eventType":"string","screenshot?":"string — screenshot URL","attendance?":"number — total attendee count","attendees":"string — comma-separated Roblox usernames"},"response":"{ ok: true, notified: number, flaggedAttendees: string[], skipped: number }"}],"api:mainframe":[{"method":"GET","path":"/api/mainframe/data","description":"Returns all mainframe sheet data in one call: weekly and monthly events, activity tracker, officer tracker, honored tracker, departments, and settings. Cached for 10 minutes with stale-while-revalidate. Add ?bust=true to force an immediate refresh.","response":"{ weeklyEvents, monthlyEvents, activity: { members[] }, officers: { officers[] }, honored: { members[] }, departments[], settings }"},{"method":"GET","path":"/api/mainframe/events","description":"Returns all rows from the Event Log sheet as structured objects. Cached for 5 minutes.","response":"Array of event objects: { eventId, date, host, eventType, screenshot, attendeeCount, attendeeList, apPoints, opPoints, ... }"},{"method":"GET","path":"/api/mainframe/objectives","description":"Returns current objectives data from the configured Apps Script deployment. Cached for 5 minutes."},{"method":"GET","path":"/api/mainframe/query","description":"Run a specific named read query against the mainframe. Use the fn parameter to select which query to run.","params":{"fn":"\"getGroupMembers\" | \"getEventById\" | \"getExemptionDays\" | \"getDeploymentEvents\"","payload?":"URL-encoded JSON object of arguments for the selected function (see below)","bust?":"\"true\" to bypass the cache for this request"}},{"method":"GET","path":"/api/mainframe/query?fn=getGroupMembers","description":"Returns a flat list of Roblox usernames from the GroupMembers sheet tab. Cached for 1 hour.","response":"string[]"},{"method":"GET","path":"/api/mainframe/query?fn=getEventById","description":"Look up a single event log entry by its Event ID (column P). Cached for 1 minute.","params":{"payload (JSON)":"{ eventId: string }"},"response":"{ found: boolean, hostUsername, date, eventType, attendees, screenshot }"},{"method":"GET","path":"/api/mainframe/query?fn=getExemptionDays","description":"Look up how many exemption days a member has used and how many remain.","params":{"payload (JSON)":"{ username: string }"},"response":"{ found: boolean, daysUsed: number, daysRemaining: number }"},{"method":"GET","path":"/api/mainframe/query?fn=getDeploymentEvents","description":"Returns per-attendee rows from the Monthly Events tab, used for DIS sync. Each row includes the Roblox gameId the DIS tile is keyed to.","response":"{ events: [{ date, username, eventType, gameId }] }"},{"method":"POST","path":"/api/mainframe/eventlog","description":"Submit a new event log entry. Writes via the Google Form (same path as the /eventlog Discord command) to ensure formula columns (AP, OP, multiplier) are populated correctly. Responds immediately with a locally-generated eventId; sheet confirmation and DM to the submitter happen in the background.","body":{"host":"string — host Roblox username","date":"string — YYYY-MM-DD","eventType":"string — must match an event type defined in the Settings tab","screenshot":"string — direct URL to event screenshot","attendees?":"string — comma-separated Roblox usernames","notes?":"string","discordId?":"string — submitter's Discord ID; if provided, a confirmation DM is sent"},"response":"{ success: true, eventId: string }"},{"method":"POST","path":"/api/mainframe/strikes","description":"Notify officers of strike changes after a quota reset. DMs each affected officer and posts a summary to the configured strike log channel. Called by the mainframe Apps Script after quota resets.","body":{"quotaWeek?":"string — week label e.g. \"Week 12\"","strikes":"[{ username: string, amount: number, newTotal: number }] — positive amount = added, negative = removed"},"response":"{ ok: true, notified: number, failed: number }"},{"method":"POST","path":"/api/mainframe/submit","description":"Run a named write operation against the mainframe. Use the fn field to select which action to perform.","body":{"fn":"\"submitEditEventLog\" | \"submitStatsTransfer\" | \"submitMissingAP\" | \"addOfficer\" | \"removeOfficer\" | \"processCallback\"","payload":"object — arguments for the selected function (see below)","discordId?":"string — if provided, a DM confirmation is sent to this Discord user ID"}},{"method":"POST","path":"/api/mainframe/submit (fn: submitEditEventLog)","description":"Edit an existing event log row by its Event ID. Only fields provided in the payload are updated; omitted fields are left unchanged. Busts the event log cache.","body":{"payload.eventId":"string — required; the Event ID (column P) of the row to edit","payload.host?":"string","payload.date?":"string — YYYY-MM-DD","payload.eventType?":"string","payload.screenshot?":"string","payload.attendees?":"string — full replacement comma-separated list","payload.notes?":"string"},"response":"{ success: true }"},{"method":"POST","path":"/api/mainframe/submit (fn: submitStatsTransfer)","description":"Submit a stats transfer request. Logs to the transfer sheet as Pending, sends a Discord notification with Approve/Deny links.","body":{"payload.oldUsername":"string — current Roblox username","payload.newUsername":"string — new Roblox username","payload.transferType":"string — transfer type label","payload.evidence":"string — URL to evidence screenshot"},"response":"{ success: true, requestId: string }"},{"method":"POST","path":"/api/mainframe/submit (fn: submitMissingAP)","description":"Submit a missing AP request. Logs to the Missing AP Log sheet as Pending and sends a Discord notification with Approve/Deny links. Rate limited to 1 request per 5 minutes per username. Evidence must be a direct http(s) screenshot URL — Discord links are rejected.","body":{"payload.username":"string — Roblox username of the member requesting AP","payload.hostUsername":"string — Roblox username of the event host","payload.date":"string — YYYY-MM-DD","payload.eventType":"string","payload.evidence":"string — direct screenshot URL (no Discord links)"},"response":"{ success: true, requestId: string }"},{"method":"POST","path":"/api/mainframe/submit (fn: addOfficer)","description":"Add a member to the Officer Tracker. Inserts at the correct rank-grouped position. Returns 409 if the username is already on the tracker.","body":{"payload.username":"string","payload.rank?":"string — rank label for position grouping"},"response":"{ success: true }"},{"method":"POST","path":"/api/mainframe/submit (fn: removeOfficer)","description":"Remove a member from the Officer Tracker by username (column A). Returns 404 if not found.","body":{"payload.username":"string"},"response":"{ success: true }"}],"api:applications":[{"method":"POST","path":"/api/applications/notify","description":"Post an application review card to the configured Discord channel. Called by Google Apps Script when a form submission is categorised. The card includes an Approve/Deny interface for staff. The applicationType must match a key defined in lib/applicationTypes.js.","body":{"applicationType":"string — e.g. \"department\", \"iwo_app\"","applicant?":"{ discordId?: string, robloxId?: string, robloxUsername?: string }","fields":"[{ question: string, answer: string }] — dynamic Q&A to display on the card","sourceUrl?":"string — link back to the raw submission row","callback?":"{ spreadsheetId, tab, keyColumn, keyValue, statusColumn, reviewedByColumn, reasonColumn, decidedAtColumn } — sheet cells to write when the application is decided"},"response":"{ success: true, flowId: string }"}],"api:ia":[{"method":"POST","path":"/api/ia/form-submission","description":"Create a Notion card for an IA form submission and post an interactive notification to the configured ia_channel. Supports misconduct reports, punishment appeals, and clemency requests. The Discord notification includes Create Case / Dismiss buttons for IA staff. History requests create a Notion card only (no Discord buttons). Requires NOTION_IA_DATABASE_ID to be configured.","body":{"formType":"\"misconduct\" | \"appeal\" | \"clemency\" | \"history_request\"","submitterRobloxUsername?":"string","submitterDiscordId?":"string","Timestamp?":"string — ISO 8601; defaults to now","...fields":"All other keys are mapped to Notion paragraph blocks. Standard fields vary by formType — see the Google Form for the exact question text used as keys."},"response":"{ success: true, cardId: string, url: string, caseId: number }"}],"api:intake":[{"method":"POST","path":"/api/intake/create","description":"Create intakes for one or more users — the same pipeline as the /intake slash command (RoWifi verification, grace-period + active-intake checks, group-membership routing to officer eval / ghost entrance completion / background check). aep and ot are not supported here — they have their own dedicated flows.","body":{"guildId":"string — the /intake command reads this from the interaction; the API has no interaction so it must be passed explicitly","type":"\"entrance_bypass\" | \"ghost\" | \"department\" | \"iwo\" | \"wo\" | \"nco_iwo\" | \"ghost_trial\" | \"ghost_cadet\"","userIds":"string[] — Discord user IDs (not mentions), deduped internally","requestedBy?":"string — Discord ID credited as the submitter in DMs/pings"},"response":"{ success: true, results: [{ userId, status, detail, flowId?, robloxUsername?, tag? }] } — status is one of: processed, skipped, error, not_verified, already_active, grace_period, already_in_division"},{"method":"POST","path":"/api/intake/create-direct","description":"CreateIntake — single-user variant for callers that already know BOTH the Roblox ID and Discord ID (no RoWifi lookup happens). Restricted to the three bypass types. Still runs the same active-intake, grace-period, and group-membership checks as the bulk endpoint before submitting the background check.","body":{"guildId":"string — required","type":"\"Entrance Bypass\" (entrance_bypass) | \"Ghost Bypass\" (ghost) | \"Department Bypass\" (department)","robloxUserId":"string — required","discordUserId":"string — required; Discord user ID (not a mention)","requestedBy?":"string — Discord ID credited as the submitter in DMs/pings"},"response":"{ success: true, status, detail, flowId?, robloxUsername? } — status is one of: processed, skipped, error, already_active, grace_period, already_in_division"}],"api:verify-check":[{"method":"GET","path":"/api/verify/check","description":"CheckVerify — looks up whether a user has a verified (primary) linked Roblox account and, if so, whether they passed the lightweight background check. Read-only; matches BOTH the caller's discordId and robloxId params against the primary linked account only — pass exactly one of discordId or robloxId.","params":{"guildId":"string — required","discordId?":"string — Discord user ID; provide this OR robloxId","robloxId?":"string — Roblox user ID; provide this OR discordId"},"response":"Verified: { verified: true, discordId, robloxId, robloxUsername, backgroundCheckPassed, backgroundCheckStatus }\nNot verified: { verified: false }"}]}}