JSON

Authentication

All endpoints require an API key passed via the x-api-key request header or ?api_key= query parameter.

Keys are scoped — each key grants access only to the feature groups listed on it. Keys are issued by the server owner and shown once at creation.
adminlog

Admin Logging

Game server command and spectate logging

POST /adminlog/session/start

Register a new server session when the Roblox server boots. Must be called before any other /adminlog endpoints.

Request Body
sessionId string — unique per server boot
serverType string — e.g. "Training", "Tryout"
POST /adminlog/session/match-start

Mark when the event formally begins. Captures the overseer team at this instant.

Request Body
sessionId string
overseerTeam optional string[] — usernames present at match start
timestamp optional number — Unix seconds or ms, defaults to now
POST /adminlog/session/match-end

Record when the match ends. Does not post the log — that happens on server close via /session/discard.

Request Body
sessionId string
timestamp optional number — Unix seconds or ms, defaults to now
POST /adminlog/session/discard

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.

Request Body
sessionId string
POST /adminlog/overseer/add

Add a player to the overseer team at any point during the session. Idempotent — safe to call multiple times for the same username.

Request Body
sessionId string
username string
POST /adminlog/command

Record an admin command execution. Call this every time a player runs an Adonis command during the session.

Request 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 optional string — target username; omit or set to "Self" for self-targeting commands
timestamp optional number — Unix seconds or ms, defaults to now
duringMatch boolean — true if /session/match-start has already been called
POST /adminlog/spectate/start

Record when an admin opens the spectate or CXW UI on a target.

Request 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 optional number — Unix seconds or ms, defaults to now
POST /adminlog/spectate/end

Record when the spectate or CXW UI is closed.

Request Body
sessionId string
spectateId string — same ID sent to /spectate/start
timestamp optional number — Unix seconds or ms, defaults to now
api:recording-policy

Recording Policy

Notify flagged attendees when a new event is logged

POST /api/recording-policy/event

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.

Request Body
eventId string — unique event identifier
host string — host Roblox username
date string — YYYY-MM-DD
eventType string
screenshot optional string — screenshot URL
attendance optional number — total attendee count
attendees string — comma-separated Roblox usernames
Response
{ ok: true, notified: number, flaggedAttendees: string[], skipped: number }
api:mainframe

Mainframe

GAS proxy endpoints for mainframe sheet operations

GET /api/mainframe/data

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 }
GET /api/mainframe/events

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, ... }
GET /api/mainframe/objectives

Returns current objectives data from the configured Apps Script deployment. Cached for 5 minutes.

GET /api/mainframe/query

Run a specific named read query against the mainframe. Use the fn parameter to select which query to run.

Query Parameters
fn "getGroupMembers" | "getEventById" | "getExemptionDays" | "getDeploymentEvents"
payload optional URL-encoded JSON object of arguments for the selected function (see below)
bust optional "true" to bypass the cache for this request
GET /api/mainframe/query?fn=getGroupMembers

Returns a flat list of Roblox usernames from the GroupMembers sheet tab. Cached for 1 hour.

Response
string[]
GET /api/mainframe/query?fn=getEventById

Look up a single event log entry by its Event ID (column P). Cached for 1 minute.

Query Parameters
payload (JSON) { eventId: string }
Response
{ found: boolean, hostUsername, date, eventType, attendees, screenshot }
GET /api/mainframe/query?fn=getExemptionDays

Look up how many exemption days a member has used and how many remain.

Query Parameters
payload (JSON) { username: string }
Response
{ found: boolean, daysUsed: number, daysRemaining: number }
GET /api/mainframe/query?fn=getDeploymentEvents

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 }] }
POST /api/mainframe/eventlog

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.

Request 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 optional string — comma-separated Roblox usernames
notes optional string
discordId optional string — submitter's Discord ID; if provided, a confirmation DM is sent
Response
{ success: true, eventId: string }
POST /api/mainframe/strikes

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.

Request Body
quotaWeek optional 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 }
POST /api/mainframe/submit

Run a named write operation against the mainframe. Use the fn field to select which action to perform.

Request Body
fn "submitEditEventLog" | "submitStatsTransfer" | "submitMissingAP" | "addOfficer" | "removeOfficer" | "processCallback"
payload object — arguments for the selected function (see below)
discordId optional string — if provided, a DM confirmation is sent to this Discord user ID
POST /api/mainframe/submit (fn: submitEditEventLog)

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.

Request Body
payload.eventId string — required; the Event ID (column P) of the row to edit
payload.host optional string
payload.date optional string — YYYY-MM-DD
payload.eventType optional string
payload.screenshot optional string
payload.attendees optional string — full replacement comma-separated list
payload.notes optional string
Response
{ success: true }
POST /api/mainframe/submit (fn: submitStatsTransfer)

Submit a stats transfer request. Logs to the transfer sheet as Pending, sends a Discord notification with Approve/Deny links.

Request 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 }
POST /api/mainframe/submit (fn: submitMissingAP)

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.

Request 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 }
POST /api/mainframe/submit (fn: addOfficer)

Add a member to the Officer Tracker. Inserts at the correct rank-grouped position. Returns 409 if the username is already on the tracker.

Request Body
payload.username string
payload.rank optional string — rank label for position grouping
Response
{ success: true }
POST /api/mainframe/submit (fn: removeOfficer)

Remove a member from the Officer Tracker by username (column A). Returns 404 if not found.

Request Body
payload.username string
Response
{ success: true }
api:applications

Applications

Application review system

POST /api/applications/notify

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.

Request Body
applicationType string — e.g. "department", "iwo_app"
applicant optional { discordId?: string, robloxId?: string, robloxUsername?: string }
fields [{ question: string, answer: string }] — dynamic Q&A to display on the card
sourceUrl optional string — link back to the raw submission row
callback optional { spreadsheetId, tab, keyColumn, keyValue, statusColumn, reviewedByColumn, reasonColumn, decidedAtColumn } — sheet cells to write when the application is decided
Response
{ success: true, flowId: string }
api:ia

Internal Affairs

IA Notion integration

POST /api/ia/form-submission

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.

Request Body
formType "misconduct" | "appeal" | "clemency" | "history_request"
submitterRobloxUsername optional string
submitterDiscordId optional string
Timestamp optional 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

Intake

Create intakes (background checks, officer applications, bypasses) for users

POST /api/intake/create

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.

Request 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 optional 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
POST /api/intake/create-direct

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.

Request 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 optional 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

Verification Check

Read-only lookup of a user's Roblox verification and background check status

GET /api/verify/check

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.

Query Parameters
guildId string — required
discordId optional string — Discord user ID; provide this OR robloxId
robloxId optional string — Roblox user ID; provide this OR discordId
Response
Verified: { verified: true, discordId, robloxId, robloxUsername, backgroundCheckPassed, backgroundCheckStatus }
Not verified: { verified: false }