All paths are relative to https://we-fly.cloud.
Authentication: every /api/v1/* endpoint requires
Authorization: Bearer <token> where the token is an OAuth access
token (wf_at_…) or an API key (wf_key_…).
OAuth endpoints
POST /api/oauth/token
Mint or refresh tokens. Grant types: client_credentials,
authorization_code, refresh_token. See
Server-to-server and
User authorization.
Content-Type: application/x-www-form-urlencoded (recommended) or
application/json.
Client auth: HTTP Basic (Authorization: Basic …) or body params
(client_id, client_secret). For public clients on
authorization_code, client_id alone is acceptable.
POST /api/oauth/revoke
Revoke an access or refresh token. RFC 7009.
Body:
token(required) — the access or refresh tokentoken_type_hint(optional) —access_tokenorrefresh_token
Returns 200 OK regardless of whether the token was valid (per RFC).
POST /api/oauth/introspect
Check whether a token is active. RFC 7662. Only available to confidential clients introspecting tokens they themselves issued.
Body:
token(required)token_type_hint(optional)
Returns either { "active": false } or:
{
"active": true,
"scope": "flights:read profile:read",
"client_id": "wf_app_…",
"token_type": "access_token",
"sub": "65a1…",
"exp": 1748426400,
"iat": 1748422800
}
POST /api/oauth/device_authorization
Start an RFC 8628 device flow. Returns device_code, user_code,
verification_uri, and polling instructions. See
Device authorization grant.
Body:
client_id(required, or via HTTP Basic)client_secret(required for confidential, or via HTTP Basic)scope(optional)
GET /.well-known/oauth-authorization-server
RFC 8414 metadata document. Use it to discover endpoint paths instead of hardcoding.
GET /oauth/authorize
Browser-only. The OAuth consent screen. Parameters per User authorization.
API v1 endpoints
GET /api/v1
Public discovery shim. Lists endpoint paths.
GET /api/v1/flights
List the authenticated user's flights.
Scope: flights:read
Query parameters:
| Name | Default | Notes |
|---|---|---|
page | 1 | 1-indexed |
pageSize | 25 | Max 100 |
Response:
{
"data": [
{
"id": "65a4f1bcdeadbeef12345678",
"hash": "f4b9a8c…",
"flight_date": "2026-05-15",
"date_created": "2026-05-15T18:42:11.000Z",
"original_file_name": "2026-05-15-XCT-001.igc",
"is_tandem": false,
"launch_type": "Self",
"season": 2026,
"scoring": {
"distance": 87.3,
"score": 130.95,
"type": "FAITriangle"
},
"flightInfo": {
"takeoff": {
"id": "65aaf0…",
"lat": 46.123,
"lng": 6.456,
"name": "Plaine Joux",
"country": "FR"
},
"metadata": {
"glider-type": "Ozone Zeolite GT"
},
"track": {
"duration": 14820,
"distance": 105230
}
}
}
],
"pagination": {
"page": 1,
"pageSize": 25,
"totalCount": 142,
"totalPages": 6
}
}
Field reference:
id— flight's MongoDB_idhex. Use with the detail endpoint.scoring.distance— XContest scored distance, kmscoring.score— XContest score (points)scoring.type— one ofFAITriangle,FlatTriangle,FreeFlight,OutAndReturnflightInfo.track.duration— flight duration, secondsflightInfo.track.distance— track length (sum of GPS-fix distances), metreslaunch_type—Self,Towed,Powered, orDriving
GET /api/v1/flights/{id}
Detail view of a single flight.
Scope: flights:read
Path: id is the flight's _id hex (24 chars).
Response: same shape as the list rows but with additional fields under
flightInfo.track.analysis:
thermals.summary— count, mean climb rate, ceiling, etc.phases— thermal/glide/cruise breakdownwind— estimated wind direction and speedceiling— top-of-climb statistics
Returns 404 not_found if the flight doesn't exist or belongs to
another user.
POST /api/v1/flights/upload
Upload an IGC file to the authenticated user's logbook.
Scope: flights:write
Accepts either:
multipart/form-datawith afilefield, ORapplication/octet-streamwith the raw IGC bytes; filename via theX-Filenamerequest header.
Max body: 3 MB (returns 413 payload_too_large otherwise).
The IGC must contain at least 10 B-records (400 invalid_request
otherwise).
Response:
{
"data": {
"hash": "f4b9a8c…",
"fileName": "2026-05-15-XCT-001.igc",
"isNew": true
}
}
isNew: true→201 Created. Flight was parsed and added.isNew: false→200 OK. The GPS-fix hash matches an existing flight; nothing changed.
DELETE /api/v1/flights/{id}
Permanently delete a flight (DB row + IGC file + cached gzipped analysis).
Scope: flights:delete
Response: 204 No Content on success, 404 not_found otherwise.
Pending applications cannot use this endpoint regardless of scope — destructive endpoints require an approved app.
GET /api/v1/profile
The authenticated user's profile.
Scope: profile:read. The email field additionally requires the
profile:email scope — without it, email is returned as null.
Response:
{
"data": {
"id": "65a1…",
"name": "Alice",
"email": "alice@example.com",
"image": "/api/profile-picture/…",
"publicProfile": true,
"emailVerified": true,
"earlyAdopter": false,
"club": "Parapente Chamonix",
"civlId": 12345
}
}
Developer-portal endpoints
The /developer dashboard is backed by internal, cookie-authenticated
endpoints (and React Server Actions for mutations). These are not
part of the partner contract, are undocumented by design, and may
change without notice. Drive the dashboard through its UI; use the
OAuth and /api/v1/* endpoints above for everything programmatic.