Skip to main content
reference 5 min read

Automated upload from a native app

Upload IGC flights straight from an iOS/Android app — Leonardo, personal API key, or the OAuth device flow.

If you build a native mobile or desktop app and want it to push a pilot's IGC flights into their We-Fly logbook automatically (say, right after they land), this page is your map. It compares the three supported paths and shows a complete request for each. All of them end in silent background uploads after a one-time setup — there's no in-app browser at upload time.

Not building a native app? For a personal script use personal API keys; for a website or server integration use Authorization Code + PKCE or server-to-server.

Which path?

A. LeonardoB. API keyC. Device grant
Registration on your sideNoneNoneOne-time app registration
What the pilot does oncePastes a device passwordPastes a wf_key_… keyEnters a short code in a browser
Client secret to embed?NoNoNo (public client)
Credential lifetimeLong-lived (revocable)Long-lived (revocable)Short access token + rotating refresh token
Per-pilot revocationYes (Connected Services)Yes (developer portal)Yes (their We-Fly dashboard)
Best forApps that already speak LeonardoQuick integration, one pasted keyThe cleanest long-term "link once, upload forever"

All three deposit the flight through the same ingestion pipeline as a website upload — scoring, mapping, thermal analysis, and content-based dedup all apply, so re-uploading a flight is always safe.


A. Leonardo direct upload

We-Fly implements the classic Leonardo flight_submit.php protocol, so an app that already supports "upload to a Leonardo server" (e.g. Flyskyhy) works with zero new code — it just needs the right URL and a device password.

Endpoint

POST https://we-fly.cloud/api/leonardo/flight_submit.php
(alias: https://we-fly.cloud/modules/leonardo/flight_submit.php)
Content-Type: multipart/form-data  (or application/x-www-form-urlencoded)

Fields

FieldValue
passA We-Fly device password (wf_key_…) — not the account login password. The pilot generates it under Profile → Connected Services → Instrument & cloud sync.
userThe pilot's account email. Informational only (also scanned as a fallback slot for the key).
IGCigcIGCThe raw IGC text. (The original pgxc.pl doc abbreviates this field igcIGC; we accept either, but real clients send IGCigcIGC.)
igcfnFilename — sanitised, .igc appended.

Other Leonardo fields (glider, Category, startType, turnpoints, …) are accepted and ignored — We-Fly re-scores from the IGC header.

Complete request

curl -X POST https://we-fly.cloud/api/leonardo/flight_submit.php \
  -F "user=pilot@example.com" \
  -F "pass=wf_key_1a2b3c…" \
  -F "igcfn=2026-07-23-flight.igc" \
  -F "IGCigcIGC=<raw IGC file contents>"

Response contract — Leonardo signals the outcome in the body, not the HTTP status, so every business outcome answers 200:

OutcomeBody contains
Accepted / duplicateflight scored + response=<9 chars>
Bad credentialsInvalid User account. … (with a hint naming the fix)
Bad/oversized IGC, server faulterror: … (no flight scored token)
Rate limitederror: rate limited …

A duplicate re-upload is reported as success (same flight scored token) so the app stops retrying. Limits: 3 MB max, ≥10 B-records.

The #1 mistake: sending the account login password instead of a wf_key_… device password. The login password is rejected by design (pilots may sign in via Google/Facebook/GitHub and have no password at all), so uploads always authenticate with a device password. The failure body now says exactly this.


B. Personal API key

A We-Fly device/personal key (wf_key_…) is a complete bearer credential — there's no client_id, no app registration, and no token exchange. If the key carries flights:write, present it directly:

curl -X POST https://we-fly.cloud/api/v1/flights/upload \
  -H "Authorization: Bearer wf_key_1a2b3c…" \
  -F "file=@2026-07-23-flight.igc"
# → 201 Created (new) or 200 OK (duplicate)
# → { "data": { "hash": "…", "fileName": "…", "isNew": true } }

The pilot generates the key once (Profile → Connected Services, or the developer portal) and pastes it into your app. See personal API keys for details. This is the same credential the Leonardo path uses — Option A is just this key spoken over the Leonardo wire format.


C. OAuth device grant

The cleanest long-term path for a shipped app: the pilot never pastes a key. They enter a short code in a browser once; your app then holds a rotating refresh token and uploads silently thereafter. Register as a public client — no client secret embedded in the app. Full details in device authorization grant; the shape:

1. Request a device + user code

curl -X POST https://we-fly.cloud/api/oauth/device_authorization \
  -d "client_id=${CLIENT_ID}" \
  -d "scope=flights:write"
# → { "device_code": "wf_dc_…", "user_code": "ABCD-1234",
#     "verification_uri": "https://we-fly.cloud/oauth/device",
#     "verification_uri_complete": "…?user_code=ABCD-1234",
#     "expires_in": 600, "interval": 5 }

2. Show the pilot ABCD-1234 and open verification_uri (or a QR of verification_uri_complete). They log in on We-Fly and tap Authorize.

3. Poll the token endpoint every interval seconds until authorized:

curl -X POST https://we-fly.cloud/api/oauth/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  -d "device_code=wf_dc_…" \
  -d "client_id=${CLIENT_ID}"
# pending → HTTP 400 { "error": "authorization_pending" }
# done    → { "access_token": "wf_at_…", "refresh_token": "wf_rt_…", … }

4. Upload with the access token (same endpoint as Option B):

curl -X POST https://we-fly.cloud/api/v1/flights/upload \
  -H "Authorization: Bearer wf_at_…" \
  -F "file=@2026-07-23-flight.igc"

5. Refresh when the access token expires. A stale token returns 401 invalid_token; refresh once and retry. Refresh tokens are single-use and rotate — persist the new one each time. See the lifecycle section for the full error-handling contract.

flights:write needs an approved app. While your OAuth application is pending, only its owner can authorize it. Get it promoted to approved before onboarding other pilots for upload.


The upload endpoint

Options B and C both target POST /api/v1/flights/upload with a Bearer token and flights:write scope. It accepts either:

  • multipart/form-data with a file field (a .igc part), or
  • the raw IGC as the request body with an X-Filename header.

Responses: 201 (new flight) / 200 (duplicate), 400 (bad or unparseable IGC / < 10 B-records), 401 / 403 (auth), 413 (over the size cap). See the endpoint reference and errors.