Skip to content

API ​

LCOJ has two machine-facing interfaces: personal API tokens (scripts acting as a user) and the contest sync API (external tools reading contest data as JSON). There is no public data API like /api/v2/problems.

⏱ ~15 min read · 👤 Developers, administrators · 🔑 An API token or the GLOBAL_API_KEY

When you need this page ​

  • You want to write a script that acts on the site as a particular account.
  • You run a contest and want to connect a live scoreboard or an ICPC-style resolver.
  • You tried calling /api/v2/... from the DMOJ docs and got a 404.

LCOJ exposes two machine-facing interfaces:

InterfaceWhat it is forAuthenticationEnabled by default?
Personal API tokenScripts that act as a specific user on regular site pagesAuthorization: Bearer <token>Yes
Contest sync APIExternal scoreboards / resolvers that read a contest's problems, ranking and submissions as JSONX-Global-API-Key header (one site-wide key)No (VNOJ_ENABLE_SYNC_API = False)

No public data API

If you are coming from DMOJ: LCOJ has no public JSON endpoints such as /api/v2/problems, /api/v2/users, or /api/v2/contests; requests to those URLs return 404. The only JSON API under /api/v2/ is the contest sync API described below.

Personal API token ​

A personal API token lets a script log in as you without a password or two-factor authentication. The token is sent as an HTTP header, and the request is then handled exactly as if you were logged in on a browser.

Getting a token ​

MethodWhoHow
Management commandServer administrators./scripts/manage.py generate_api_token <username> (run from dmoj/). Prints the token. See Management commands.
HTTP endpointThe logged-in userPOST /accounts/api/token/generate/ (needs a logged-in session and a CSRF token). Returns {"data": {"token": "..."}}.
  • A token is 48 URL-safe base64 characters. It encodes your user ID plus a random secret; the site stores only an HMAC of the secret, so a token cannot be shown again. Store it when you get it.
  • Generating a new token invalidates the previous one.
  • To revoke a token: POST /accounts/api/token/remove/ as the logged-in user.

INFO

The LCOJ profile editing page does not currently show an API token section, so regular users cannot generate a token from the UI. Ask an administrator if you need one.

Using a token ​

Add this header to every request:

http
Authorization: Bearer <API token>
bash
curl -H "Authorization: Bearer $LCOJ_TOKEN" https://luyencode.net/user

When the token is valid, the request is authenticated as the token's owner, counts as having passed 2FA, and skips CSRF checks.

Errors ​

HTTP statusBodyCause
400Invalid authorization headerThe header is not exactly Bearer followed by 48 characters [A-Za-z0-9_-]
401Invalid tokenUnknown user, the user has no token, or the secret does not match (for example, the token was regenerated)
403Admin inaccessibleTokens can never be used for the Django admin (/admin/)

Treat the token like a password

The token bypasses two-factor authentication. Never share it, never commit it to git, and regenerate it if it leaks.

Contest sync API ​

The sync API lets an external tool (for example, an ICPC-style resolver or a live scoreboard) poll a contest's data. It is read-only and returns plain JSON.

Enabling it ​

Both settings live in dmoj/local_settings.py (see Environment configuration); restart site after changing them.

SettingDefaultMeaning
VNOJ_ENABLE_SYNC_APIFalseWhen True, the /api/v2/sync/... URLs are registered. When False they return 404.
GLOBAL_API_KEYa test value in settings.pyShared secret that every sync request must present. If it is empty/None, every request is rejected with 403.

Change GLOBAL_API_KEY before enabling

The default value in dmoj/settings.py is a public placeholder used by tests. Set a long random key in local_settings.py before turning on VNOJ_ENABLE_SYNC_API. Anyone who knows the key can read any contest by its code, including private and hidden contests: the sync API does not check contest visibility.

Authentication ​

Send the key in a header (preferred) or as a query parameter:

http
X-Global-API-Key: <GLOBAL_API_KEY>
/api/v2/sync/contest/<contest_code>?global_api_key=<GLOBAL_API_KEY>

A missing or wrong key returns 403 api key required.

Endpoints ​

All endpoints are GET. <contest_code> is the contest key (the code in the contest URL /contest/<key>).

EndpointReturns
/api/v2/sync/contest/<contest_code>Contest timing (object)
/api/v2/sync/contest/<contest_code>/problemsProblems in contest order (array)
/api/v2/sync/contest/<contest_code>/participantsCurrent ranking (array)
/api/v2/sync/contest/<contest_code>/submissionsSubmissions judged since a timestamp (array)

