API reference
Reference for rCTF API versions, authentication, envelopes, permissions, rate limits, and typed route definitions.
The rCTF API is a JSON REST API served from /api. Route definitions are exported from the @rctf/types package, and the API server registers each route by prefixing the typed path with /api.
Versions#
Both API versions are available at the same time:
The two versions overlap rather than forming separate complete APIs. Use V2 where a V2 route exists, and fall back to V1 routes for actions that don’t have V2 replacements. Many V2 routes share the same response kind string as their V1 equivalents, but the data payload is often wider in V2 (avatars, country codes, blood index, and similar additions).
Typed route contract#
The @rctf/types package exports every public route definition, response definition, enum, and helper type:
import { GetChallengesRouteV2, type RouteBodyInput, type RouteQueryInput, type RouteSuccessResponse,} from '@rctf/types'
type Query = RouteQueryInput<typeof GetChallengesRouteV2>type Response = RouteSuccessResponse<typeof GetChallengesRouteV2>RouteBodyInput<T> describes the client-side input shape before any zod coercions and transforms have run. The matching RouteQueryInput<T> and RouteParamsInput<T> helpers do the same for query strings and path parameters.
Authentication#
User-authenticated routes require an auth token in the Authorization header:
Authorization: Bearer <auth-token>Service-authenticated admin bot routes also use the Authorization header, but the token here is the shared admin bot service secret from adminBot.provider.options.secretKey.
Token types#
Tokens are encrypted with AES-GCM using the configured tokenKey. Changing tokenKey invalidates every token that was issued before the rotation.
Response envelope#
Most API routes return a JSON envelope:
{ "kind": "goodChallenges", "message": "The retrieval of challenges was successful.", "data": []}Routes whose response definition has no data schema omit the data field:
{ "kind": "goodFlag", "message": "The flag is correct."}The v1 CTFtime leaderboard route is the only typed route that intentionally returns the data body directly. GET /api/v1/integrations/ctftime/leaderboard returns { "standings": [...] } rather than an rCTF envelope.
Request validation#
JSON routes parse the request body as JSON when a body schema exists. Form routes parse multipart/form-data. Validation failures return 400 badBody with a machine-readable reason string:
{ "kind": "badBody", "message": "The request body does not meet requirements.", "data": { "reason": "query:limit: Too small: expected number to be >=1" }}The reason prefix identifies the source as body, query, or params. Malformed JSON returns 400 badJson. Malformed form data returns 400 badBody with body:formData:malformed.
Permissions#
Admin routes use bitmask permissions from Permissions:
When a route lists multiple permission bits, the user needs all of them.
Timing gates#
Routes marked as start-gated return 401 badNotStarted before startTime. Admin users can bypass that gate when the route defines a bypass permission and their token has the required bit.
POST /api/v1/challs/:id/submit is also end-gated, returning 401 badEnded after endTime.
Captcha#
Captcha-protected routes only validate a challenge response when the configured captcha provider marks that action as protected. V1 request bodies use recaptchaCode, and V2 request bodies use captchaCode.
Rate limits#
Rate-limited routes return 429 badRateLimit with data.timeLeft, measured in milliseconds: