# OpenSky Flight Scraper: Live Aircraft Positions (`arman-bd/opensky-flights-scraper`) Actor

Scrape live ADS-B aircraft states from the OpenSky Network: ICAO24, callsign, origin country, latitude, longitude, altitude, velocity and heading. Filter by bounding box.

- **URL**: https://apify.com/arman-bd/opensky-flights-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Travel, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.37 / 1,000 state scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## OpenSky Flight Scraper: Live Aircraft Positions

![OpenSky Flight Scraper: A live snapshot of aircraft inside a bounding box, position, altitude, speed, heading and vertical rate](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/opensky-flights-scraper.jpg)

Takes a snapshot of every aircraft currently transmitting inside a geographic box: ICAO24 address, callsign, origin country, latitude, longitude, barometric and geometric altitude, ground speed, heading and vertical rate.

The OpenSky Network is a community ADS-B receiver network that publishes its live state vectors over a plain HTTP API. This Actor calls that API directly, so there is no browser, no proxy, no login and no scraping of a rendered map. A box covering the Alps returns roughly 240 aircraft.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/opensky-flights-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/opensky-flights-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `icao24` | 24-bit ICAO transponder address in lowercase hex, the stable aircraft identifier |
| `callsign` | Flight callsign as broadcast, trimmed. `null` when the aircraft is not transmitting one |
| `originCountry` | Country the transponder address is registered to |
| `timePosition`, `timePositionIso` | When the position fix was taken, as Unix seconds and ISO 8601 |
| `lastContact`, `lastContactIso` | Last time any message was received from this aircraft |
| `latitude`, `longitude` | WGS84 position in decimal degrees |
| `baroAltitude`, `geoAltitude` | Barometric and geometric altitude in metres |
| `onGround` | Whether the network places the aircraft on a surface |
| `velocity` | Ground speed in metres per second |
| `trueTrack` | Track over ground in degrees clockwise from true north |
| `verticalRate` | Metres per second, positive climbing |
| `squawk` | Transponder code |
| `spi` | Special-purpose indicator flag |
| `positionSource`, `positionSourceLabel` | 0 to 3 and its name: ADS-B, ASTERIX, MLAT or FLARM |
| `snapshotAt` | The single timestamp the whole snapshot belongs to |
| `scrapedAt` | Run timestamp |

`RUN_SUMMARY` in the key-value store holds counts, whether the run was authenticated, the API credits left, the filters you used, and any request that failed.

### Use cases

- **Regional traffic monitoring.** Schedule a run every few minutes over one box and build a time series.
- **Airport apron analytics.** A tight box plus `onGroundOnly` gives taxiing and parked aircraft.
- **Fleet tracking.** Pass a list of `icao24` addresses and follow specific airframes.
- **Live displays.** Feed a map or dashboard with a current snapshot on demand.
- **Aviation research.** Sample altitude, speed and vertical-rate distributions over a corridor.

### Quick start

Everything over the Alps:

```json
{
 "boundingBox": { "lamin": 45, "lomin": 5, "lamax": 48, "lomax": 10 }
}
```

Aircraft on the ground around a UK box, capped:

```json
{
 "boundingBox": { "lamin": 49.9, "lomin": -6.4, "lamax": 55.8, "lomax": 1.8 },
 "onGroundOnly": true,
 "maxResults": 200
}
```

Named airframes, authenticated for the higher quota:

```json
{
 "icao24": ["4b4072", "3c6444"],
 "clientId": "your-opensky-client-id",
 "clientSecret": "your-opensky-client-secret"
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `boundingBox` | object | none | `{ lamin, lomin, lamax, lomax }` in WGS84 degrees. Omit for a worldwide query, which costs 4 credits instead of 1. |
| `icao24` | array | `[]` | 6-hex-character ICAO addresses. Batched 50 per request. Combines with `boundingBox` if both are set. |
| `onGroundOnly` | boolean | `false` | Keep only aircraft reported on a surface. Off returns airborne and grounded alike. |
| `maxResults` | integer | `500` | Cap on states saved, applied after filtering. |
| `clientId` | string (secret) | none | Optional OAuth2 client ID. |
| `clientSecret` | string (secret) | none | Optional OAuth2 client secret. Both must be present or the run stays anonymous. |

`boundingBox` is validated before any request goes out: latitudes must be -90 to 90, longitudes -180 to 180, and `lamin`/`lomin` must be below `lamax`/`lomax`. An inverted or out-of-range box fails immediately rather than silently returning nothing.

### Output example

```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"
}
```

Units are SI throughout: metres for altitude, metres per second for velocity and vertical rate, degrees for position and track. Nothing is converted to feet or knots, so convert on your side if you need to and no precision is lost here.

### Credits and rate limits

OpenSky meters `/states/all` in credits, and the cost depends on how much sky you ask for:

| Query | Credits |
|---|---|
| Box of 25 square degrees or less | 1 |
| Box up to 100 square degrees | 2 |
| Box up to 400 square degrees | 3 |
| Larger box or worldwide | 4 |

| Access | Daily credits | Snapshot resolution |
|---|---|---|
| Anonymous (by IP) | 400 | 10 seconds |
| OAuth2 client credentials | 4,000 | 5 seconds |

Anonymous access is shared per source IP, and Apify runs go out through shared addresses, so the 400 you nominally get may already be partly spent. The Actor logs the `x-rate-limit-remaining` value OpenSky returns and stores it in `RUN_SUMMARY.creditsRemaining`, so you can see exactly where you stand after every run. If you plan to schedule this, register a free OpenSky account, create an API client, and paste the client ID and secret in. The Actor then fetches an OAuth2 token itself.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~opensky-flights-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "boundingBox": { "lamin": 45, "lomin": 5, "lamax": 48, "lomax": 10 },
 "maxResults": 100
 }'
```