Timestamps are ISO 8601 strings with a UTC offset, for example 2026-03-01T08:00:00+00:00.

Contest detail ​

GET /api/v2/sync/contest/<contest_code>

json
{
  "code": "icpc2026",
  "start_time": "2026-03-01T08:00:00+00:00",
  "end_time": "2026-03-01T13:00:00+00:00",
  "frozen_at": "2026-03-01T12:00:00+00:00"
}
FieldDescription
codeContest key
start_time, end_timeContest window
frozen_atend_time minus the contest's frozen last minutes; null when the contest has no freeze

Problems ​

GET /api/v2/sync/contest/<contest_code>/problems

json
[
  {"code": "sum2", "contest": "icpc2026"},
  {"code": "maxpath", "contest": "icpc2026"}
]

Problems are sorted by their order in the contest. code is the problem code.

Participants (ranking) ​

GET /api/v2/sync/contest/<contest_code>/participants

json
[
  {"user": "alice", "contest": "icpc2026", "rank": 1},
  {"user": "bob", "contest": "icpc2026", "rank": 1},
  {"user": "carol", "contest": "icpc2026", "rank": 3}
]
  • Includes only live participations (no virtual participants or spectators) that are not disqualified.
  • Sorted by score (descending), then cumulative time, then the format's tie-breaker. Equal (score, time, tie-breaker) share a rank; the next rank skips (1, 1, 3).
  • While the scoreboard is frozen (only for the icpc and vnoj formats with frozen last minutes > 0, from frozen_at onwards, including after the contest ends), the ranking uses the frozen score and time. See Contest formats.

Submissions ​

GET /api/v2/sync/contest/<contest_code>/submissions?from_timestamp=...

Query parameterRequiredDefaultDescription
from_timestampYes—ISO 8601 time. Returns submissions with a judge time >= this value. A time without an offset is treated as UTC. URL-encode + as %2B.
limitNo2000Maximum number of rows; must be a positive integer, capped at 2000
statusNofinalfinal skips submissions still queued/processing/grading (QU, P, G); all includes them
json
[
  {
    "id": "123456",
    "submittedAt": "2026-03-01T08:15:02+00:00",
    "judgedAt": "2026-03-01T08:15:04+00:00",
    "author": "alice",
    "submissionStatus": "AC",
    "contest_code": "icpc2026",
    "problem_code": "sum2"
  }
]
FieldDescription
idSubmission ID, as a string
submittedAtSubmission time
judgedAtJudge time
authorUsername
submissionStatusResult code (AC, WA, TLE, ...), or the status code if there is no result yet. See Status codes.
contest_code, problem_codeContest key and problem code

Rows are sorted by judge time, then ID. To poll incrementally, pass the last judgedAt you received as the next from_timestamp and de-duplicate by id (the boundary row is returned again because the filter is >=). Rejudged submissions reappear with a new judge time.

Freeze is not applied to submissions

The submissions endpoint returns real verdicts even while the scoreboard is frozen. Keep the API key away from anything that is shown to contestants.

Errors ​

Successful sync responses are the bare object or array shown above. Errors are wrapped:

json
{
  "api_version": "2.0",
  "method": "get",
  "fetched": "2026-03-01T09:00:00.123456+00:00",
  "error": {"code": 400, "message": "invalid filter value type"}
}
HTTP statusmessageCause
400invalid filter value typeMissing or unparseable from_timestamp, bad limit, or status other than final/all
403api key requiredMissing or wrong key, or GLOBAL_API_KEY is not set
404page/object not foundNo contest with this code

Example ​

python
import requests

BASE = "https://luyencode.net/api/v2/sync/contest/icpc2026"
HEADERS = {"X-Global-API-Key": "<your key>"}

contest = requests.get(BASE, headers=HEADERS).json()
ranking = requests.get(f"{BASE}/participants", headers=HEADERS).json()
subs = requests.get(
    f"{BASE}/submissions",
    headers=HEADERS,
    params={"from_timestamp": contest["start_time"], "status": "final"},
).json()
print(len(subs), "judged submissions")
bash
curl -H "X-Global-API-Key: $LCOJ_SYNC_KEY" \
  "https://luyencode.net/api/v2/sync/contest/icpc2026/submissions?from_timestamp=2026-03-01T08:00:00Z&limit=500"

Next steps ​