Use this flow when you control both ends: your own backend pushing or pulling data for your own we-fly account. The classic example is a flight-tracker (XContest, WeGlide, SkyLines) that imports IGC files into we-fly on a schedule.
For "let me act on behalf of any user who installs my app", see User authorization instead — client credentials cannot impersonate arbitrary users.
Registering the application
- Sign in to https://we-fly.cloud as the pilot whose data the integration will manage.
- Open https://we-fly.cloud/developer.
- Click New application and fill out:
- Name — shown in audit logs (
"XContest backfill"). - Description — one sentence; appears in the consent screen (irrelevant for this flow but recommended for clarity).
- Client type —
confidential. - Redirect URIs — required field, even though client credentials
doesn't use them. Use a placeholder like
https://example.invalid/unused. - Grant types — tick
client_credentials. - Allowed scopes — pick the minimum your integration needs.
- Name — shown in audit logs (
- Copy the Client ID (
wf_app_…) and Client secret (wf_secret_…). The secret is shown once; store it in a dedicated secret store on your side.
New applications start in
pendingstatus. While pending you can only mint tokens for read-only scopes against your own account. Ask an admin to promote the app toapprovedonce you've validated the integration locally.
Exchanging credentials for an access token
curl -X POST https://we-fly.cloud/api/oauth/token \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
-d "grant_type=client_credentials" \
-d "scope=flights:write"
Response (example):
{
"access_token": "wf_at_3b8…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "flights:write"
}
Notes:
- Access tokens last 1 hour. There is no refresh token on this
flow — the access token is cheap to re-mint with the same
client_id/client_secret. scopeis optional. Omitted → the token carries every scope your app is registered for. Supplying a narrowerscopereduces blast radius if the token leaks.- Authentication may also be passed as form fields if your HTTP client
cannot do Basic auth (
-d "client_id=…" -d "client_secret=…"). HTTP Basic is recommended.
Calling the API
curl -X POST https://we-fly.cloud/api/v1/flights/upload \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/octet-stream" \
-H "X-Filename: 2026-05-15-XCT-001.igc" \
--data-binary "@2026-05-15-XCT-001.igc"
Or with multipart:
curl -X POST https://we-fly.cloud/api/v1/flights/upload \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-F "file=@2026-05-15-XCT-001.igc"
A successful upload returns 201 Created with:
{
"data": {
"hash": "f4b9…",
"fileName": "2026-05-15-XCT-001.igc",
"isNew": true
}
}
we-fly de-duplicates by GPS-fix hash. Re-uploading the same flight is
idempotent and returns 200 OK with isNew: false.
Reading data
curl https://we-fly.cloud/api/v1/flights \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
{
"data": [
{
"id": "65a4f1…",
"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": "65aa…",
"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
}
}
Query parameters:
| Parameter | Default | Notes |
|---|---|---|
page | 1 | 1-indexed |
pageSize | 25 | Maximum 100 |
The full schema of data[] is in the endpoint reference.
Reference implementation (Node.js, fetch)
import { readFile } from "node:fs/promises";
const TOKEN_URL = "https://we-fly.cloud/api/oauth/token";
const API_BASE = "https://we-fly.cloud/api/v1";
let cachedToken; // { value, expiresAt }
async function getToken() {
if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) {
return cachedToken.value;
}
const basic = Buffer.from(
`${process.env.WEFLY_CLIENT_ID}:${process.env.WEFLY_CLIENT_SECRET}`,
).toString("base64");
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": "MyIntegration/1.0",
},
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "flights:write flights:read",
}),
});
if (!res.ok) throw new Error(`token exchange failed: ${res.status}`);
const json = await res.json();
cachedToken = {
value: json.access_token,
expiresAt: Date.now() + json.expires_in * 1000,
};
return cachedToken.value;
}
export async function uploadIgc(path) {
const igc = await readFile(path);
const res = await fetch(`${API_BASE}/flights/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/octet-stream",
"X-Filename": path.split("/").pop(),
"User-Agent": "MyIntegration/1.0",
},
body: igc,
});
if (res.status === 429) {
const retry = Number(res.headers.get("Retry-After") ?? 60);
await new Promise(r => setTimeout(r, retry * 1000));
return uploadIgc(path);
}
if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(`upload failed: ${res.status} ${JSON.stringify(error)}`);
}
return res.json();
}
Reference implementation (Python, requests)
import os
import time
import requests
from base64 import b64encode
TOKEN_URL = "https://we-fly.cloud/api/oauth/token"
API_BASE = "https://we-fly.cloud/api/v1"
UA = "MyIntegration/1.0"
_token = {"value": None, "expires_at": 0}
def get_token():
if _token["value"] and _token["expires_at"] > time.time() + 60:
return _token["value"]
basic = b64encode(
f"{os.environ['WEFLY_CLIENT_ID']}:{os.environ['WEFLY_CLIENT_SECRET']}".encode()
).decode()
r = requests.post(
TOKEN_URL,
headers={
"Authorization": f"Basic {basic}",
"Accept": "application/json",
"User-Agent": UA,
},
data={"grant_type": "client_credentials", "scope": "flights:write flights:read"},
timeout=15,
)
r.raise_for_status()
body = r.json()
_token["value"] = body["access_token"]
_token["expires_at"] = time.time() + body["expires_in"]
return _token["value"]
def upload_igc(path: str) -> dict:
with open(path, "rb") as fh:
data = fh.read()
for _ in range(3):
r = requests.post(
f"{API_BASE}/flights/upload",
headers={
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/octet-stream",
"X-Filename": os.path.basename(path),
"User-Agent": UA,
},
data=data,
timeout=60,
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
r.raise_for_status()
return r.json()
raise RuntimeError("upload retries exhausted")
What you must implement on your side
- Token caching. Re-use the access token until
expires_inelapses. Do not mint a fresh token per request. - Backoff on 429. Honour
Retry-After(seconds). - Secret rotation. Store the client_secret in a secret manager. To
rotate, visit
/developer, click Rotate secret, deploy the new value, then immediately invalidate any cached token (it was minted under the old secret; refreshing is fine, but new tokens will need the new credentials). - TLS verification. Don't disable certificate checks. we-fly.cloud uses Let's Encrypt with strict OCSP.
User-Agent. See overview.