### JavaScript example

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_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 },
 maxResults: 500,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const state of items) {
 console.log(`${state.callsign ?? state.icao24} - ${state.baroAltitude} m at ${state.velocity} m/s`);
}
```

### Notes

- One snapshot gets one timestamp. Every record in a run shares the same `snapshotAt`, so the dataset is a coherent instant rather than a smear.
- Positional arrays are decoded for you. The API returns each aircraft as a bare 17-element array, and the Actor maps it against the documented state-vector contract and hands you named fields.
- Nulls are real. `baroAltitude`, `squawk`, `velocity` and `verticalRate` are absent for many aircraft, and an aircraft on the ground usually has no barometric altitude. Those stay `null` rather than being coerced to zero.
- Duplicate addresses collapse. If the same `icao24` appears twice in one snapshot only the first is saved.
- Transient errors get retried. 429 and 5xx responses get three attempts with linear backoff. A 401 or 403 is fatal, because it means credentials or quota, not a blip.
- Public data only. ADS-B is broadcast unencrypted by aircraft, and OpenSky aggregates it from volunteer receivers.

### FAQ

**Do I need an OpenSky account?** No. You supply no credentials.

**Why did I get zero aircraft?** Either the box is over empty airspace or your IP's daily credits ran out. `RUN_SUMMARY.creditsRemaining` tells you which.

**Can I get historical flights?** No. This Actor reads the live state snapshot only. OpenSky's historical endpoints need an authenticated account and a different query model.

**Are altitudes in feet?** No, metres, exactly as the API delivers them. Multiply by 3.28084 for feet.

**Why is `callsign` empty for some aircraft?** Not every transponder broadcasts one, particularly general aviation and aircraft on the ground. `icao24` is always present and is the field to key on.

**Can I schedule it?** Yes, and that is the point. A run every five minutes over one small box costs about 288 credits a day, which fits inside the anonymous allowance if the IP is not shared. Authenticate for anything tighter than that.

# Actor input Schema

## `boundingBox` (type: `object`):

Geographic window as WGS84 degrees: lamin/lamax are latitudes (-90 to 90), lomin/lomax longitudes (-180 to 180). Leave empty to query the whole world, which costs 4 API credits instead of 1.

## `icao24` (type: `array`):

Track named aircraft by their 24-bit ICAO transponder address in lowercase hex, e.g. 4b4072. Requests are batched 50 addresses at a time. Leave empty to take everything in the bounding box.

## `onGroundOnly` (type: `boolean`):

Keep only aircraft the network reports as being on the ground, which is what you want for airport apron and taxiway analytics. Off returns everything, airborne and grounded.

## `maxResults` (type: `integer`):

Cap the states saved per run, applied after filtering. A busy box over central Europe returns a few hundred aircraft.

## `clientId` (type: `string`):

Optional OAuth2 client ID from your OpenSky account. Authenticated runs get 4,000 credits/day and 5-second resolution instead of the anonymous 400/day at 10 seconds. Leave empty to run anonymously.

## `clientSecret` (type: `string`):

Optional OAuth2 client secret paired with the client ID. Both must be set for authentication to be attempted; if either is missing the run falls back to anonymous access.

## Actor input object example

```json
{
  "boundingBox": {
    "lamin": 49.9,
    "lomin": -6.4,
    "lamax": 55.8,
    "lomax": 1.8
  },
  "icao24": [
    "4b4072",
    "3c6444"
  ],
  "onGroundOnly": false,
  "maxResults": 500
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN_SUMMARY record from the run's key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "boundingBox": {
        "lamin": 45,
        "lomin": 5,
        "lamax": 48,
        "lomax": 10
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/opensky-flights-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "boundingBox": {
        "lamin": 45,
        "lomin": 5,
        "lamax": 48,
        "lomax": 10,
    } }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/opensky-flights-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "boundingBox": {
    "lamin": 45,
    "lomin": 5,
    "lamax": 48,
    "lomax": 10
  }
}' |
apify call arman-bd/opensky-flights-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/opensky-flights-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/94wSnlyQ8Gwy7aHRW/builds/njcAJtWkZW9TxjSOQ/openapi.json
