API routes are served through SetHttpHandler (Koa), under http://<server>:<port>/<resource>/v1. GET /health and GET /v1/health are public. Other API routes require a scoped API key. The browser panel has separate session authentication.
Keys are created only from the server console (never from in-game chat):
simpleac:apikey create <name> <scope[,scope...]> [expiryDays] [allowedIp[,ip...]]
simpleac:apikey list
simpleac:apikey revoke <SAC-KEY-id>
The raw key (sac_<12 hex>.<43 chars>) is printed once. Only a scrypt hash and the non-secret prefix are stored. Creation and revocation write api_key.created / api_key.revoked actions. Send the key as Authorization: Bearer <key>.
Verified keys are cached in memory for 30 seconds; revoking from the console clears the cache immediately.
| Scope | Grants |
|---|---|
admin |
Everything |
players:read |
Player list/detail, sessions |
identifiers:read |
Raw identifiers in player detail; identifier lookups |
actions:read |
Action ledger |
detections:read |
Detections, evidence and the overview |
detections:write |
Confirm or dismiss a detection (implies read) |
cases:read |
Cases, their notes and linked evidence |
cases:write |
Create, update and annotate cases (implies read) |
exceptions:read |
Persistent exceptions |
exceptions:write |
Create and revoke exceptions (implies read) |
profiles:read |
Detection profiles and their versions |
bans:read |
Ban list/detail |
bans:write |
Create and revoke bans (implies bans:read) |
captures:read |
Screenshot evidence, OCR results and images |
captures:write |
Request a screenshot (implies captures:read) |
keys:read |
API key metadata via lookup |
| Method | Path | Scope |
|---|---|---|
| GET | /v1/players |
players:read (q, limit, before) |
| GET | /v1/players/:id |
players:read |
| GET | /v1/players/:id/links |
identifiers:read |
| GET | /v1/captures |
captures:read (playerId, caseId, status, trigger, ocr=matched) |
| GET | /v1/captures/:id |
captures:read |
| GET | /v1/captures/:id/image |
captures:read |
| POST | /v1/players/:id/captures |
captures:write |
| POST | /v1/captures/:id/rescan |
captures:write (re-reads the stored image with the current OCR rules) |
| POST | /v1/captures/:id/delete |
captures:write (reason; removes the image, keeps the record and OCR text) |
| GET | /v1/ocr/rules |
captures:read |
| GET | /v1/overview |
detections:read |
| GET | /v1/detections |
detections:read (playerId, caseId, ruleKey, category, status) |
| GET | /v1/detections/:id |
detections:read (measurements, evidence, captures) |
| POST | /v1/detections/:id/review |
detections:write (status: open, confirmed, dismissed; reason) |
| GET | /v1/cases |
cases:read (playerId, status, assignedTo) |
| GET | /v1/cases/:id |
cases:read (detections, timeline, captures) |
| POST | /v1/cases |
cases:write (playerId, title, priority, reason) |
| POST | /v1/cases/:id/update |
cases:write (status, priority, assignedTo, reason) |
| POST | /v1/cases/:id/notes |
cases:write (note) |
| POST | /v1/cases/:id/detections |
cases:write (detectionId) |
| POST | /v1/cases/:id/captures |
cases:write (captureId) |
| GET | /v1/exceptions |
exceptions:read (active, scopeType, scopeValue) |
| GET | /v1/exceptions/:id |
exceptions:read |
| POST | /v1/exceptions |
exceptions:write (scopeType, scopeValue, effect, reason, durationHours) |
| POST | /v1/exceptions/:id/revoke |
exceptions:write (reason) |
| GET | /v1/profiles |
profiles:read |
| GET | /v1/profiles/:id |
profiles:read (version selects an older config) |
| GET | /v1/actions |
actions:read (targetId, actionType) |
| GET | /v1/actions/:id |
actions:read |
| GET | /v1/bans |
bans:read (playerId, active) |
| GET | /v1/bans/:id |
bans:read |
| POST | /v1/bans |
bans:write |
| POST | /v1/bans/:id/revoke |
bans:write |
| GET | /v1/lookup?q= |
scope of the record type found |
/v1/lookup accepts any SAC-* identifier or a platform identifier (for example license:..., which requires identifiers:read).
Lists are paginated newest-first: pass nextBefore from the previous page as before. limit is 1-100 (default 25).
- Bodies must be
application/json, at most 64 KiB. - Send an
Idempotency-Keyheader (8-128 chars) to make retries safe. A repeated request with the same key and payload replays the stored response withIdempotent-Replayed: true; the same key with a different payload returns422 idempotency_key_reuse. Keys are retained for 24 hours. - Every mutation writes an immutable
sac_actionsrow withactor_type = web_api, the key ID as actor, and the request ID as correlation ID. Reverting a ban creates a newban.revokedaction referencing the original. POST /v1/bansdrops the player if currently connected.- Detections are never edited:
reviewonly changes the triage status (open,confirmed,dismissed) and writes adetection.reviewedaction, which also appears on any linked case's timeline. - Case changes (
case.created,case.updated,case.note_added,case.detection_linked,case.capture_linked) each write an action and a timeline event. A case can only link detections and captures of its own player. Setting a case to the values it already has returns409. - Exceptions take effect immediately: the API rebuilds the detection engine's in-memory cache after every create or revoke.
scopeTypeisplayer(aSAC-PLYid),detection(a rule key such asstate.godmode) orresource(a resource name);effectisalloworignore. An active exception with the same scope, value and effect returns409. Revoking records the reversal of theexception.createdaction. POST /v1/captures/:id/deleteremoves only the stored image. The capture row, OCR text and ledger entries stay, and the capture shows asexpiredwith the errordeleted_by_staff.POST /v1/captures/:id/rescanre-reads the image with the current OCR rules and can raise a newevidence.ocr_matchdetection (subject to that rule's cooldown). It returns202; the result appears on the capture.- Profiles are read-only. The detection engine runs the bundled profile from
configs/server/profile.luaand records each version in the database for the audit trail; changing thresholds means editing that file.
{ "error": { "code": "validation_failed", "message": "...", "requestId": "...", "details": [] } }Codes: bad_request, validation_failed, unauthenticated, forbidden, not_found, conflict, idempotency_key_reuse, idempotency_in_progress, payload_too_large, unsupported_media_type, rate_limited, internal_error. Callers may supply X-Request-Id (letters, digits, ., _, -, max 40); otherwise one is generated.
- 60-request burst, 2 requests/second sustained, per key.
- Failed authentication is throttled per remote address (10 burst, 10/minute).
- Keys may be restricted to specific source IPs.
Route browser and API traffic through an HTTPS reverse proxy. FXServer's game port must remain reachable by players.