Skip to main content
reference 7 min read

Endpoint reference

Every OAuth and /api/v1 endpoint — methods, scopes, request and response shapes.

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 token
  • token_type_hint (optional) — access_token or refresh_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

Two read modes. ?since is the only switch between them; without it the behaviour and response are exactly as they have always been.

Page-based?page (default 1, 1-indexed), ?pageSize (default 25, max 100). Sorted flight_date descending.

Delta?since=<cursor|0|now>, ?limit (default 100, max 500). Returns only what changed since a cursor, carries deletions, and answers 304 with an empty body when nothing has. Adds ETag / If-None-Match, and the statuses 400 invalid_cursor and 410 cursor_expired. Full contract: Incremental sync.

Page-based 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 _id hex. Use with the detail endpoint.
  • scoring.distance — XContest scored distance, km
  • scoring.score — XContest score (points)
  • scoring.type — one of FAITriangle, FlatTriangle, FreeFlight, OutAndReturn
  • flightInfo.track.duration — flight duration, seconds
  • flightInfo.track.distance — track length (sum of GPS-fix distances), metres
  • launch_typeSelf, Towed, Powered, or Driving

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 breakdown
  • wind — estimated wind direction and speed
  • ceiling — 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-data with a file field, OR
  • application/octet-stream with the raw IGC bytes; filename via the X-Filename request 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: true201 Created. Flight was parsed and added.
  • isNew: false200 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
  }
}

Instrument endpoints

Full guide with rationale, request bodies and the conflict rules: Instrument integration. Reference only here.

MethodPathScopeNotes
GET/api/v1/taskstasks:readDelta with ?since; shared-library search without it
POST/api/v1/taskstasks:write{ content, filename?, name?, official? }; deduped on geometry, data is always an array
GET/api/v1/tasks/{id}tasks:readOne-shot; ?format=xctsk or ?format=code. Refuses ?since
GET/api/v1/waypointstasks:readDelta with ?since (default limit 10, max 50); summary listing without it
POST/api/v1/waypointstasks:write{ name, waypoints[] } or { name, content, filename }
GET/api/v1/waypoints/{id}tasks:readPoints decompressed; owner-or-public
PUT/api/v1/waypoints/{id}tasks:writeLast-writer-wins over the whole point list
DELETE/api/v1/waypoints/{id}tasks:write204
GET/api/v1/sitescommunity:readDelta with ?since (global stream); ?bbox / ?near / ?country / ?q without it
GET/api/v1/screensscreens:readDelta with ?since
GET/api/v1/screens/{id}screens:readETag, honours If-None-Match
PUT/api/v1/screens/{id}screens:writeUpsert with a client-chosen id; If-Match drives the conflict fork
DELETE/api/v1/screens/{id}screens:write204
POST/api/v1/screens/{id}/publishscreens:write{ published }; does not bump revision
GET/api/v1/screens/gallerycommunity:readPublished layouts, ?q, paged
POST/api/v1/live/positionslive:write202; ≤500 fixes; partly-bad batches accepted
GET/api/v1/live/buddiescommunity:readNewest fix per followed public pilot, last 30 min
POST/api/v1/live/sharelive:write{ label?, ttlSeconds? }; always expires
GET/api/v1/live/sharelive:writeThe pilot's active links
DELETE/api/v1/live/share/{token}live:writeRevoke early
GET/live/{token}publicThe page family opens. No auth, noindex
GET/api/v1/contactsprofile:readAddresses masked
POST/api/v1/contactsprofile:writeMax 10; sends a confirmation email
DELETE/api/v1/contacts/{id}profile:write204
POST/api/v1/notify/takeofflive:write202; confirmed contacts only
POST/api/v1/notify/landinglive:write202; confirmed contacts only
POST/api/v1/notify/emergencylive:write202; retry hard, idempotent on eventId; reaches unconfirmed contacts too
GET/api/v1/thermalscommunity:read?bbox required. ?since is an ISO timestamp, not a cursor
GET/api/v1/airspace/regionsairspace:readTile source descriptors + snapshot
GET/api/v1/airspace/activationsairspace:read?bbox required. Per-pilot autorouter credentials; source.available says whether we could look

Three of these behave differently enough to be worth restating:

  • /api/v1/thermals?since= is a build-time freshness filter (an ISO 8601 timestamp — the meta.builtAt from your last response), not a sync cursor. The resource is filtered by bbox, and the delta contract takes no filters.
  • PUT /api/v1/screens/{id} never returns 412. A stale If-Match still writes, and the displaced revision is preserved as a conflict copy named in conflictCopyId.
  • /api/v1/airspace/activations returns 200 with source.available: false when the pilot has not connected autorouter — an empty data array on its own would read as "no active airspace here".

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.