Gaggle Logo
Gaggle Developers

Flights and tracks

Three endpoints give you a pilot’s flights. They need the flights scope; tracks need tracks. What comes back depends on the pilot’s plan: see Permissions and plans.

EndpointReturns
GET /v1/flightsA page of flight log entries, newest flight first
GET /v1/flights/{id}One flight
GET /v1/flights/{id}/track.gpxThe flight’s track as GPX

A flight

{
  "id": "8Vq2kLw0TbXn4YhZ",
  "date": "2026-10-18T09:42:10.000Z",
  "durationMinutes": 96,
  "site": "Bell Hill",
  "source": "recording",
  "hasTrack": true,
  "updatedAt": "2026-10-18T11:31:55.000Z",
  "aircraft": { "name": "Ozone Zeno 2" },
  "takeOffCoordinates": { "lat": -33.9722, "lng": 18.4361 },
  "distanceKm": 23.4,
  "flightType": "PG",
  "weather": { "windSpeed": 4.2, "windDirection": 190 }
}

Flights appear only at the sharing levels the pilot allows your app to see (flightSharing in /v1/me). Imported flights ("source": "import") are private by default in Gaggle, so you only see them when the pilot includes the private level. Manual entries are always visible. distanceKm is rounded to 0.1 km; weather units are in Permissions and plans.

The example above is an Adventurer flight. Every plan gets id, date, durationMinutes, site, source, hasTrack, updatedAt and, when recorded, aircraft, takeOffCoordinates and flightType. Frequent Flyer and Adventurer add distanceKm; Adventurer adds weather. source tells you whether the flight was recorded in Gaggle (recording), imported from a file (import) or typed in by hand (manual). hasTrack says whether there is a GPX track to download; a flight without one is still returned by GET /v1/flights/{id}, and only the track endpoint answers 404 for it.

updatedAt is when Gaggle last changed the flight for you, not when the pilot flew it. It only moves forward, so it is safe to sync on. The date is the take-off time (UTC). For a manual log entry it is the time the pilot typed in, read as UTC.

Which flights you see depends on the plan: Free sees only flights that reached Gaggle after the pilot connected, Frequent Flyer the last 30 days and Adventurer the whole logbook. See Permissions and plans. Tracks are the same on every plan.

Syncing the flight log

GET /v1/flights takes three optional parameters:

ParameterMeaning
updatedSinceOnly entries created, changed or deleted at or after this ISO 8601 time
cursorThe nextCursor of the previous page. Opaque.
limit1 to 100, default 50

Entries are ordered by flight date, newest first, not by updatedAt. updatedSince returns entries whose updatedAt is at or after that time. An invalid limit, updatedSince or cursor answers 400.

First sync. Follow nextCursor until it is null.

Later syncs. Remember the largest updatedAt you have seen, and pass it as updatedSince. You then get only what changed, including deletions. Because of the ordering, take that value only after you have read every page.

async function syncFlights(accessToken, updatedSince) {
  const flights = [];
  let cursor = null;
  let newest = updatedSince;

  for (;;) {
    const url = new URL("https://connect.flygaggle.com/v1/flights");
    url.searchParams.set("limit", "100");
    if (updatedSince) url.searchParams.set("updatedSince", updatedSince);
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after") ?? 5);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue; // retry the same page
    }
    if (!res.ok) throw new Error(`Gaggle answered ${res.status}`);

    const page = await res.json();
    for (const f of page.flights) {
      flights.push(f);
      if (!newest || f.updatedAt > newest) newest = f.updatedAt;
    }
    if (!page.nextCursor) break;
    cursor = page.nextCursor;
  }

  return { flights, nextUpdatedSince: newest };
}

Save nextUpdatedSince only after you have processed every page. If you receive a webhook, you can fetch that one flight instead of syncing everything.

Deleted flights (tombstones)

When a pilot deletes a flight, when it is taken under an event organiser’s control, or when Gaggle stops sharing it with you because its sharing level is no longer one you can see (the flight changed level, or the pilot narrowed the levels you see), it comes back in the list as a tombstone for the next 30 days. Tombstones are in the full listing as well, not only with updatedSince. They follow the same history window as flights, so on Free you never get a tombstone for a flight from before the pilot connected. A tombstone holds only the id: the flight’s details are gone.

{ "id": "2mNf8RcA1QsJz6Uk", "deleted": true, "updatedAt": "2026-10-19T07:02:11.000Z" }

Remove that flight from your copy (ignore it if you never had it). When the pilot narrows the levels you see, the tombstones are timed at that change, so an updatedSince sync finds them. A flight whose sharing level stops being visible to you also sends a flight.deleted webhook. If you may be offline for longer than 30 days, do a full sync instead of an incremental one. A flight that stops being visible only because the history window moved on (a plan change, a Frequent Flyer flight getting older than 30 days, or the pilot correcting a flight’s date to before the window) gets no tombstone: a full sync shows the current picture.

Flights can leave or enter the window

When a pilot’s plan changes, or they change the sharing levels you see or the flights scope, the set of flights you can see changes. A connection.updated webhook tells you; run a full sync to pick up the new picture.

Downloading a track

curl -s https://connect.flygaggle.com/v1/flights/8Vq2kLw0TbXn4YhZ/track.gpx \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept-Encoding: gzip" --compressed \
  -o flight.gpx

The response is application/gpx+xml, a plain GPX 1.1 file with one track segment and every recorded point. It is gzip-compressed on the wire; most HTTP clients decompress it for you. It is the same on every plan.

404 means the flight has no track (hasTrack is false), was deleted, is outside the history window, or is not shared. If Gaggle finds the flight has no track after all, you get 404 and hasTrack becomes false. The file is sent as an attachment named {id}.gpx.

The very first download of a flight’s track takes a moment longer than later ones, and first downloads are throttled so that a backfill cannot flood Gaggle. If you ask for many tracks Gaggle has not served before, you get 429 with Retry-After: wait, then carry on. Tracks that were downloaded before come back quickly and do not count towards this. A 502 with bridge_unavailable means the track could not be fetched this time; retry shortly.

What is in each point

The file has the flight date in <metadata> and the take-off site as the track name. Every point has lat, lon, <ele> (altitude in metres, left out when it was not recorded) and <time> (UTC), exactly as recorded on the device or in the imported file. Nothing else is included.

Compute the rest yourself from the points: speed and heading from the distance and bearing between consecutive points, climb rate from the altitude change over time, and so on.

<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Gaggle" xmlns="http://www.topografix.com/GPX/1/1">
<metadata><time>2026-10-18T09:42:10.000Z</time></metadata>
<trk><name>Bell Hill</name><trkseg>
<trkpt lat="-33.9722" lon="18.4361"><ele>412.3</ele><time>2026-10-18T09:42:10Z</time></trkpt>
<trkpt lat="-33.9725" lon="18.4366"><ele>408.9</ele><time>2026-10-18T09:42:11Z</time></trkpt>
</trkseg></trk>
</gpx>

Caching

Every response carries an ETag. Send it back as If-None-Match, exactly as received, and an unchanged flight or track answers 304 with no body. A track’s ETag also changes when the flight’s date or site changes. See Rate limits and errors.