Zum Hauptinhalt springen
Entwickler
Referenz · 5 Min. Lesezeit

Server-to-server integration (Client Credentials)

The client-credentials flow: your own backend syncing data for your own we-fly account.

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

  1. Sign in to https://we-fly.cloud as the pilot whose data the integration will manage.
  2. Open https://we-fly.cloud/developer.
  3. 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.
  4. 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 pending status. While pending you can only mint tokens for read-only scopes against your own account. Ask an admin to promote the app to approved once 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.
  • scope is optional. Omitted → the token carries every scope your app is registered for. Supplying a narrower scope reduces 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:

ParameterDefaultNotes
page11-indexed
pageSize25Maximum 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_in elapses. 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.