Challenges

Challenge listing, solve listing, flag submission, and dynamic-scoring webhook routes.

Edit this page View Markdown

These routes cover public challenge data, solve history, flag submission, and dynamic scoring. Challenge administration is documented under Admin.

Use V2 where available. Flag submission still uses V1 because it has no V2 replacement.

Scoring behavior

For a decay challenge, the configured scoring provider calculates the current value from its point range and solve count. Public responses include the current value, not points.min or points.max. Challenge and solve changes recalculate the leaderboard.

dynamic challenges receive per-team scores through a webhook. See Submit dynamic scores for the request format and Scoring for setup guidance.

Visibility behavior

Participants see challenges after the event starts and each challenge is released. A user with challsRead can bypass the event start time. Hidden challenges remain unavailable, and scheduled challenges stay unavailable until their release time.

GET Challenge list

GET /api/[v2,v1]/challs

Edit this page View Markdown
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

idstring
Challenge ID.
namestring
Display name.
descriptionstring
Markdown challenge description.
categorystring
Category name.
authorstring
Author display name.
files[].namestring
File name.
files[].urlstring
Download URL.
files[].sizenumber | null
File size in bytes.
pointsnumber
Current score after dynamic scoring.
solvesnumber
Current solve count.
sortWeightnumber | null
Optional ordering weight.
tagsstring[] | null
Challenge tags shown to players.
instancerLifetimenumber | null
Instance timeout in milliseconds when the challenge has instancer config and an instancer is enabled.
instancerExtendableboolean
false when the challenge disables extension or the instancer does not support it.
instancerStoppableboolean
false when the instancer does not support stopping an instance.
instancerActions[].idstring
Stable action identifier sent back to the instancer.
instancerActions[].labelstring
Button label shown to players.
adminBotInputsRecord<string, { pattern: string, flags?: string }> | null | undefined
Participant input schema for admin-bot challenges.
hasFlagboolean
Whether the challenge has a flag configured.
scoringKind"decay" | "dynamic" | undefined
Scoring kind: decay, static, or dynamic.
yourScorenumber | undefined
Caller’s current points for this challenge. Present when the user has a solve (or feed entry) for it.
yourPointDeltanumber | undefined
Caller’s latest dynamic point delta for this challenge. Present for dynamic challenges when the caller had an entry in the latest feed tick.

GET Challenge solves

GET /api/[v2,v1]/challs/:id/solves

Edit this page View Markdown
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*string
Challenge ID.

Query parameters

limit*number
Integer >= 1. Maximum enforced by config.
offset*number
Integer >= 0.

Response fields

solves[].idstring
Solve ID.
solves[].createdAtnumber
Unix timestamp in milliseconds.
solves[].userIdstring
Team ID.
solves[].userNamestring
Team display name.
solves[].userAvatarUrlstring | null
Avatar URL when set.
solves[].userCountryCodestring | null
ISO country code when set.
solves[].userStatusTextstring | null
Status text when set.
solves[].globalPlacenumber
Team’s current global standing at solve time.
solves[].divisionstring
Team division at solve time.
solves[].divisionPlacenumber
Team’s current divisional standing at solve time.
solves[].bloodIndexnumber | null
0, 1, or 2 for the first three solves. Otherwise null.
mySolvePositionnumber | null
Authenticated user’s solve position. Present when optional auth is sent.

POST Submit a flag

POST /api/v1/challs/:id/submit

Edit this page View Markdown
Auth
Required
Gate
Started + Before end (bypass challsWrite)
Permissions
No extra permissions
Captcha
No captcha
Rate limit
Burst 5, refill window 25000 ms, 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*string
Challenge ID.

Request body

flag*string
Maximum length 1024.

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

Edit this page View Markdown
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 checks an HMAC signature derived from the challenge’s per-instance webhook secret in scoring.source.secret. It does not use a user auth token. See Scoring for team identifiers and a publisher example.

Warning (No event-timing gate)

This route accepts scores before startTime and after endTime. The scoring backend must stop sending updates when the event ends, or later requests will continue to change the final tally.

Authentication

Header Value
X-RCTF-Timestamp Unix milliseconds at signing time. Must be within a five-minute skew window of the server clock.
X-RCTF-Signature sha256= followed by the lowercase hex HMAC-SHA256 of ${timestamp}.${challenge_id}.${raw_body} using the challenge’s webhook secret.

Sign the exact bytes sent in the request body. Changes to whitespace, key order, or serialization after signing will make the signature invalid. Include the :id path parameter in the signed value, which binds the signature to that challenge.

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.

rCTF also refuses to apply the same delivery twice. Each accepted signature is remembered for ten minutes, and re-sending the same signed bytes to the same challenge within that window returns 409 badReplayedRequest, so a captured request can’t be replayed against the scoreboard.

Only accepted deliveries are remembered, which makes retries safe. If a push fails or the response gets lost, resend the identical signed request. It will either go through or return 409 badReplayedRequest, confirming that the original attempt was accepted. Retry one attempt at a time, and sign a new request with a fresh timestamp whenever the scores change. Because the challenge ID is part of the signed string, the same payload must be signed separately for each challenge, even when several challenges share a secret.

Path parameters

id*string
Challenge ID.

Request body

scores[].userId*string
Team ID the score applies to.
scores[].points*number
Point value to assign (32-bit signed integer).

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 were written.

Response fields

insertednumber
Number of new dynamic scores inserted.
updatednumber
Number of existing dynamic scores updated.
deletednumber
Number of dynamic scores removed.

Successful 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.

Type to search.