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. Leonardo | B. API key | C. Device grant | |
|---|---|---|---|
| Registration on your side | None | None | One-time app registration |
| What the pilot does once | Pastes a device password | Pastes a wf_key_… key | Enters a short code in a browser |
| Client secret to embed? | No | No | No (public client) |
| Credential lifetime | Long-lived (revocable) | Long-lived (revocable) | Short access token + rotating refresh token |
| Per-pilot revocation | Yes (Connected Services) | Yes (developer portal) | Yes (their We-Fly dashboard) |
| Best for | Apps that already speak Leonardo | Quick integration, one pasted key | The 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
| Field | Value |
|---|---|
pass | A We-Fly device password (wf_key_…) — not the account login password. The pilot generates it under Profile → Connected Services → Instrument & cloud sync. |
user | The pilot's account email. Informational only (also scanned as a fallback slot for the key). |
IGCigcIGC | The raw IGC text. (The original pgxc.pl doc abbreviates this field igcIGC; we accept either, but real clients send IGCigcIGC.) |
igcfn | Filename — 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:
| Outcome | Body contains |
|---|---|
| Accepted / duplicate | flight scored + response=<9 chars> |
| Bad credentials | Invalid User account. … (with a hint naming the fix) |
| Bad/oversized IGC, server fault | error: … (no flight scored token) |
| Rate limited | error: 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:writeneeds an approved app. While your OAuth application ispending, only its owner can authorize it. Get it promoted toapprovedbefore 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-datawith afilefield (a.igcpart), or- the raw IGC as the request body with an
X-Filenameheader.
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.