Authentication
All endpoints require an API key passed via the x-api-key request header or ?api_key= query parameter.
Admin Logging
Game server command and spectate logging
/adminlog/session/start
Register a new server session when the Roblox server boots. Must be called before any other /adminlog endpoints.
sessionId |
string — unique per server boot |
serverType |
string — e.g. "Training", "Tryout" |
/adminlog/session/match-start
Mark when the event formally begins. Captures the overseer team at this instant.
sessionId |
string |
overseerTeam optional |
string[] — usernames present at match start |
timestamp optional |
number — Unix seconds or ms, defaults to now |
/adminlog/session/match-end
Record when the match ends. Does not post the log — that happens on server close via /session/discard.
sessionId |
string |
timestamp optional |
number — Unix seconds or ms, defaults to now |
/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.
sessionId |
string |
/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.
sessionId |
string |
username |
string |
/adminlog/command
Record an admin command execution. Call this every time a player runs an Adonis command during the session.
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 |
/adminlog/spectate/start
Record when an admin opens the spectate or CXW UI on a target.
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 |
/adminlog/spectate/end
Record when the spectate or CXW UI is closed.
sessionId |
string |
spectateId |
string — same ID sent to /spectate/start |
timestamp optional |
number — Unix seconds or ms, defaults to now |
Recording Policy
Notify flagged attendees when a new event is logged
/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.
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 |
{ ok: true, notified: number, flaggedAttendees: string[], skipped: number }
Mainframe
GAS proxy endpoints for mainframe sheet operations
/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.
{ weeklyEvents, monthlyEvents, activity: { members[] }, officers: { officers[] }, honored: { members[] }, departments[], settings }
/api/mainframe/events
Returns all rows from the Event Log sheet as structured objects. Cached for 5 minutes.
Array of event objects: { eventId, date, host, eventType, screenshot, attendeeCount, attendeeList, apPoints, opPoints, ... }
/api/mainframe/objectives
Returns current objectives data from the configured Apps Script deployment. Cached for 5 minutes.
/api/mainframe/query
Run a specific named read query against the mainframe. Use the fn parameter to select which query to run.
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 |
/api/mainframe/query?fn=getGroupMembers
Returns a flat list of Roblox usernames from the GroupMembers sheet tab. Cached for 1 hour.
string[]
/api/mainframe/query?fn=getEventById
Look up a single event log entry by its Event ID (column P). Cached for 1 minute.
payload (JSON) |
{ eventId: string } |
{ found: boolean, hostUsername, date, eventType, attendees, screenshot }
/api/mainframe/query?fn=getExemptionDays
Look up how many exemption days a member has used and how many remain.
payload (JSON) |
{ username: string } |
{ found: boolean, daysUsed: number, daysRemaining: number }
/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.
{ events: [{ date, username, eventType, gameId }] }
/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.
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 |
{ success: true, eventId: string }
/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.
quotaWeek optional |
string — week label e.g. "Week 12" |
strikes |
[{ username: string, amount: number, newTotal: number }] — positive amount = added, negative = removed |
{ ok: true, notified: number, failed: number }
/api/mainframe/submit
Run a named write operation against the mainframe. Use the fn field to select which action to perform.
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 |
/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.
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 |
{ success: true }
/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.
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 |
{ success: true, requestId: string }
/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.
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) |
{ success: true, requestId: string }
/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.
payload.username |
string |
payload.rank optional |
string — rank label for position grouping |
{ success: true }
/api/mainframe/submit (fn: removeOfficer)
Remove a member from the Officer Tracker by username (column A). Returns 404 if not found.
payload.username |
string |
{ success: true }
Applications
Application review system
/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.
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 |
{ success: true, flowId: string }
Internal Affairs
IA Notion integration
/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.
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. |
{ success: true, cardId: string, url: string, caseId: number }
Intake
Create intakes (background checks, officer applications, bypasses) for users
/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.
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 |
{ 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
/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.
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 |
{ success: true, status, detail, flowId?, robloxUsername? } — status is one of: processed, skipped, error, already_active, grace_period, already_in_division
Verification Check
Read-only lookup of a user's Roblox verification and background check status
/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.
guildId |
string — required |
discordId optional |
string — Discord user ID; provide this OR robloxId |
robloxId optional |
string — Roblox user ID; provide this OR discordId |
Verified: { verified: true, discordId, robloxId, robloxUsername, backgroundCheckPassed, backgroundCheckStatus }
Not verified: { verified: false }