ACARS V2 API — Device Code Authentication
The V2 ACARS API introduces a device code authentication flow, replacing the traditional username/password login. This allows ACARS desktop clients to authenticate securely without handling user credentials.
Authentication Flow
sequenceDiagram
participant Client as ACARS Client
participant API as Airspace API
participant Pilot as Pilot (Browser)
Client->>API: POST /api/v2/acars/auth/request
API-->>Client: { user_code, authorization_token, expires_in, poll_interval }
Client->>Pilot: Display "Enter code at /acars/authorize"
loop Every poll_interval seconds
Client->>API: POST /api/v2/acars/auth/token
API-->>Client: 202 Pending
end
Pilot->>API: Enters code on /acars/authorize
Note over API: Code approved
Client->>API: POST /api/v2/acars/auth/token
API-->>Client: 200 { access_token, token_type }
Client->>API: GET /api/v2/acars/va (Bearer token)
API-->>Client: VA data
Step 1: Request a Device Code
POST /api/v2/acars/auth/request
No authentication required. No request body.
Response (201 Created):
{
"user_code": "482910",
"authorization_token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"expires_in": 300,
"poll_interval": 5
}
| Field | Type | Description |
|---|---|---|
user_code | string | 6-digit code to display to the pilot |
authorization_token | string | 64-character hex token (keep secret, used for polling) |
expires_in | integer | Seconds until the code expires (300 = 5 minutes) |
poll_interval | integer | Minimum seconds between poll requests |
Step 2: User Approves the Code
The pilot navigates to /acars/authorize in their browser (must be logged in to the VA), enters the 6-digit code, and clicks "Authorize Device".
The code can also be pre-filled via query parameter: /acars/authorize?code=482910.
Step 3: Poll for Access Token
POST /api/v2/acars/auth/token
Content-Type: application/json
{
"authorization_token": "a1b2c3d4e5f6..."
}
| Parameter | Type | Required | Description |
|---|---|---|---|
authorization_token | string | Yes | The 64-character token from step 1 |
Response Codes:
200 OK — Approved
{
"access_token": "1|abc123def456...",
"token_type": "Bearer"
}
The device has been approved. Use the access_token as a Bearer token for all authenticated endpoints.
202 Accepted — Pending
{
"status": "pending"
}
The user hasn't approved the code yet. Continue polling every poll_interval seconds.
410 Gone — Expired
{
"status": "expired"
}
The code has expired or doesn't exist. Start a new device code flow from step 1.
429 Too Many Requests — Slow Down
{
"status": "slow_down"
}
You're polling too frequently. Wait at least poll_interval seconds between requests.
Authenticated Endpoints
Once you have an access_token, include it in the Authorization header:
Authorization: Bearer 1|abc123def456...
All V2 endpoints below require this header unless marked as public.
Pilot Profile & History
GET /api/v2/acars/pilot
Returns the authenticated pilot's profile information and 6-month flight history in a single call. This is the recommended endpoint for building pilot dashboards.
Response (200):
{
"profile": {
"id": 42,
"name": "John Doe",
"profile_picture_url": "https://...",
"member_since": "2025-01-15T00:00:00+00:00",
"rank": { "name": "Captain", "level": 5, "color": "#FFD700", "icon": "captain.svg" },
"current_airport": { "icao": "KJFK", "iata": "JFK", "name": "John F. Kennedy Intl", "city": "New York" },
"base_airport": { "icao": "KLAX", "iata": "LAX", "name": "Los Angeles Intl", "city": "Los Angeles" },
"vatsim_id": "1234567",
"ivao_id": null,
"totals": {
"flights": 150,
"flight_hours": 320.5,
"points": 4500,
"avg_landing_rate": -185.3,
"last_flight_at": "2026-03-01T14:30:00+00:00"
}
},
"history": {
"months": ["2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"],
"flight_hours": [12.5, 8.3, 15.0, 6.2, 10.1, 3.5],
"flights": [5, 3, 7, 2, 4, 1],
"points": [500, 300, 700, 200, 400, 100],
"avg_landing_rate": [-180, -200, -150, -220, -170, -190]
}
}
| Field | Type | Description |
|---|---|---|
profile.rank | object | null | Pilot's current rank (null if unranked) |
profile.current_airport | object | null | Airport where the pilot is currently located |
profile.base_airport | object | null | Pilot's assigned home base |
profile.totals | object | Lifetime statistics |
history | object | 6-month monthly breakdown (same format as /history) |
The response also carries the pilot's id and lifetime aggregates at the top level, which the desktop client's My Flights tab shows next to the paginated logbook (see Pilot Flights). They cover accepted flights only, like profile.totals:
{
"id": 42,
"name": "John Doe",
"flights_count": 150,
"flight_hours": 320.5,
"landing_rate_avg": -185.3,
"total_distance": 45210,
"points": 4500,
"rank": "Captain",
"profile": { "..." },
"history": { "..." }
}
| Field | Type | Description |
|---|---|---|
id | integer | Pilot's user id |
flights_count | integer | Accepted flights |
flight_hours | float | Accepted flight hours |
landing_rate_avg | float | null | Average landing rate (fpm) over accepted flights |
total_distance | integer | Nautical miles flown on accepted flights |
points | integer | Lifetime points |
rank | string | null | Current rank name |
Also available at the V1 path: GET /api/acars/pilot
VA Information
GET /api/v2/acars/va
Returns the virtual airline's name, logos, primary color, and domains.
Response (200):
{
"name": "Example Airways",
"logo_url": "https://...",
"banner_url": "https://...",
"primary_color": "#1a73e8",
"domains": ["example.airspace.app"]
}
Active Booking
GET /api/v2/acars/booking
Returns the authenticated pilot's active booking with airline, aircraft, and airport details.
Response (200):
{
"id": 42,
"flight_number": "EX101",
"callsign": "EXA101",
"is_charter": false,
"is_alternating": false,
"expires_at": "2026-02-24T15:30:00+00:00",
"airline": { "name": "Example Airways", "icao": "EXA", "iata": "EX" },
"aircraft": { "registration": "N12345", "name": "Spirit of Adventure", "type": "B738", "subfleet": "738", "fleet": "Boeing 737" },
"departure_airport": { "icao": "KJFK", "iata": "JFK", "name": "John F. Kennedy Intl", "city": "New York", "latitude": 40.6398, "longitude": -73.7789, "elevation": 13 },
"arrival_airport": { "icao": "KLAX", "iata": "LAX", "name": "Los Angeles Intl", "city": "Los Angeles", "latitude": 33.9425, "longitude": -118.4081, "elevation": 128 },
"alternate_airport": null
}
Last Position
GET /api/v2/acars/position/last
Returns the latest position report for the active booking.
Response (200):
{
"sent_at": "2026-02-24T14:30:00.000+00:00",
"latitude": 40.6398,
"longitude": -73.7789,
"altitude": 35000,
"altitude_agl": 34500,
"heading": 270,
"ground_speed": 450,
"indicated_airspeed": 280,
"true_airspeed": 460,
"vertical_speed": 0,
"phase": "cruise",
"on_ground": false
}
Start Flight
POST /api/v2/acars/start
Marks the active booking as started and clears prior position reports.
Response (200):
{
"message": "Flight started",
"trackingID": 42
}
Submit Position Report
POST /api/v2/acars/position
Content-Type: application/json
Accepts a single position object or an array of position objects. Returns 202 immediately; processing is asynchronous.
Every position is stored durably before the 202 is returned — even if the background enrichment worker crashes, the data is not lost. The server also de-duplicates on (booking, sent_at), so a client that retries the same payload after a flaky network will never produce duplicate rows. Clients can therefore retry any 5xx response without coordination.
Single position:
{
"timestamp": "2026-03-08T12:00:00.123Z",
"position": { "latitude": { "value": 51.4775 }, "longitude": { "value": -0.4614 } },
"..."
}
Batch (array of positions):
[
{ "timestamp": "2026-03-08T12:00:00.000Z", "position": { "latitude": { "value": 51.0 }, "longitude": { "value": -0.5 } }, "..." },
{ "timestamp": "2026-03-08T12:00:00.250Z", "position": { "latitude": { "value": 51.5 }, "longitude": { "value": -0.6 } }, "..." }
]
timestamp is stored with millisecond precision (timestamp(3) in MySQL/Postgres). All of the following formats are accepted, so existing clients that report at whole-second resolution continue to work unchanged:
- ISO-8601 string with fractional seconds —
"2026-03-08T12:00:00.123Z" - ISO-8601 string with whole seconds —
"2026-03-08T12:00:00Z" - Unix epoch seconds (integer or numeric string) —
1741435200 - Unix epoch milliseconds (13-digit integer or numeric string) —
1741435200123
The server parses the format automatically: any numeric value ≥ 1e12 is treated as milliseconds since epoch, anything below that as seconds. Response timestamps are always returned in ISO-8601 with the .vvv fractional component.
Key fields (per position):
| Field | Type | Required | Description |
|---|---|---|---|
latitude | float | Yes | Current latitude |
longitude | float | Yes | Current longitude |
altitude | int | No | Altitude MSL (feet) |
altitudeAgl | int | No | Altitude AGL (feet) |
heading | int | No | True heading (degrees) |
groundSpeed | int | No | Ground speed (knots) |
indicatedAirspeed | int | No | IAS (knots) |
trueAirspeed | int | No | TAS (knots) |
verticalSpeed | int | No | VS (fpm) |
flightPhase | string | No | Current phase |
onGround | bool | No | On ground flag |
landingRate | float | No | Landing VS (fpm) |
aircraftType | string | No | Aircraft ICAO type code (e.g. B738) |
acarsVersion | string | No | Top-level field. Reporting client version (semver or short SHA, max 32 chars). Persisted on every position row for triage and rollout monitoring. |
Engine fields:
Each engine (1–4) supports the following nested fields:
| Field | Type | Description |
|---|---|---|
engines[].n1 | float | N1 percentage |
engines[].n2 | float | N2 percentage |
engines[].throttle | float | Throttle position |
engines[].mixture | float | Mixture lever position |
engines[].propeller | float | Propeller lever position |
engines[].firing | bool | Engine running |
engines[].exists | bool | Engine physically present on aircraft |
Autopilot fields:
| Field | Type | Description |
|---|---|---|
autopilot.master | bool | Autopilot master switch |
autopilot.heading | float | Selected heading |
autopilot.altitude | float | Selected altitude |
autopilot.vs | float | Selected vertical speed |
autopilot.speed | float | Selected speed |
autopilot.approach | bool | Approach mode active |
autopilot.nav | bool | NAV mode active |
Radio & navigation fields:
| Field | Type | Description |
|---|---|---|
radios.com1 | float | COM1 frequency |
radios.com2 | float | COM2 frequency |
radios.nav1 | float | NAV1 frequency |
radios.nav2 | float | NAV2 frequency |
radios.nav1Obs | float | NAV1 OBS setting |
radios.nav2Obs | float | NAV2 OBS setting |
radios.transponder | int | Transponder code |
radios.transponderState | int | Transponder state (0=off, 1=standby, 2=on, 3=test) |
Lights fields:
| Field | Type | Description |
|---|---|---|
lights.beacon | bool | Beacon light |
lights.strobe | bool | Strobe lights |
lights.landing | bool | Landing lights |
Controls fields:
| Field | Type | Description |
|---|---|---|
controls.elevator | float | Elevator deflection |
controls.aileron | float | Aileron deflection |
controls.rudder | float | Rudder deflection |
controls.spoilers | float | Spoilers position |
APU fields:
| Field | Type | Description |
|---|---|---|
apu.switchOn | bool | APU master switch |
apu.rpm | float | APU RPM percentage |
apu.genSwitch | bool | APU generator switch |
apu.genActive | bool | APU generator active |
Doors fields:
| Field | Type | Description |
|---|---|---|
doors[].open | float | Door open position (0.0 = closed, 1.0 = fully open) |
Up to 5 doors are supported (doors[0] through doors[4]).
Runway & stand fields:
null unless the aircraft is on the ground on a runway or a stand. A client that omits these keys entirely (for example, a report already sitting in an outbox from before the client supported them) is treated identically to sending null.
| Field | Type | Description |
|---|---|---|
runway.ends[].designator | string | Runway end identifier, e.g. 09L |
runway.ends[].latitude | float | Threshold latitude |
runway.ends[].longitude | float | Threshold longitude |
runway.ends[].altitude | float | Threshold elevation (ft). Both ends report the same value — neither simulator source gives a separate elevation per end. |
stand.name | string | Parking stand name, e.g. A12 |
stand.latitude | float | Stand latitude |
stand.longitude | float | Stand longitude |
The server resolves which of the two reported ends the aircraft is aligned with (from aircraft heading, preferring the airline's own runway record when one is seeded) and stores that alongside the reported pair. The flight's departure/arrival runway and stand are derived from this on every processing pass, and the landing performance report (touchdown distance/offset, runway used) is derived from it when a seeded airport_runways row exists for the resolved designator — see the Flights page's landing performance report section.
Environment & state fields:
| Field | Type | Description |
|---|---|---|
wind.direction | float | Wind direction (degrees true) |
wind.speed | float | Wind speed (knots) |
sensors.pressureQNH | float | QNH pressure setting |
sensors.gForce | float | Current G-force |
sensors.stallWarning | bool | Stall warning active |
sensors.overspeedWarning | bool | Overspeed warning active |
sensors.paused | bool | Simulation paused |
sensors.slew | bool | Slew mode active |
sensors.crashed | bool | Aircraft crashed |
simulationRate | float | Simulation rate multiplier |
totalWeight | float | Aircraft total weight (lbs) |
fuelWeight | float | Fuel weight (lbs) |
See the Scramble API docs at https://<your-domain>/docs/acars for the complete field schema and validation rules.
Response (202):
{
"message": "Position received"
}
or for batch:
{
"message": "3 positions received"
}
Cancel Flight
POST /api/v2/acars/stop
Stops the active flight and clears all position reports.
Response (200):
{
"message": "Flight canceled"
}
Finish Flight
POST /api/v2/acars/finish
Completes the flight, creates a PIREP, and deletes the booking.
Response (200):
{
"message": "Flight finished",
"pirepID": 123
}
Speech Instructions
GET /api/v2/acars/sound
Returns pending TTS speech instructions for the active booking.
Response (200):
{
"instructions": [
{ "type": "play", "url": "/api/v2/acars/sound/fragment/42" },
{ "type": "pause", "duration_ms": 500 },
{ "type": "play", "url": "/api/v2/acars/sound/combined/abc123.mp3" }
]
}
Audio Streaming (Public)
These endpoints do not require authentication:
GET /api/v2/acars/sound/fragment/{id}
GET /api/v2/acars/sound/combined/{filename}
GET /api/v2/acars/sound/upload/{filename}
Returns audio/mpeg streams.
Send Message
POST /api/v2/acars/message
Content-Type: application/json
{
"message": "Requesting pushback clearance."
}
For compatibility with ACARS / Hoppie / Baudot-era teleprinters, every persisted message is normalized to a reduced character set: A–Z, 0–9, space, comma (,), period (.), slash (/), hyphen (-). Lowercase letters are upcased and accented letters are converted to their unaccented equivalent (À → A, ç → C); anything else outside the set (non-Latin scripts, !, ?, :, ;, parentheses, em-dashes, emojis, newlines) is stripped silently. Clients should expect the response message field to differ from the request when input contained disallowed characters.
Response (201):
{
"id": 1,
"user_id": 5,
"type": "chat",
"sender_id": 5,
"sender_name": "John Doe",
"sender_role": "user",
"message": "REQUESTING PUSHBACK CLEARANCE",
"sent_at": "2026-02-24T14:30:00.000000Z",
"received_at": "2026-02-24T14:30:00.000000Z",
"read_at": null,
"created_at": "2026-02-24T14:30:00.000000Z",
"updated_at": "2026-02-24T14:30:00.000000Z"
}
Confirm Message
PUT /api/v2/acars/message/confirm
Content-Type: application/json
{
"message_id": 1
}
Marks a message as read. Returns the updated message object.
List Messages
GET /api/v2/acars/messages?page=1
Returns paginated messages (50 per page), newest first.
Pilot Resources
These endpoints back the desktop client's My Flights, NOTAMs and Documents tabs. They share the rules below.
Scoping. Every response is limited to the pilot behind the Bearer token and to their airline. The client sends filter[user_id] on the flights route; the server ignores it and never returns another pilot's data.
Status codes. 200 ok · 403 the record exists but is not available to this pilot · 404 no such route or record · 401 only for a missing, invalid or expired token. The client signs the pilot out on any 401 from /api/v2/acars/*, so nothing else answers 401.
Pagination. Laravel style: { "data": [...], "meta": { "current_page", "last_page", "per_page", "total" } }. page and per_page (capped at 100) select the page; sort and sort_dir (asc | desc) order it. An unknown sort falls back to the endpoint's default order.
Private API probe. The client tries /api/v1/<resource> before falling back to /api/v2/acars/<resource> and stops on 401/403. The private API v1 only accepts API keys, so a pilot's ACARS token on /api/v1/* answers 404 (not 401) and the client falls through to the routes below.
Pilot Flights
GET /api/v2/acars/pilot/flights
GET /api/v2/acars/flights (alias)
The pilot's filed PIREPs, newest first by default.
| Parameter | Default | Description |
|---|---|---|
page, per_page | 1, 50 | Pagination (per_page capped at 100) |
sort | created_at | One of created_at, blocks_off, blocks_on, landing_rate, distance, flight_time |
sort_dir | desc | asc or desc |
filter[user_id] | — | Ignored; the token decides whose logbook this is |
Response (200):
{
"data": [
{
"id": 12345,
"callsign": "GLO1234",
"flight_number": "G31234",
"status": "acceptedStaff",
"departure_airport": { "icao": "SBGR", "iata": "GRU", "name": "Guarulhos", "city": "Sao Paulo", "latitude": -23.435556, "longitude": -46.473056 },
"arrival_airport": { "icao": "SBRJ", "iata": "SDU", "name": "Santos Dumont", "city": "Rio de Janeiro", "latitude": -22.910461, "longitude": -43.163133 },
"aircraft": { "registration": "PR-GXZ", "name": "Boeing 737-800", "icao_code": "B738" },
"flight_time": 55,
"block_time": 60,
"distance": 195,
"landing_rate": -180,
"fuel_used": 1450,
"score": 98,
"composite_score": 9.8,
"pax": 120,
"cargo": 800,
"departure_time": "2026-09-26T14:30:00Z",
"arrival_time": "2026-09-26T15:25:00Z",
"created_at": "2026-09-26T15:30:00Z"
}
],
"meta": { "current_page": 1, "last_page": 3, "per_page": 50, "total": 120 }
}
| Field | Type | Description |
|---|---|---|
status | string | PIREP status: pendingStaff, pendingUser, pendingSystem, acceptedSystem, acceptedStaff, rejectedSystem, rejectedStaff, warningSystem, warningStaff |
flight_time, block_time | integer | Minutes |
distance | integer | Nautical miles |
landing_rate | integer | fpm (negative) |
fuel_used | integer | null | kg burned, once the flight has been processed |
score | integer | null | Flight score on a 0–100 scale (composite_score × 10), once rated |
composite_score | float | null | The same score on the platform's 0–10 scale |
departure_time, arrival_time | string | null | Blocks off / blocks on, ISO-8601 UTC |
Company NOTAMs
GET /api/v2/acars/notams
GET /api/v2/acars/notams/{id}
The company NOTAMs currently in effect for the airline — active, started and not yet ended — exactly as the pilot NOTAMs page shows them. NOTAMs that are inactive, scheduled for later or already ended answer 404 on the detail route.
| Parameter | Default | Description |
|---|---|---|
page, per_page | 1, 25 | Pagination (per_page capped at 100) |
sort | created_at | One of created_at, starts_at, ends_at, title |
sort_dir | desc | asc or desc |
filter[is_active] | — | Accepted for compatibility; only NOTAMs in effect are ever returned |
Response (200):
{
"data": [
{
"id": 7,
"title": "Runway closure",
"content": "<p>Runway 09R closed for works.</p>",
"body": "<p>Runway 09R closed for works.</p>",
"starts_at": "2026-09-20T00:00:00Z",
"ends_at": null,
"requires_acknowledgement": true,
"is_active": true,
"created_at": "2026-09-19T18:04:11Z",
"updated_at": "2026-09-19T18:04:11Z"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}
content and body carry the same sanitised HTML. The detail route wraps the object in data.
Documents
GET /api/v2/acars/documents
GET /api/v2/acars/documents/{id}
GET /api/v2/acars/documents/{id}/pdf
The document library as the pilot sees it: standalone documents whose visibility (public, minimum rank, licence, staff) opens them to the pilot behind the token. Documents attached to another record, such as MEL definitions, are not part of the library and answer 404; a library document closed to the pilot answers 403.
| Parameter | Default | Description |
|---|---|---|
page, per_page | 1, 100 | Pagination (per_page capped at 100) |
sort | sort_order | One of sort_order, title, created_at, updated_at |
sort_dir | asc | asc or desc |
filter[parent_id] | — | Documents inside this folder; an empty value selects the library roots |
filter[title] | — | Documents whose title contains this text |
Response (200):
{
"data": [
{
"id": 3,
"title": "FCOM",
"slug": "fcom",
"type": "pdf",
"parent_id": 1,
"visibility": "public",
"is_auto_generated": false,
"source": null,
"source_type": null,
"content": null,
"file_url": "https://example.airspace.app/api/v2/acars/documents/3/pdf",
"sort_order": 2,
"created_at": "2026-08-02T09:12:00Z",
"updated_at": "2026-08-02T09:12:00Z"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 100, "total": 1 }
}
| Field | Type | Description |
|---|---|---|
type | string | html, pdf or folder; auto-generated documents are html with is_auto_generated: true |
source | string | null | For auto-generated documents, a readable label of what they were generated from: Airport briefing, Load profile or FDM profile |
source_type | string | null | The same as a stable key: airport, load_profile or fdm_profile |
content | string | null | Sanitised HTML for html documents |
file_url | string | null | Absolute URL of the PDF download below for pdf documents; null otherwise |
visibility | string | public, staff, rank or license |
PDF download
GET /api/v2/acars/documents/{id}/pdf
Authorization: Bearer <token>
Returns application/pdf with Content-Disposition: attachment, authenticated with the ACARS token rather than a web session, so the client can fetch the bytes directly and open the file locally. pdf documents return the uploaded file; html (and auto-generated) documents return a rendered PDF, pre-generated when one is stored and rendered on the spot otherwise. Folders have no PDF and answer 404; a document closed to the pilot answers 403.
Interactive API Documentation
Full interactive API documentation with request/response schemas is available at:
https://<your-domain>/docs/acars
This is auto-generated from the live routes and covers the V2 API — every endpoint shows its authentication requirements, and requests can be tried directly from the page using a Bearer access token obtained through the device code flow.
Pilot History (Legacy)
Returns 6 months of the authenticated pilot's flight statistics — hours, flights, points, and average landing rate. Prefer /pilot which includes profile data alongside history.
GET /api/v2/acars/history
Authorization: Bearer <token>
Response:
{
"months": ["2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"],
"flight_hours": [12.5, 8.3, 15.0, 6.2, 10.1, 3.5],
"flights": [5, 3, 7, 2, 4, 1],
"points": [500, 300, 700, 200, 400, 100],
"avg_landing_rate": [-180, -200, -150, -220, -170, -190]
}
| Field | Type | Description |
|---|---|---|
months | string[] | 6 month labels in YYYY-MM format (oldest first) |
flight_hours | float[] | Total accepted flight hours per month |
flights | int[] | Total accepted flight count per month |
points | int[] | Total points earned per month |
avg_landing_rate | float[] | Average landing rate (fpm) per month |
Also available at the V1 path: GET /api/acars/history
VA Information — favicon_url
The VA information endpoint now includes a favicon_url field:
GET /api/v2/acars/va
The favicon_url field returns the tenant's configured favicon as a fully resolved URL, or null if not set.
Error Handling
All endpoints return JSON error responses:
{
"message": "No active booking"
}
Common error codes:
| Code | Meaning |
|---|---|
| 401 | Missing or invalid Bearer token — the client signs the pilot out |
| 403 | The record exists but is not available to this pilot |
| 404 | Resource not found (no active booking, no such route or record, etc.) |
| 410 | Device code expired |
| 422 | Validation error |
| 429 | Rate limited (polling too fast) |