--- name: opensky-flights-scraper description: Take a snapshot of live aircraft state vectors via the Apify Actor arman-bd/opensky-flights-scraper. Returns one row per aircraft currently transmitting inside a bounding box or matching a list of ICAO24 addresses, with callsign, origin country, latitude and longitude, barometric and geometric altitude, ground speed, track, vertical rate, squawk, on-ground flag and position source, all in SI units and all sharing one snapshot timestamp. Use when a task needs regional traffic monitoring on a schedule, airport apron and taxiway analytics, fleet tracking of named airframes, or a live feed for a map or dashboard. Not for historical flights, arrival and departure boards, flight numbers, routes, schedules, aircraft registrations or airline ownership. --- # OpenSky Flight Scraper Apify Actor `arman-bd/opensky-flights-scraper`. Give it a bounding box, a list of ICAO24 addresses, or both, and get one dataset record per aircraft state in a single coherent snapshot. It runs without credentials; an OpenSky OAuth2 client pair is optional and raises the daily credit allowance. ## When to use it - Regional traffic monitoring: one box, one run every few minutes, building a time series out of successive snapshots. - Airport apron and taxiway analytics: a tight box plus `onGroundOnly` gives you taxiing and parked aircraft. - Fleet tracking: pass the ICAO24 addresses of specific airframes and follow them. - Feeding a live map or dashboard with a current instant on demand. - Sampling altitude, speed and vertical-rate distributions over a corridor for research. ## When not to use it - Historical flights or past tracks. This is a live snapshot only; there is no time parameter. - Arrival and departure boards, flight numbers, routes, schedules or gate information. A `callsign` is not a flight number and there is no origin or destination field. - Aircraft registration, type, operator or ownership. `icao24` identifies the transponder, and you would need a separate registry to resolve it. - Continuous streaming. Each run is one instant; a schedule of runs is the closest thing available. - Aircraft that are not transmitting, or airspace with no receiver coverage. Absence from the result is not evidence of absence from the sky. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/opensky-flights-scraper').call({ boundingBox: { lamin: 49.9, lomin: -6.4, lamax: 55.8, lomax: 1.8 }, onGroundOnly: false, maxResults: 500, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); if (items.length === 0) console.log('credits left:', summary.creditsRemaining); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~opensky-flights-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"boundingBox":{"lamin":45,"lomin":5,"lamax":48,"lomax":10},"maxResults":100}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/opensky-flights-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `boundingBox` | object | no | none | `{ lamin, lomin, lamax, lomax }` in WGS84 degrees. Validated before any request: an inverted or out-of-range box throws. Omit it and you query the whole world. | | `icao24` | string[] | no | `[]` | Six-character hex transponder addresses. Normalised to lower case, deduplicated, batched 50 per request. Combines with `boundingBox` when both are set. | | `onGroundOnly` | boolean | no | `false` | Keep only aircraft the network reports on a surface. Off returns airborne and grounded alike. | | `maxResults` | integer | no | `500` | Cap on states saved, 1 to 20000, applied after filtering. | | `clientId` | string | no | `""` | OpenSky OAuth2 client ID. Secret field. | | `clientSecret` | string | no | `""` | Paired secret. Both must be present or the run stays anonymous. Secret field. | **Box area is the whole cost model.** The source meters this in credits, and the price depends on how much sky you ask for: a box of 25 square degrees or less costs 1, up to 100 square degrees costs 2, up to 400 costs 3, and anything larger, including the worldwide query, costs 4. An anonymous run draws on a 400-credit daily pool attached to the source IP, which on Apify is shared, so the balance you actually get may already be partly spent. Supplying `clientId` and `clientSecret` raises that to 4,000 a day and the snapshot resolution from 10 seconds to 5. A run every five minutes over one small box is about 288 credits a day, which only fits anonymously if nothing else is drawing on the same address. `maxResults` does not reduce the credit cost, because the box is priced before anything is filtered. Shrink the box, not the cap. ## Output One record per aircraft state. Units are SI throughout: metres, metres per second, degrees. Nothing is converted to feet or knots. | Field | Type | Notes | |---|---|---| | `icao24` | string | 24-bit transponder address, lower-case hex. Always present, and the only reliable key. | | `callsign` | string \| null | Callsign as broadcast, trimmed. `null` when the aircraft transmits none, which is common for general aviation and on the ground. | | `originCountry` | string \| null | Country the transponder address block is registered to. This is not the flight's origin airport. | | `timePosition` | number \| null | Unix seconds of the position fix. | | `timePositionIso` | string \| null | The same moment as ISO 8601. | | `lastContact` | number \| null | Unix seconds of the last message of any kind from this aircraft. | | `lastContactIso` | string \| null | The same moment as ISO 8601. Compare with `timePosition` to spot a stale position. | | `longitude` | number \| null | WGS84 decimal degrees. | | `latitude` | number \| null | WGS84 decimal degrees. | | `baroAltitude` | number \| null | Barometric altitude in metres. Usually `null` on the ground. | | `geoAltitude` | number \| null | Geometric altitude in metres. Differs from `baroAltitude` by the local pressure error. | | `onGround` | boolean | Coerced: anything other than an explicit true becomes `false`. See below. | | `velocity` | number \| null | Ground speed in metres per second. | | `trueTrack` | number \| null | Track over ground, degrees clockwise from true north. This is track, not heading. | | `verticalRate` | number \| null | Metres per second, positive climbing. | | `squawk` | string \| null | Transponder code, as a string, so leading zeros survive. | | `spi` | boolean | Special-purpose indicator. Coerced the same way as `onGround`. | | `positionSource` | number \| null | 0 to 3. | | `positionSourceLabel` | string \| null | `ADS-B`, `ASTERIX`, `MLAT` or `FLARM`. `null` for an index outside that range. | | `snapshotAt` | string \| null | The single timestamp the whole snapshot belongs to. Identical on every record from one request. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record: ```json { "icao24": "39de43", "callsign": "TVF99KE", "originCountry": "France", "timePosition": 1786016349, "timePositionIso": "2026-08-06T11:39:09.000Z", "lastContact": 1786016350, "lastContactIso": "2026-08-06T11:39:10.000Z", "longitude": 9.2508, "latitude": 45.3561, "baroAltitude": 11574.78, "geoAltitude": 12230.1, "onGround": false, "velocity": 220.77, "trueTrack": 288.9, "verticalRate": 0, "squawk": "5515", "spi": false, "positionSource": 0, "positionSourceLabel": "ADS-B", "snapshotAt": "2026-08-06T11:39:10.000Z", "scrapedAt": "2026-08-06T11:39:11.586Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial, and it is the only place the remaining credit balance appears. ```json { "targetsRequested": 1, "targetsFailed": 0, "failures": [], "statesSaved": 238, "authenticated": false, "creditsRemaining": 331, "filters": { "boundingBox": { "lamin": 45, "lomin": 5, "lamax": 48, "lomax": 10 }, "icao24": [], "onGroundOnly": false, "maxResults": 500 }, "finishedAt": "2026-08-06T11:39:12.004Z" } ``` **A run with zero rows is ambiguous, and this record disambiguates it.** `creditsRemaining` at or near zero means the allowance is gone, not that the sky was empty. `filters.boundingBox` being `null` when you thought you passed a box means your box was unreadable and the run went worldwide at four times the price. `targetsRequested` is one per 50 ICAO24 addresses, or one for a plain box query. `statesSaved` equal to `maxResults` means you were truncated. ## Behaviour to plan around - **A malformed `boundingBox` silently becomes a worldwide query.** Out-of-range and inverted boxes throw with a clear message, but a box whose values are not numbers at all reads as no box, and no box means the whole world at 4 credits. Always check `RUN_SUMMARY.filters.boundingBox` after a run you did not expect to be global. - **`onGround: false` does not prove the aircraft is airborne.** The flag is coerced from the source value, and anything that is not an explicit true, including a missing value, lands as `false`. `spi` behaves identically. Use `baroAltitude` and `velocity` as corroboration. - **Every record in one request shares `snapshotAt`.** The dataset is a coherent instant, not a smear. When `icao24` spans more than 50 addresses there is more than one request, so rows can carry different `snapshotAt` values within the same run. Group on it before computing anything positional. - **Nulls are real and must not be coerced to zero.** `baroAltitude`, `squawk`, `velocity` and `verticalRate` are absent for many aircraft, and an aircraft on the ground usually has no barometric altitude at all. A `null` velocity is not a stationary aircraft. - **Duplicate addresses collapse.** If the same `icao24` appears twice in a snapshot, only the first is saved. - **`maxResults` truncates mid-batch.** With several ICAO24 batches the run stops as soon as the cap is reached, so later batches are never requested and their aircraft are simply absent, with nothing in `failures` to say so. - **`trueTrack` is track over ground, not heading.** In wind the two differ. There is no heading field. - **Credential errors are final.** A 401 or 403 means credentials or exhausted quota, not a blip, so it is not retried. Transient conditions get three attempts with linear backoff. The run only throws when every request failed. - **Coverage is receiver-dependent.** The network is fed by volunteer receivers, so oceanic and low-altitude coverage is patchy. A thin result over remote airspace is a coverage fact, not a filter mistake. ## Recipes **Regional traffic time series.** One small box, scheduled. Cheapest possible query at 1 credit per run. ```json { "boundingBox": { "lamin": 45, "lomin": 5, "lamax": 48, "lomax": 10 }, "maxResults": 500 } ``` Append each run to a table keyed on `icao24` plus `snapshotAt` and you have tracks without ever asking for history. **Airport apron snapshot.** Tight box around the field, ground traffic only. ```json { "boundingBox": { "lamin": 51.45, "lomin": -0.50, "lamax": 51.49, "lomax": -0.41 }, "onGroundOnly": true, "maxResults": 200 } ``` Expect `baroAltitude` to be `null` on most of these rows; use `latitude` and `longitude` against your own stand geometry instead. **Fleet tracking by airframe.** Addresses are batched 50 at a time, so 120 of them is three requests and three credit charges. ```json { "icao24": ["4b4072", "3c6444"], "clientId": "…", "clientSecret": "…" } ``` Rows come back only for aircraft currently transmitting; a missing address means not airborne or not in coverage, and there is no row to say so. **Rule of thumb for credentials.** Anonymous is fine for ad-hoc runs and for a schedule no tighter than roughly every ten minutes on a small box. Anything more frequent, or a large box, or several boxes per run, and you should supply `clientId` and `clientSecret`. Watch `creditsRemaining` for a few runs before committing to a schedule.