Base path: /api/v1. JSON in and out (Content-Type: application/json).
The browser reaches the API on its own origin (/api), so cookies are
first-party and no CORS is involved. Interactive documentation (Swagger UI)
is served at /api/docs when SWAGGER_ENABLED=true (local default; off in
production).
-
Authentication: server-side session in the HttpOnly cookie
CONTESTER_SESSION(PostgreSQL-backed Spring Session). Sessions expire after 7 days of inactivity and can be revoked. -
CSRF: every state-changing request (
POST,PUT,PATCH,DELETE) must send the headerX-XSRF-TOKENwith the value of theXSRF-TOKENcookie.GET /auth/csrfsets the cookie if it is missing. Missing or wrong token:403. -
Roles:
USERandADMIN. Everything is deny-by-default; the table below lists who may call each endpoint. "user" = any logged-in account, "public" = no session needed. -
Pagination:
?page=0&size=20(size 1–100). Paged responses:{ "items": [], "page": 0, "size": 20, "totalItems": 0, "totalPages": 0 } -
Timestamps: ISO-8601 UTC strings (
2026-11-10T14:00:00Z). -
Errors: RFC 9457 problem details, always with a request ID:
{ "type": "about:blank", "title": "Bad Request", "status": 400, "detail": "Validation failed", "instance": "/api/v1/auth/register", "requestId": "6f1c…", "errors": [{ "field": "username", "message": "must match ^[A-Za-z0-9_]{3,20}$" }] }401no or expired session ·403authenticated but not allowed, or bad CSRF token ·404not found or not visible to the caller (other users' submissions, unpublished contests, problems before a contest starts) ·409conflict (duplicate email/username, idempotency mismatch) ·422rule violation (e.g. submitting to a contest that has not started) ·429rate limited (Retry-Afterheader) ·503judge unavailable. -
Request IDs: every response carries
X-Request-Id; send your own to correlate.
type Role = 'USER' | 'ADMIN'
type Language = 'python' | 'javascript' | 'java' | 'cpp'
type Difficulty = 'EASY' | 'MEDIUM' | 'HARD'
type ContestStatus = 'UPCOMING' | 'ONGOING' | 'ENDED'
type SubmissionStatus =
| 'QUEUED' | 'RUNNING' // pending
| 'ACCEPTED' | 'WRONG_ANSWER' | 'TIME_LIMIT_EXCEEDED'
| 'MEMORY_LIMIT_EXCEEDED' | 'RUNTIME_ERROR' | 'COMPILATION_ERROR'
| 'INTERNAL_ERROR' // judge failure, safe to retry
interface CurrentUser {
id: number; email: string; username: string; role: Role
firstName: string | null; lastName: string | null; bio: string | null; country: string | null
emailVerified: boolean; createdAt: string
}
interface PublicProfile {
username: string; firstName: string | null; lastName: string | null
bio: string | null; country: string | null; createdAt: string
stats: { points: number; solved: number; contests: number; submissions: number; acceptanceRate: number | null; rank: number | null }
}
interface ContestSummary {
id: number; title: string; startsAt: string; endsAt: string; status: ContestStatus
problemCount: number; participantCount: number; registered: boolean
}
interface ProblemSummary {
id: number; label: string // "A", "B", ...
title: string; points: number; difficulty: Difficulty
solved: boolean; attempted: boolean // for the caller
}
interface ContestDetail extends ContestSummary {
description: string // Markdown
problems: ProblemSummary[] // [] until the contest starts (admins always see them)
}
interface ProblemDetail {
id: number; contestId: number; contestTitle: string; contestStatus: ContestStatus
label: string; title: string; points: number; difficulty: Difficulty
statement: string; inputSpec: string; outputSpec: string // Markdown
timeLimitMs: number; memoryLimitMb: number
samples: { input: string; output: string }[]
solved: boolean
}
interface TestResult {
position: number; sample: boolean; verdict: SubmissionStatus; timeMs: number | null
// only for sample tests (hidden test data is never returned):
input?: string; expectedOutput?: string; actualOutput?: string
}
interface Submission {
id: number; problemId: number; problemLabel: string; problemTitle: string
contestId: number; username: string
language: Language; status: SubmissionStatus
passedCount: number; totalCount: number
failedTest: number | null // 1-based position of the first failing test
timeMs: number | null // max over executed tests
compileOutput: string | null // compiler message for COMPILATION_ERROR
inContest: boolean // submitted while the contest was running
createdAt: string; judgedAt: string | null
sourceCode?: string // only in GET /submissions/{id}
results?: TestResult[] // only in GET /submissions/{id}
}
interface Comment {
id: number; author: { username: string }; body: string
createdAt: string; updatedAt: string; canDelete: boolean
replies: Comment[] // one level deep
}| Method | Path | Who | Body | Success | Notes |
|---|---|---|---|---|---|
| GET | /auth/csrf |
public | – | 204 |
Sets XSRF-TOKEN cookie |
| POST | /auth/register |
public | {email, username, password} |
201 CurrentUser |
Logs in; sends verification e-mail. 409 duplicate |
| POST | /auth/login |
public | {email, password} |
200 CurrentUser |
401 wrong credentials. Rate limited |
| POST | /auth/logout |
user | – | 204 |
Invalidates the session |
| POST | /auth/logout-all |
user | – | 204 |
Invalidates every session of the user |
| GET | /auth/me |
user | – | 200 CurrentUser |
401 if not logged in |
| POST | /auth/verify-email |
public | {token} |
204 |
Single-use token, 24 h |
| POST | /auth/verify-email/resend |
user | – | 204 |
|
| POST | /auth/password/forgot |
public | {email} |
202 |
Always 202 (no account enumeration) |
| POST | /auth/password/reset |
public | {token, password} |
204 |
Single-use, 1 h; revokes all sessions |
Rules: e-mail ≤ 254 chars; username ^[A-Za-z0-9_]{3,20}$ (case-insensitive
unique); password 8–128 chars with a letter and a digit.
| Method | Path | Who | Body | Success |
|---|---|---|---|---|
| PATCH | /me |
user | {username?, firstName?, lastName?, bio?, country?} |
200 CurrentUser |
| POST | /me/password |
user | {currentPassword, newPassword} |
204 (other sessions revoked) |
| DELETE | /me |
user | {password} |
204 (account, submissions, comments deleted) |
firstName/lastName ≤ 50, bio ≤ 500, country ISO 3166-1 alpha-2 or null.
| Method | Path | Who | Success |
|---|---|---|---|
| GET | /users/{username} |
public | 200 PublicProfile |
| GET | /users/{username}/activity?limit=10 |
public | 200 [{problemId, problemTitle, contestId, contestTitle, solvedAt}] (limit 1–50) |
| GET | /leaderboard?page&size&country |
public | 200 Page<{rank, username, country, points, solved}> |
Global points = sum of points of every distinct problem the user has solved (in or after a contest). Ties share a rank.
| Method | Path | Who | Success | Notes |
|---|---|---|---|---|
| GET | /contests?status&q&page&size |
public | 200 Page<ContestSummary> |
status = UPCOMING/ONGOING/ENDED; upcoming sorted soonest first, others newest first. Published only |
| GET | /contests/{id} |
public | 200 ContestDetail |
|
| PUT | /contests/{id}/registration |
user | 200 ContestSummary |
Idempotent. 422 after the contest ended |
| DELETE | /contests/{id}/registration |
user | 204 |
Only before the start (422 otherwise) |
| GET | /contests/{id}/standings?page&size |
public | 200 Standings |
422 before the contest starts |
interface Standings {
problems: { id: number; label: string; points: number }[]
rows: Page<{
rank: number; username: string; country: string | null
points: number; penaltyMinutes: number
problems: { problemId: number; solved: boolean; attempts: number; solvedAtMinute: number | null }[]
}>
}Scoring: only submissions made while the contest runs count. A problem scores its points on the first accepted submission. Penalty = minutes from the contest start to each first acceptance plus 10 minutes per rejected attempt before it (compilation errors and judge errors are not counted). Rank by points desc, then penalty asc; equal pairs share a rank. Registered participants without submissions appear with 0 points.
| Method | Path | Who | Body | Success | Notes |
|---|---|---|---|---|---|
| GET | /problems/{id} |
public | – | 200 ProblemDetail |
404 before the contest starts (admins excepted) |
| POST | /problems/{id}/run |
user | {language, sourceCode, input} |
200 RunResult |
Runs once on custom input. 10/min per user. 503 when the judge is unreachable |
| POST | /problems/{id}/submissions |
user | {language, sourceCode, clientRequestId} |
202 Submission |
Queued for judging. Replay with the same clientRequestId returns 200 with the original; a different payload returns 409. 6/min per user. While the contest runs the user is registered automatically |
| GET | /submissions/{id} |
owner, admin | – | 200 Submission (with sourceCode, results) |
Poll until status is final |
| GET | /submissions?problemId&contestId&page&size |
user | – | 200 Page<Submission> |
The caller's own submissions, newest first |
interface RunResult {
status: 'OK' | 'COMPILATION_ERROR' | 'RUNTIME_ERROR' | 'TIME_LIMIT_EXCEEDED' | 'MEMORY_LIMIT_EXCEEDED'
stdout: string; stderr: string; compileOutput: string | null
exitCode: number | null; timeMs: number | null
}sourceCode ≤ 64 KB, input ≤ 64 KB. Output is compared line by line
after trimming trailing whitespace on each line and trailing blank lines.
Judging stops at the first failing test. Time limits apply to CPU time;
Java gets 2 × limit + 3 s because Piston compiles Java inside the run
stage (judge.md). If the judge stays unavailable for
three attempts, the submission ends as INTERNAL_ERROR.
| Method | Path | Who | Success |
|---|---|---|---|
| GET | /languages |
public | 200 [{id: Language, name, version, monaco, template}] |
monaco is the editor's language mode and template the starter code shown
in the editor.
| Method | Path | Who | Body | Success |
|---|---|---|---|---|
| GET | /problems/{id}/comments?page&size |
public | – | 200 Page<Comment> (top-level, newest first, replies oldest first) |
| POST | /problems/{id}/comments |
user (verified e-mail) | {body, parentId?} |
201 Comment |
| DELETE | /comments/{id} |
author, admin | – | 204 (replies are deleted too) |
body 1–2000 chars; replies to replies are rejected (422). 5 comments
per minute per user.
| Method | Path | Body | Success |
|---|---|---|---|
| GET | /admin/contests?page&size |
– | 200 Page<AdminContest> (includes unpublished) |
| POST | /admin/contests |
ContestInput |
201 AdminContest |
| GET | /admin/contests/{id} |
– | 200 AdminContest |
| PUT | /admin/contests/{id} |
ContestInput |
200 AdminContest |
| DELETE | /admin/contests/{id} |
– | 204 (problems, tests, submissions deleted) |
| GET | /admin/contests/{id}/problems |
– | 200 AdminProblem[] |
| POST | /admin/contests/{id}/problems |
ProblemInput |
201 AdminProblem |
| GET | /admin/problems/{id} |
– | 200 AdminProblem |
| PUT | /admin/problems/{id} |
ProblemInput |
200 AdminProblem |
| DELETE | /admin/problems/{id} |
– | 204 |
| GET | /admin/problems/{id}/tests |
– | 200 TestCase[] |
| PUT | /admin/problems/{id}/tests |
TestCaseInput[] |
200 TestCase[] (replaces all, atomically) |
| POST | /admin/problems/{id}/rejudge |
– | 202 {queued: number} |
interface ContestInput {
title: string // 3–120
description: string // Markdown, ≤ 10 000
startsAt: string; endsAt: string // endsAt > startsAt, duration ≤ 30 days
published: boolean
}
interface AdminContest extends ContestInput {
id: number; status: ContestStatus; problemCount: number; participantCount: number
createdAt: string; updatedAt: string
}
interface ProblemInput {
title: string // 3–120
statement: string; inputSpec: string; outputSpec: string // Markdown, ≤ 20 000 each
timeLimitMs: number // 250–10 000
memoryLimitMb: number // 32–512
points: number // 1–10 000
difficulty: Difficulty
position: number // 1-based order in the contest; label = A, B, ...
}
interface AdminProblem extends ProblemInput {
id: number; contestId: number; label: string; testCount: number; sampleCount: number
createdAt: string; updatedAt: string
}
interface TestCaseInput { input: string; expectedOutput: string; sample: boolean } // ≤ 1 MB each, 1–100 tests
interface TestCase extends TestCaseInput { id: number; position: number }| Path | Purpose |
|---|---|
/actuator/health/liveness |
Process is up |
/actuator/health/readiness |
Database reachable |
/actuator/health |
Aggregate including the judge (Piston) |
/actuator/prometheus |
Metrics (internal network only; not routed publicly) |