Challenges
Challenge listing, solve listing, flag submission, and dynamic-scoring webhook routes.
Challenge routes provide public challenge metadata, solve history, the legacy flag submission endpoint, and the dynamic-scoring webhook. Admin challenge mutation routes are documented in Admin.
For new clients, prefer V2 routes. V1 routes remain available for older clients. Flag submission still uses V1 because there is no V2 route for that action.
Scoring behavior#
Challenge point values come from the configured scoring provider for decay challenges. The public challenge list returns the current point value. It does not expose the configured points.min or points.max range. Challenge updates and solve changes trigger leaderboard recalculation.
dynamic challenges receive per-team scores over a webhook instead - see Submit dynamic scores for the wire format and Scoring for the operator-facing model.
Visibility behavior#
Regular users see challenges once those challenges are visible and released. Admin users with challsRead can read through the CTF start gate with the same public routes. Hidden and scheduled release rules still apply inside the challenge service.
GET Challenge list
GET /api/[v2,v1]/challs
- Auth
- Optional
- Gate
- Started (bypass challsRead)
- Permissions
- No extra permissions
- Captcha
- No captcha
- Rate limit
- No rate limit
Returns the challenge list visible to the current request. Regular users do not receive hidden challenges or challenges with a future releaseTime.
For new clients, prefer V2. It adds instancer metadata, admin bot input schemas, and the hasFlag field. V1 remains available for older clients and returns the original challenge fields.
Response fields
idstringnamestringdescriptionstringcategorystringauthorstringfiles[].namestringfiles[].urlstringfiles[].sizenumber | nullpointsnumbersolvesnumbersortWeightnumber | nullinstancerLifetimenumber | nullinstancerExtendablebooleanadminBotInputsRecord<string, { pattern: string, flags?: string }> | null | undefinedhasFlagbooleanscoringKind"decay" | "dynamic" | undefinedyourScorenumber | undefinedyourPointDeltanumber | undefinedResponse fields
idstringnamestringdescriptionstringcategorystringauthorstringfiles[].namestringfiles[].urlstringpointsnumbersolvesnumbersortWeightnumber | null | undefinedGET Challenge solves
GET /api/[v2,v1]/challs/:id/solves
- Auth
- Optional
- Gate
- Started (bypass challsRead)
- Permissions
- No extra permissions
- Captcha
- No captcha
- Rate limit
- No rate limit
Returns a page of solve history for one challenge.
V2 includes solve avatars, country codes, division placements, blood index, and the authenticated user’s mySolvePosition when optional auth is present. V1 remains available for older clients and returns a smaller solve row.
Path parameters
id requiredstringQuery parameters
limit requirednumber>= 1. Maximum enforced by config.offset requirednumber>= 0.Response fields
solves[].idstringsolves[].createdAtnumbersolves[].userIdstringsolves[].userNamestringsolves[].userAvatarUrlstring | nullsolves[].userCountryCodestring | nullsolves[].userStatusTextstring | nullsolves[].globalPlacenumbersolves[].divisionstringsolves[].divisionPlacenumbersolves[].bloodIndexnumber | null0, 1, or 2 for the first three solves. Otherwise null.mySolvePositionnumber | nullResponse fields
solves[].idstringsolves[].createdAtnumbersolves[].userIdstringsolves[].userNamestringPOST Submit a flag
POST /api/v1/challs/:id/submit
- Auth
- Required
- Gate
- Started + Before end (bypass challsWrite)
- Permissions
- No extra permissions
- Captcha
- No captcha
- Rate limit
- Burst
5, refill window25000ms, scoped to user and challenge.
Submits a flag for the authenticated team. This route uses V1 because there is no V2 equivalent.
Rate limit conventions are documented under /api#rate-limits.
Path parameters
id requiredstringRequest body
flag requiredstring1024.A correct flag returns 200 goodFlag (no data) and records a solve for the team. The route records a submission audit row for both correct and incorrect attempts. Admins can read those rows through /api/v2/admin/submissions.
POST Submit dynamic scores
POST /api/v2/challs/:id/scores
- Auth
- Service
- Gate
- None
- Permissions
- No extra permissions
- Captcha
- No captcha
- Rate limit
- No rate limit
Publishes per-team scores for a dynamic challenge. The endpoint is authenticated through an HMAC signature derived from the challenge’s per-instance webhook secret (the scoring.source.secret value set on the challenge); no user auth token is involved. The operator guide for this endpoint, including the team-identifier patterns and the recommended publisher snippet, lives at Scoring.
Warning (No event-timing gate)
This route deliberately has neither onlyWhenStarted nor onlyWhenNotFinished. Backends can seed
scores before startTime and any deliveries that land after endTime are still applied to the
final tally. Cutting the feed off cleanly at end is the operator’s job.
Authentication#
Sign the raw request bytes, not the re-serialized JSON. Any difference between the signed bytes and the bytes on the wire (whitespace, key ordering, middleware re-serialization) will fail verification.
Unknown challenge IDs, non-dynamic challenges, mismatched signatures, and timestamps outside the skew window all return the same 401 badSignature so the endpoint can’t be probed for which challenges accept a feed.
Path parameters
id requiredstringRequest body
scores requiredobject[]Each entry is an absolute setter for that team and challenge. A points value of 0 clears the team’s score for the challenge, negative values are accepted, and teams omitted from the payload keep whatever score they already had. Entries for team IDs that don’t exist are silently dropped - the response counts only reflect rows that actually landed.
Response fields
insertednumberupdatednumberdeletednumberSuccessful pushes also fire the leaderboard:force-update Redis pub/sub message, so a split frontend + leaderboard deployment still picks up the new scores on the worker’s next tick.