This document describes the current /api/v1 gateway for login,
IGC upload, live tracking, flight retrieval, and task access.
https://skytrackpro.app/api/v1/...POST /api/v1/auth/login,
POST /api/v1/flights/upload,
GET /api/v1/flights,
POST /api/v1/live/start,
GET /api/v1/tasks
The SkyTrackPro API allows trusted clients such as mobile apps, companion tools, and integrations to:
Every request requires a valid API key. User-specific actions additionally require a Bearer access token.
Every JSON response uses the same envelope. Payload data lives under
data; errors carry a stable error_code string in
addition to the human-readable message.
{
"ok": true,
"message": "Login successful.",
"data": {
"token": "...",
"refresh_token": "...",
"user": { "id": 123, "email": "pilot@example.com" }
}
}
{
"ok": false,
"message": "Invalid credentials",
"error_code": "INVALID_CREDENTIALS",
"data": null
}
The /api/v1 gateway accepts active keys from
jscms_skytrack_api_clients. For personal integrations,
a user can generate a personal API key from the SkyTrackPro profile / flight profile
area and send it with every request.
GET /api/v1/auth/me HTTP/1.1
Host: skytrackpro.app
X-Api-Key: <YOUR_API_KEY>
Authorization: Bearer <ACCESS_TOKEN>
GET /api/v1/auth/me?api_key=<YOUR_API_KEY> HTTP/1.1
Host: skytrackpro.app
Authorization: Bearer <ACCESS_TOKEN>
error_code: MISSING_API_KEY (HTTP 401)error_code: INVALID_API_KEY (HTTP 401)
User authentication uses the existing JScms login system and issues opaque access tokens (short-lived) and refresh tokens (long-lived).
POST /auth/logout on sign-out to revoke themPOST /api/v1/auth/loginRequest body:
{
"email": "pilot@example.com",
"password": "secret-password",
"remember": true
}
Example:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: <YOUR_API_KEY>" \
-d '{"email":"pilot@example.com","password":"secret-password","remember":true}' \
"https://skytrackpro.app/api/v1/auth/login"
Success response:
{
"ok": true,
"message": "Login successful.",
"data": {
"token": "64-char-hex-access-token",
"refresh_token": "64-char-hex-refresh-token",
"user": {
"id": 123,
"email": "pilot@example.com"
}
}
}
GET /api/v1/auth/meRequires a Bearer token (or an existing logged-in browser session).
{
"ok": true,
"message": "",
"data": {
"user": {
"id": 123,
"email": "pilot@example.com",
"name": "Pilot Name",
"usergroupid": 5,
"access": 1,
"public": 1
}
}
}
POST /api/v1/auth/refreshRequest body:
{
"refresh_token": "64-char-hex-refresh-token"
}
Success response:
{
"ok": true,
"message": "Token refreshed.",
"data": {
"token": "new-64-char-hex-access-token",
"refresh_token": "64-char-hex-refresh-token",
"user": {
"id": 123,
"email": "pilot@example.com",
"name": "Pilot Name",
"usergroupid": 5,
"access": 1,
"public": 1
}
}
}
POST /api/v1/auth/logout
Revokes the access token in the Authorization header. If a
refresh_token belonging to the same user is supplied, it is revoked
as well. Idempotent: revoking an already-revoked token returns
error_code: TOKEN_REVOKED on the next attempt.
// Request
{
"refresh_token": "optional-64-char-hex-refresh-token"
}
// Response
{
"ok": true,
"message": "Logged out.",
"data": null
}
POST /api/v1/flights/uploadagree or st_agree set to 112 flights from the current calendar yearfile - required .igc filetitle - optionalvisibility - optional, one of private, org, public, unlistedorg_id - optionaltask_id - optionalst_agree or agree - required, must be 1{
"file_base64": "<base64 encoded IGC>",
"file_name": "flight.igc",
"title": "Optional title",
"visibility": "private",
"org_id": 5,
"task_id": 123,
"agree": 1
}
Compatibility fallback: if visibility is omitted, the gateway also accepts
is_public and maps it to public or private.
Success response:
{
"ok": true,
"message": "IGC uploaded and analyzed.",
"data": {
"track_id": 987,
"duplicate": false,
"date": "2025-06-13",
"year": 2025,
"visibility": "private",
"late_upload": false,
"notices": [],
"meta": { "...": "full analysis data" }
}
}
HTTP status is 201 for a new upload and 200 for a duplicate
IGC that already exists in the same user's tracks.
GET /api/v1/flightsRequires a Bearer token. Returns flights visible to the requester.
Optional query parameters:
user_id - filter to a specific user. When this is not the requester, only that user's public flights are returned. Default: requester's own flights.visibility - private | org | public | unlisted. Honored when listing your own flights; ignored (forced to public) when listing another user.from, to - YYYY-MM-DD bounds on date_flownlimit - default 50, max 200offset - default 0order - date_desc (default) or date_asc{
"ok": true,
"message": "",
"data": {
"flights": [
{
"id": 987,
"user_id": 123,
"title": "Saturday Aletsch",
"date_flown": "2025-06-13",
"duration_s": 9450,
"distance_m": 87420,
"longest_xc_m": 73210,
"longest_xc_key": "free3",
"xc_points": 91.45,
"max_alt_m": 3640,
"gain_total_m": 4120,
"avg_speed_kmh": 33.4,
"max_speed_kmh": 56.1,
"launch": { "lat": 46.41, "lon": 8.13 },
"landing": { "lat": 46.27, "lon": 8.04 },
"visibility": "private",
"takeoff_id": 412,
"analysis_status": "done",
"is_competition": false,
"comp_task_id": null,
"created_at": "2025-06-13 18:22:01"
}
],
"count": 1,
"limit": 50,
"offset": 0
}
}
GET /api/v1/flights/{id}Requires a Bearer token.
analysis_status)public or unlisted only, and only when active (else 403 FORBIDDEN or 404 NOT_FOUND)The detail response adds notes, bbox, share_token,
analysis_error, fai_class, wing_hg_type,
tz_offset_min, and updated_at to the list shape above.
The live API stores sessions and positions in the dedicated live-tracking database.
All endpoints (read and write) require a Bearer token.
Read endpoints return only sessions that are public
(privacy_level = 0) or owned by the requester.
POST /api/v1/live/start{
"task_id": 123,
"group_id": 5,
"mode": "freeflight",
"device_id": "device-uuid",
"device_label": "Phone",
"privacy_level": 0,
"client_version": "1.2.3",
"app_version": "1.2.3"
}
privacy_level: 0 = public (visible to anyone authenticated). Any other value = private (only the owner can read it).group_id identifies the session group (e.g. competition, organization)device_id is preferred; if omitted, device_label is used as the stored identifierclient_version is preferred; app_version is accepted as an aliasmode is accepted for compatibility but is not currently stored or used as a server-side filteruser_id + device_id can return the same session_id until the cleanup cron removes that row{
"ok": true,
"message": "Live session created.",
"data": { "session_id": 456 }
}
Success status: 201 for a new session, 200 when an existing session is reused or reactivated.
POST /api/v1/live/stop{
"session_id": 456,
"end_reason": 1
}
Stopping ends the session for now, but the next start on the same user/device may reactivate the same session_id until cleanup archives it.
POST /api/v1/live/tick{
"session_id": 456,
"points": [
{
"ts": 1730784300,
"lat": 46.123456,
"lon": 7.123456,
"alt": 1500,
"speed": 11.2,
"heading": 230,
"accuracy": 8.5,
"h_acc": 8.5,
"source": 2,
"seq": 1
}
]
}
ts is a Unix timestamp in secondsaccuracy is preferred; h_acc is accepted as a compatibility aliassource and seq are optional and preserved in stored positionsGET /api/v1/live/sessionsRequires a Bearer token. Returns only sessions that are public (privacy_level = 0) or owned by the requester.
Optional query parameters: group_id, task_id
GET /api/v1/live/session/{id}/trackRequires a Bearer token. Returns 403 FORBIDDEN when the session is private and the requester is not the owner.
Optional query parameters: limit (default 500, max 5000), since_ts
GET /api/v1/live/session/{id}/tailReturns the latest stored point for a session. Same auth and privacy rules as /track.
GET /api/v1/live/usersRequires a Bearer token. Returns markers for sessions that are public or owned by the requester.
Optional query parameters:
group_idtask_idmode - accepted for compatibility; currently echoed back but not used as a real server-side filtersince_tspublic_only - 0 or 1 (filters by user.public)limit - default 200, max 1000GET /api/v1/tasksOptional query parameters: org_id, category_id, published
This endpoint works with only the API key. Supplying a Bearer token is optional but may affect competition-linked visibility checks.
GET /api/v1/tasks/{id}
Returns full task metadata including defaults, task_board,
spec, and calc.
Competition-linked tasks may return 403 FORBIDDEN when the current viewer does not pass the task access gate.
Failures always include a stable error_code string. Branch on the
code, not on the human-readable message.
| HTTP | error_code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | A required field is missing or malformed |
| 400 | AGREE_REQUIRED | IGC upload was not confirmed with agree=1 |
| 400 | SESSION_INACTIVE | Live session is missing, ended, or owned by someone else |
| 400 | UNSUPPORTED_VERSION | Path uses a version other than v1 |
| 400 / 500 | UPLOAD_FAILED | IGC analyzer rejected the file or threw |
| 401 | MISSING_API_KEY | X-Api-Key header / api_key query missing |
| 401 | INVALID_API_KEY | Key not recognized or marked inactive |
| 401 | UNAUTHENTICATED | Endpoint requires a Bearer token |
| 401 | INVALID_TOKEN | Bearer token not found |
| 401 | TOKEN_REVOKED | Bearer or refresh token was revoked (typically by logout) |
| 401 | TOKEN_EXPIRED | Token is past its expiry |
| 401 | TOKEN_NOT_FOR_CLIENT | Refresh token was issued to a different API client |
| 401 | INVALID_CREDENTIALS | Email / password did not match |
| 401 | LOGIN_FAILED | Internal error during login (rare) |
| 403 | FORBIDDEN | Authenticated, but not allowed (private session, gated task, no track access) |
| 404 | NOT_FOUND | Resource (user, session, flight, task) does not exist or is inactive |
| 404 | UNKNOWN_RESOURCE | Unknown top-level resource in the URL |
| 404 | UNKNOWN_ENDPOINT | Resource exists but the action / method does not |
| 500 | SERVER_ERROR | Unexpected server-side failure |
POST /api/v1/auth/login once and store both data.token and data.refresh_token.X-Api-Key with every request.Authorization: Bearer ... for write endpoints, live read endpoints, and other user-specific calls.error_code: TOKEN_EXPIRED), call POST /api/v1/auth/refresh and retry the original request.POST /api/v1/auth/logout to revoke both tokens, then drop them locally.