This document defines the exact request and payload contract a Minecraft mod should follow when uploading training cases to ScamScreener.
It distinguishes between:
- what the server minimally accepts
- what the mod must send if all current UI and admin features should work correctly
This matters because the upload endpoint only validates a small core schema, while the website and admin views render additional fields directly from the stored payload. If those fields are missing, the upload succeeds, but the UI shows -, blank values, or empty sections.
Target API base URL:
https://scamscreener.creepans.net
- Never ask the player for Minecraft, Microsoft, or Mojang credentials.
- Only use ScamScreener account credentials.
- Only send requests over
https://. - Do not disable TLS verification.
- Do not store the ScamScreener password after login.
- Prefer keeping the API session token in memory only.
- Never log passwords or Bearer tokens.
- Do not send multipart form data for uploads.
- Do not wrap training cases in an outer JSON object or array.
Endpoint:
POST /api/v1/client/auth/login
Headers:
Content-Type: application/json
Request body:
{
"usernameOrEmail": "alice",
"password": "supersecret"
}The server also accepts username_or_email, but usernameOrEmail is the preferred field name for clients.
Success response:
{
"status": "ok",
"sessionToken": "TOKEN_VALUE",
"expiresAt": "2026-03-30T20:15:00Z",
"user": {
"id": 1,
"username": "alice",
"isAdmin": false
}
}Use the returned token in all subsequent API requests:
Authorization: Bearer TOKEN_VALUE
Important responses:
200withstatus=ok: login succeeded401: invalid credentials403: admin account blocked for API use when web MFA is required429withstatus=locked: login temporarily locked; respectretryAfter
Logout endpoint:
POST /api/v1/client/auth/logout
Endpoint:
POST /api/v1/client/uploads
Send these headers:
Authorization: Bearer TOKEN_VALUE
Content-Type: application/x-ndjson
X-ScamScreener-Filename: training-cases-v2.jsonl
Notes:
- The server reads raw request bytes, so the body must be the NDJSON file itself.
- Do not send multipart form data.
- Do not gzip the request unless the server explicitly adds support for it later.
X-ScamScreener-Filenameshould be a plain file name, not a path.
Request body rules:
- UTF-8 encoded
- one complete JSON object per physical line
- no outer array
- no outer object
- blank lines are ignored
- line breaks inside message text must be escaped as JSON string escapes such as
\n
This is correct:
{"format":"training_case_v2","schemaVersion":2,"caseId":"case_1"}
{"format":"training_case_v2","schemaVersion":2,"caseId":"case_2"}
This is not correct:
[
{
"format": "training_case_v2",
"schemaVersion": 2,
"caseId": "case_1"
}
]Accepted:
{
"status": "accepted",
"uploadId": 12,
"caseCount": 34,
"insertedCases": 34,
"updatedCases": 0,
"sha256": "..."
}Duplicate for the same account:
{
"status": "duplicate",
"uploadId": 12,
"caseCount": 34,
"sha256": "..."
}Quota exceeded:
{
"status": "quota-exceeded",
"detail": "Daily upload count limit reached for your account.",
"caseCount": 34,
"sha256": "..."
}Relevant status codes:
201: upload accepted200: duplicate upload for the same account400: invalid UTF-8, invalid JSON, invalid schema, or missingcaseId401: missing or invalid Bearer token413: upload too large429: upload quota exceeded
Each non-empty line must be a JSON object containing:
{
"format": "training_case_v2",
"schemaVersion": 2,
"caseId": "case_000001"
}Validation details:
formatmust betraining_case_v2schemaVersionmust parse to integer2caseIdmust be present and non-empty
The server accepts schemaVersion as an integer or a numeric string, but the mod should send it as the number 2.
If you only send the minimum schema, the upload is accepted, but most UI features remain empty.
To make all current case display features work, each case should include:
caseData.labelcaseData.messagescaseData.caseSignalTagIdsobservedPipeline.outcomeAtCaptureobservedPipeline.scoreAtCaptureobservedPipeline.decidedByStageIdobservedPipeline.stageResultssupervision.contextStage.targetLabelsupervision.contextStage.signalMessageIndicessupervision.contextStage.contextMessageIndicessupervision.contextStage.excludedMessageIndicessupervision.contextStage.targetSignalTagIds
Optional but safe to include:
supervision.fixedStageCalibrations
The current server stores and exports the entire payload, but several pages derive their visible values from these exact fields. If a field is missing, the upload still succeeds; the page simply cannot render that value.
Recommended shape:
"caseData": {
"label": "risk",
"messages": [
{
"index": 0,
"role": "message",
"text": "Hello there"
}
],
"caseSignalTagIds": [
"middleman-claim",
"trust-language"
]
}Used for:
- case label in list/detail views
- conversation rendering
- case-level signal tags
Requirements:
labelshould be a non-empty string if you want a visible labelmessagesshould be an arraycaseSignalTagIdsshould be an array of non-empty strings
Supported message field aliases:
- index:
indexormessageIndex - speaker:
role,sender,author,username, orsource - text:
text,content,message,raw, orbody
Recommended message object:
{
"index": 3,
"role": "message",
"text": "I can middleman this trade for you."
}If messages is missing or empty, the conversation section is empty.
Recommended shape:
"observedPipeline": {
"outcomeAtCapture": "review",
"scoreAtCapture": 0.93,
"decidedByStageId": "stage.rule",
"stageResults": [
{
"stageId": "stage.rule",
"outcome": "pass",
"score": 0.93,
"reason": "Matched middleman phrasing"
}
]
}Used for:
- stored case outcome
- top-level score display
- decided-by-stage display
- stage results table
Requirements:
outcomeAtCaptureshould be present if you want a visible outcomescoreAtCaptureshould be present if you want the top score to renderdecidedByStageIdshould be present if you want the selected stage shownstageResultsshould be an array if you want stage rows displayed
Supported stage result field aliases:
- stage id:
stageIdorid - outcome:
outcomeordecision - score:
scoreorscoreAtStage - reason:
reasonornote
Recommended stage result object:
{
"stageId": "stage.context",
"outcome": "pass",
"score": 0.52,
"reason": "Context stage reinforced risk signal"
}If scoreAtCapture is missing, the detail page shows - for the top score.
If a stage result does not contain score or scoreAtStage, that row shows - in the score column.
If a stage result does not contain reason or note, that row shows - in the reason column.
Recommended shape:
"supervision": {
"contextStage": {
"targetLabel": "risk",
"signalMessageIndices": [1, 4],
"contextMessageIndices": [0, 2, 3],
"excludedMessageIndices": [],
"targetSignalTagIds": [
"middleman-claim",
"trust-language"
]
},
"fixedStageCalibrations": []
}Used for:
- context target label
- signal/context/excluded message lists
- target signal tag export and future compatibility
Requirements:
targetLabelshould be a stringsignalMessageIndicesshould be an array of integerscontextMessageIndicesshould be an array of integersexcludedMessageIndicesshould be an array of integerstargetSignalTagIdsshould be an array of strings if your pipeline has them
fixedStageCalibrations is currently not required for rendering, but it is safe to include and will remain part of the stored payload.
The following object includes all fields needed for the current display features. In the actual .jsonl file, serialize it onto a single physical line.
{
"format": "training_case_v2",
"schemaVersion": 2,
"caseId": "case_20260330_000001",
"caseData": {
"label": "risk",
"messages": [
{
"index": 0,
"role": "message",
"text": "yoyoyo"
},
{
"index": 1,
"role": "message",
"text": "i am a legit middleman"
}
],
"caseSignalTagIds": [
"middleman-claim",
"trust-language"
]
},
"observedPipeline": {
"outcomeAtCapture": "review",
"scoreAtCapture": 0.93,
"decidedByStageId": "stage.rule",
"stageResults": [
{
"stageId": "stage.mute",
"outcome": "pass",
"score": 0.01,
"reason": "No mute evasion indicators"
},
{
"stageId": "stage.player_list",
"outcome": "pass",
"score": 0.07,
"reason": "Player list looked normal"
},
{
"stageId": "stage.rule",
"outcome": "pass",
"score": 0.93,
"reason": "Matched middleman phrasing"
},
{
"stageId": "stage.similarity",
"outcome": "pass",
"score": 0.48,
"reason": "Moderate similarity to known scam examples"
},
{
"stageId": "stage.behavior",
"outcome": "pass",
"score": 0.65,
"reason": "Behavioral pattern suspicious"
},
{
"stageId": "stage.trend",
"outcome": "pass",
"score": 0.22,
"reason": "Low historical trend confidence"
},
{
"stageId": "stage.funnel",
"outcome": "pass",
"score": 0.18,
"reason": "Weak funnel signal"
},
{
"stageId": "stage.context",
"outcome": "pass",
"score": 0.52,
"reason": "Context stage reinforced risk signal"
}
]
},
"supervision": {
"contextStage": {
"targetLabel": "risk",
"signalMessageIndices": [
1
],
"contextMessageIndices": [
0
],
"excludedMessageIndices": [],
"targetSignalTagIds": [
"middleman-claim",
"trust-language"
]
},
"fixedStageCalibrations": []
}
}Use this when a value is missing in the website or admin UI.
caseData.label: visible label in case tables and detail viewcaseData.messages: conversation sectioncaseData.caseSignalTagIds: case signal tagsobservedPipeline.outcomeAtCapture: stored outcome and outcome displayobservedPipeline.scoreAtCapture: top-level score displayobservedPipeline.decidedByStageId: decided-by-stage displayobservedPipeline.stageResults[].stageIdorid: stage ID columnobservedPipeline.stageResults[].outcomeordecision: stage outcome columnobservedPipeline.stageResults[].scoreorscoreAtStage: stage score columnobservedPipeline.stageResults[].reasonornote: stage reason columnsupervision.contextStage.targetLabel: context target labelsupervision.contextStage.signalMessageIndices: signal message indicessupervision.contextStage.contextMessageIndices: context message indicessupervision.contextStage.excludedMessageIndices: excluded message indicessupervision.contextStage.targetSignalTagIds: retained in payload and export, recommended for completeness
These rules affect how the mod should serialize and resend files.
- Duplicate detection is based on the SHA-256 hash of the exact raw uploaded file bytes.
- For the same ScamScreener account, uploading byte-identical NDJSON again returns
status=duplicate. - Changing any byte changes the hash. This includes whitespace, field order, number formatting, and line order.
- If a different ScamScreener account uploads the exact same bytes, the upload is still accepted for that account. It is only linked internally as a duplicate of the first upload.
- Case updates are keyed by
caseId. - Re-uploading a known
caseIdreplaces the stored case summary and payload with the newest version for thatcaseId. - Do not include the same
caseIdmultiple times in one file. Later lines can overwrite earlier lines for that case during ingestion.
If you want stable duplicate behavior, serialize JSON deterministically and keep line ordering stable.
Recommended lifecycle inside the mod:
- Show a ScamScreener-specific login form.
- Make it explicit that players must not enter Minecraft credentials there.
- Call
/api/v1/client/auth/login. - Cache the returned session token in memory.
- Build the NDJSON payload deterministically.
- Upload the raw bytes to
/api/v1/client/uploads. - On
401, discard the token and force a new login. - On explicit logout, call
/api/v1/client/auth/logout. - On shutdown, clear any in-memory token.
Recommended error handling:
400on upload: keep the rejected payload for developer inspection401on upload: clear the token and require re-login429on login: wait forretryAfter429on upload: retry later, do not spam retries
If you only want the upload to pass validation, send:
formatschemaVersioncaseId
If you want all current ScamScreener case display features to work, also send:
caseData.labelcaseData.messagescaseData.caseSignalTagIdsobservedPipeline.outcomeAtCaptureobservedPipeline.scoreAtCaptureobservedPipeline.decidedByStageIdobservedPipeline.stageResults[].stageIdoridobservedPipeline.stageResults[].outcomeordecisionobservedPipeline.stageResults[].scoreorscoreAtStageobservedPipeline.stageResults[].reasonornotesupervision.contextStage.*
If scores are showing as -, the mod is not sending the score fields under observedPipeline.