Gaggle Logo
Gaggle Developers

Authorization

Gaggle uses the OAuth 2.1 authorization code flow with PKCE. You send the pilot to Gaggle, the pilot signs in and approves access in the Gaggle app, and Gaggle sends them back to you with a one-time code.

Everything runs on https://connect.flygaggle.com. You need a client id, a client secret and at least one registered redirect URI. Providers are registered by Gaggle; see how to request access.

EndpointPurpose
GET /oauth/authorizeStart the flow in the pilot’s browser
POST /oauth/tokenExchange a code, or a refresh token, for tokens
POST /oauth/revokeDisconnect a pilot
GET /.well-known/oauth-authorization-serverDiscovery document for OAuth libraries

Scopes

Ask for the scopes you need. The pilot can switch any of them off except profile.

ScopeLets you
profileKnow who the pilot is (an id that is unique to your app, and their display name). Always granted.
statsRead flight and airtime totals
flightsRead the flight log and receive flight webhooks
tracksDownload flight tracks as GPX
offline_accessAccepted for OAuth clients that expect it. You get a refresh token either way. Never shown to the pilot.

Every scope works on every plan. The plan decides how much detail comes back; see Permissions and plans.

1. Send the pilot to Gaggle

Create a random code_verifier, hash it into a code_challenge (S256), and a random state. Keep the verifier and the state with the pilot’s session on your side.

import { randomBytes, createHash } from "node:crypto";

const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("base64url");

const url = new URL("https://connect.flygaggle.com/oauth/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: process.env.GAGGLE_CLIENT_ID,
  redirect_uri: "https://example.com/gaggle/callback",
  scope: "profile stats flights tracks",
  state,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
}).toString();

// Store { codeVerifier, state } in the pilot's session, then redirect to url.toString()

Gaggle opens the Gaggle app (or the web app) where the pilot signs in, sees what you ask for, and switches individual permissions on or off and chooses which sharing levels of their flights you see. Pilots need a real Gaggle account; anonymous accounts cannot connect. state and PKCE with S256 are required, and you can only ask for the scopes your registration allows. A redirect_uri that is not registered, or an unknown client, gets a plain 400 page instead of a redirect.

2. Receive the code

On approval the pilot lands on your redirect URI:

https://example.com/gaggle/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=...

The redirect also carries iss (https://connect.flygaggle.com). Check that state matches before you use the code.

If something is wrong you get ?error=...&state=... instead:

errorMeaning
access_deniedThe pilot declined. Or Gaggle has switched your app off (error_description=provider_disabled).
invalid_requestPKCE with S256, or state, is missing
invalid_scopeYou asked for a scope that does not exist, or that your registration does not allow

Some Gaggle accounts cannot connect apps (for example accounts owned by an event), and an app that has reached its limit of connected pilots cannot take new ones. In both cases the pilot sees an error in Gaggle and you receive no redirect at all, so treat a sign-in that never returns as abandoned.

3. Exchange the code

Authenticate as your client with HTTP Basic, or send client_id and client_secret in the body.

curl -s https://connect.flygaggle.com/oauth/token \
  -u "$GAGGLE_CLIENT_ID:$GAGGLE_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri="https://example.com/gaggle/callback" \
  -d code_verifier="$CODE_VERIFIER"
const res = await fetch("https://connect.flygaggle.com/oauth/token", {
  method: "POST",
  headers: {
    Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"),
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://example.com/gaggle/callback",
    code_verifier: codeVerifier,
  }),
});
const tokens = await res.json();
{
  "access_token": "3f9c1b7e2a8d4c6f_example_access_token",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "ZWYyYTAxYjMtMzM0ZC00ZGY4_example_refresh_token",
  "scope": "profile stats flights tracks",
  "resource": "https://connect.flygaggle.com/v1"
}

Treat the token strings as opaque. Store the refresh token securely; it is the long-lived credential. You always get a refresh token; offline_access is accepted as a scope but changes nothing. A failed client login answers 401 (invalid_client), and a bad or used code answers 400 (invalid_grant).

4. Read what the pilot allows

The scope in the token response is only the upper bound of what you asked for. The pilot may have switched some scopes off, and can change them later without reconnecting. Call GET /v1/me to see what you can use right now.

curl -s https://connect.flygaggle.com/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "id": "7c1f0a5e9b3d4e28a6f1c0d2e8b94a37",
  "displayName": "Sam Pilot",
  "scopes": ["profile", "stats", "flights", "tracks"],
  "plan": "frequentFlyer",
  "historyFrom": "2026-09-21T00:00:00.000Z",
  "flightSharing": ["everyone", "groups", "friends"]
}
  • id is unique to your app. The same pilot has a different id in every provider, and it is not their Gaggle account id.
  • displayName is the pilot’s Gaggle name as it was when they connected your app.
  • scopes are the scopes enabled now You get a 403 insufficient_scope if you call an endpoint outside them.
  • plan is free, frequentFlyer or adventurer.
  • historyFrom is where the flight history starts: 30 days back on Frequent Flyer (the start of a UTC day), and on Free the exact moment the pilot connected (Free has no history). It is null on Adventurer, which has the whole logbook.
  • flightSharing lists the sharing levels of the pilot’s flights you can see: a subset of everyone, groups, friends and private (default: everyone, groups and friends). Manual log entries are always visible.

Read /v1/me after connecting and whenever you receive a connection.updated webhook. See Permissions and plans.

5. Refresh the token

Access tokens last 1 hour. Refresh before they expire, or when a request answers 401 invalid_token.

curl -s https://connect.flygaggle.com/oauth/token \
  -u "$GAGGLE_CLIENT_ID:$GAGGLE_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"

Refresh tokens rotate: the response contains a new refresh token. Save it and discard the old one (the old one keeps working only until the new one is first used). A refresh token that goes unused for 90 days expires and the connection is removed, so a provider that keeps syncing stays connected and an abandoned connection lapses. A daily check removes expired connections within a few days, and you receive connection.revoked when it does. If a refresh fails with invalid_grant, the pilot has disconnected or the grant has expired; ask them to connect again.

6. Disconnect

To disconnect a pilot from your side, revoke their token. Gaggle removes the connection, the pilot sees it disappear from Settings, Integrations, Connected apps in the Gaggle app, and Gaggle stops sending you data.

curl -s https://connect.flygaggle.com/oauth/revoke \
  -u "$GAGGLE_CLIENT_ID:$GAGGLE_CLIENT_SECRET" \
  -d token="$REFRESH_TOKEN" \
  -d token_type_hint=refresh_token

Revoking the access token or the refresh token both end the whole connection. The call answers 200 even if the token is already invalid; a missing token is a 400. You also receive a connection.revoked webhook.

Pilots can also disconnect you at any time from the Gaggle app. You then receive a connection.revoked webhook, and your tokens stop working (401). Treat that as the end of the connection and delete the pilot’s Gaggle data from your systems as your own terms and the pilot’s expectations require.