Interactive specification
The XCTrack task format, decoded.
One competition task, two encodings: a readable .xctsk JSON file that every comp tool imports, and a dense XCTSK: string small enough to fit in a QR code on a briefing screen. Same task, wildly different bytes — this page maps one onto the other and lets you decode both in the browser.
What a task file carries
A competition task is an ordered list of turnpoints — each a WGS84 position with a radius in metres — plus the rules that turn that geometry into a race: where the speed section starts, when its gates open, where it ends, and what shape the goal is.
XCTrack publishes two encodings of exactly that. The .xctsk file (version: 1) is verbose, pretty-printed JSON — the download format every competition tool reads: XCTrack, SeeYou Navigator, Airscore, FS. The task code (version: 2) is the same task squeezed into a single line so it survives being printed as a QR code on a briefing board and scanned off a phone screen.
CLASSIC. The taskType field exists because XCTrack also defines waypoint-less types. Every competition task in the wild is CLASSIC, and it is the only type described here — or accepted by We-Fly.The vocabulary
| Term | Meaning |
|---|---|
| Takeoff | The launch cylinder. Optional, and never scored — the leg to the start is flown, not counted. |
| SSS | Start of Speed Section. The clock starts when you cross it, in the direction the task declares. |
| ESS | End of Speed Section. The clock stops here; the goal may still be further on. |
| Goal | The final turnpoint. A cylinder, or a line perpendicular to the inbound leg. |
| Turnpoint | Any other cylinder you must tag, in order. |
Roles are positional as well as declared: whatever its type, the last turnpoint in the array is the goal. There is no GOAL type — the goal's shape lives in a separate goal block.
File, code, and how to tell them apart
Three byte strings can arrive at your parser. Dispatch on the first characters, not the file extension:
| Starts with | Is | Payload |
|---|---|---|
| { | .xctsk file — version 1 | Plain JSON object |
| XCTSK: | Task code — version 2 | Plain JSON after the prefix |
| XCTSKZ: | Task code — version 2, compressed | Base64 of a raw zlib deflate stream |
XCTSKZ: base64 is URL-safe-tolerant. Codes travelling through URLs or chat apps come back with - and _ in place of + and /. Translate them back before decoding, or a perfectly good code fails to inflate.The two versions are not interchangeable inside their container: a version-1 body in an XCTSK: code, or a version-2 body in a .xctsk file, is malformed. Check the version field after parsing, and reject the mismatch rather than guessing.
.xctsk · the readable form
{
"taskType": "CLASSIC",
"version": 1,
"earthModel": "WGS84",
"turnpoints": [
{
"type": "TAKEOFF",
"radius": 400,
"waypoint": {
"name": "D01", "description": "Chabre launch",
"lat": 44.3806, "lon": 5.7331, "altSmoothed": 1220
}
},
{
"type": "SSS",
"radius": 4000,
"waypoint": { "name": "B23", "lat": 44.4012, "lon": 5.7688, "altSmoothed": 980 }
},
{
"radius": 2000,
"waypoint": { "name": "A15", "lat": 44.5210, "lon": 5.9104, "altSmoothed": 1450 }
},
{
"type": "ESS",
"radius": 1000,
"waypoint": { "name": "A07", "lat": 44.4455, "lon": 5.8102, "altSmoothed": 760 }
},
{
"radius": 400,
"waypoint": {
"name": "G01", "description": "Goal field",
"lat": 44.4301, "lon": 5.7955, "altSmoothed": 640
}
}
],
"sss": { "type": "RACE", "direction": "EXIT", "timeGates": ["12:30:00Z", "12:45:00Z"] },
"goal": { "type": "LINE", "deadline": "18:00:00Z" }
}
Read it as a route: launch at D01, start the clock leaving the 4 km B23 cylinder at 12:30, round A15, stop the clock at A07, then cross the goal line at G01. 1 018 bytes as laid out here. §04 is the same task in 350 characters.
Top level
| Key | Type | Meaning |
|---|---|---|
taskType | string | Always "CLASSIC". |
version | number | Always 1 in a file. |
earthModel | string | "WGS84" (default) or "FAI_SPHERE" — which geodesy the scorer uses for distances. |
turnpoints | array | Ordered; the last entry is the goal. |
sss | object | Speed-section rules. Absent on an open-distance task. |
goal | object | Goal shape and deadline. |
o | object | Present as {"v":2} when the SeeYou observation-zone extension is in use. See §07. |
A turnpoint
| Key | Type | Meaning |
|---|---|---|
type | string | TAKEOFF · SSS · ESS. Omitted for a plain turnpoint and for the goal. |
radius | number | Cylinder radius in metres, integer. |
waypoint.name | string | Short competition code (B23, D01188) — what the instrument shows. |
waypoint.description | string | Optional full name. |
waypoint.lat / lon | number | Signed decimal degrees, WGS84. Five decimals is the working precision (≈ 1 m). |
waypoint.altSmoothed | number | Terrain elevation at the point, metres. Advisory — scoring is horizontal. |
o | object | Observation zone, when the extension is active. See §07. |
sss and goal
| Key | Values | Meaning |
|---|---|---|
sss.type | RACE · ELAPSED-TIME | Race to goal with common gates, or an individual clock. |
sss.direction | ENTER · EXIT | Which crossing of the start cylinder counts. Marked obsolete in the XCTrack spec, but still written by every tool — read it, and keep writing it. |
sss.timeGates | array of HH:MM:SSZ | Start gates, UTC. Several for a multi-gate race — and legally empty, see the note below. |
goal.type | CYLINDER · LINE | Goal shape. A line is drawn through the goal point, perpendicular to the inbound leg, of length 2 × radius. |
goal.deadline | HH:MM:SSZ | Task deadline, UTC. Optional. |
timeGates is legal. The spec calls the field required, but XCTrack exports "timeGates": [] for a task whose start time has not been set yet — a task still being planned. Rejecting the empty array turns an ordinary half-finished task into a parse error. Validate that the field is an array; do not require a length."12:30:00Z" — no date component anywhere in the format. The flying day comes from context (the file name, the comp schedule, the flight it is matched against). A task that crosses midnight UTC is ambiguous in this format; nobody flies one.XCTSK: · the QR form
Version 2 is the same task with every key abbreviated to one letter, every enum turned into a small integer, and each turnpoint's four numbers packed into a single string. The result is short enough to scan.
XCTSK:{"taskType":"CLASSIC","version":2,"t":[{"z":"{v~a@wa{mGgkA_X","n":"D01","d":"Chabre launch","t":1},{"z":"_veb@ob_nGg|@_yF","n":"B23","t":2},{"z":"_kac@govnGsyA_|B","n":"A15"},{"z":"wxmb@kwgnGon@o}@","n":"A07","t":3},{"z":"{|jb@cwdnG_g@_X","n":"G01","d":"Goal field"}],"s":{"g":["12:30:00Z","12:45:00Z"],"d":2,"t":1},"g":{"t":1,"d":"18:00:00Z"}}
Five turnpoints, two start gates and a goal line in 350 characters. Paste it into the decoder in §08 — it is the same task as the file in §03.
Key map
| v2 | v1 equivalent | Encoding |
|---|---|---|
t (top level) | turnpoints | Array, same order. |
e | earthModel | 1 = FAI_SPHERE. Absent = WGS84. |
s | sss | Object. |
g (top level) | goal | Object. |
t[].z | lon, lat, altSmoothed, radius | Four ints, polyline-packed. See §05. |
t[].n | waypoint.name | String. |
t[].d | waypoint.description | String; may be "". |
t[].t | type | 1 TAKEOFF · 2 SSS · 3 ESS. 0/absent = plain. |
s.g | sss.timeGates | Array of HH:MM:SSZ — not abbreviated. May be empty, as in v1. |
s.d | sss.direction | 1 ENTER · 2 EXIT. |
s.t | sss.type | 1 RACE · 2 ELAPSED-TIME. |
g.t | goal.type | 1 LINE · 2 CYLINDER. |
g.d | goal.deadline | HH:MM:SSZ. |
t collision is real. At the top level t is the turnpoint array; inside a turnpoint it is the role integer; inside s it is the start type; inside g it is the goal shape. Four different meanings, one letter — scope every lookup to its own object.XCTSKZ: compresses, but a typical task already fits a scannable QR uncompressed, and XCTSK: can be produced anywhere — no zlib, no WASM, works client-side. Emit XCTSK:, accept both. Both scan identically in XCTrack and SeeYou Navigator.Four numbers in one string
Each v2 turnpoint packs longitude, latitude, altitude and radius — in that order — into z. Each value is scaled to an integer and encoded independently with Google's signed polyline algorithm, and the four results are concatenated with no separator. The decoder recovers exactly four integers or the turnpoint is malformed.
| # | Value | Scale | Unit |
|---|---|---|---|
| 1 | Longitude | × 100 000 | degrees → 1e-5° |
| 2 | Latitude | × 100 000 | degrees → 1e-5° |
| 3 | Altitude | × 1 | metres |
| 4 | Radius | × 1 | metres |
The algorithm, per value
- Take the scaled signed integer.
- Left-shift by one; if the original was negative, invert every bit (
~(v << 1)). This moves the sign into bit 0. - Emit the value five bits at a time, least-significant chunk first.
- Every chunk but the last is OR-ed with
0x20as a continuation flag. - Add 63 to each chunk and write it as an ASCII character — landing in the printable range
?–~.
Decoding runs it backwards: accumulate 5-bit chunks until one arrives without the continuation flag, then undo the zig-zag (v & 1 ? ~(v >>> 1) : v >>> 1). Because the chunks are self-delimiting, four values concatenate unambiguously.
z fieldzEvery field, both versions
Search by field name or meaning, or narrow by category. Each card shows the v1 key and its v2 counterpart.
The SeeYou o extension
Base XCTrack turnpoints are always cylinders. SeeYou Navigator added an optional per-turnpoint o object to describe other observation-zone shapes, and XCTrack reads it. It appears in both versions, and is announced by a top-level "o": {"v": 2} marker.
| Key | Type | Meaning |
|---|---|---|
a1 | number | Sector half-angle in degrees. 180 = a full cylinder. 45 = the FAI 90° photo quadrant. |
a2 | number | Second half-angle, for compound zones. |
a12 | number | Fixed bearing (0–359°) the zone is oriented to. Its presence is what makes a line "fixed" rather than auto-oriented. |
r1 / r2 | number | Inner / outer radii, metres. |
l | number | 1 = this zone is a line, not a cylinder. |
Reading it back
- No
oobject, ora1: 180→ plain cylinder. l: 1witha12→ a line at that fixed bearing.l: 1withouta12→ a line auto-oriented perpendicular to the inbound leg. This is the default in both SeeYou and XCTrack.0 < a1 < 180→ a sector. Tools that model only the FAI quadrant treat any such value as the 90° sector.
"o": {"v": 2}, SeeYou writes an o block on every turnpoint — {"a1": 180} for the plain ones. Matching that byte-for-byte is what makes a round trip through SeeYou Navigator lossless.Paste a task, read it back
Drop in a .xctsk file, an XCTSK: code or an XCTSKZ: code. Everything is decoded in the page — polyline fields unpacked, roles resolved, gates listed. Nothing is uploaded.
Where task files go wrong
- Longitude leads in
z.lon, lat, alt, radius. Swapping the first two is silent — the task still parses. - The goal has no type. The last turnpoint is the goal; its shape comes from the top-level
goalblock. Do not look for aGOALvalue intype. - No
sssmeans no speed section — an open-distance or free task. Do not synthesise a start from the first turnpoint. - Absent
eis WGS84, not "unknown". Onlye: 1selects the FAI sphere. - An empty gate array is a valid task, not a malformed one.
"timeGates": [](v1) and"g": [](v2) both mean "the start time is not set yet". Check the type, not the length. - Times carry no date, and
Zis optional on input. Normalise toHH:MM:SSZand reject out-of-range components rather than letting25:00:00through. XCTSKZ:is raw deflate, not gzip. No gzip header, no member framing — inflate it as a zlib stream, and translate URL-safe base64 first.- A
zfield must yield exactly four integers. Three or five means a truncated or corrupted scan; fail the turnpoint rather than padding it. - Cap what you accept. A task code is a scanned string from an unknown source: bound the input size and the inflated size before you parse. We-Fly caps both at 64 KB.
What We-Fly reads and writes
We-Fly imports .xctsk files and pasted codes in the competition-task library and the XC planner, and exports both forms.
| Direction | Form | Notes |
|---|---|---|
| Import | .xctsk v1 file | Up to 64 KB. CLASSIC only. |
| Import | XCTSK: / XCTSKZ: code | Pasted into the task dialog; inflated server-side. |
| Export | .xctsk v1 file | Pretty-printed, 2-space indent. |
| Export | XCTSK: code + QR | Uncompressed, so it can be produced in the browser; downloadable as PNG. |
Round trips are tested both ways against the other three task formats — see the competition-task formats guide for how .fsdb, .tsk and .cup map onto this same model.
What this reconciles
o extension
Google — Encoded Polyline Algorithmdevelopers.google.com · the integer packing used by the z field
We-Fly — competition task formatswe-fly.cloud/doc · how the four task formats map onto one model