Skip to main content

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
}
FieldTypeDescription
user_codestring6-digit code to display to the pilot
authorization_tokenstring64-character hex token (keep secret, used for polling)
expires_inintegerSeconds until the code expires (300 = 5 minutes)
poll_intervalintegerMinimum 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..."
}
ParameterTypeRequiredDescription
authorization_tokenstringYesThe 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",
"email": "[email protected]",
"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]
}
}
FieldTypeDescription
profile.rankobject | nullPilot's current rank (null if unranked)
profile.current_airportobject | nullAirport where the pilot is currently located
profile.base_airportobject | nullPilot's assigned home base
profile.totalsobjectLifetime statistics
historyobject6-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": { "..." }
}
FieldTypeDescription
idintegerPilot's user id
flights_countintegerAccepted flights
flight_hoursfloatAccepted flight hours
landing_rate_avgfloat | nullAverage landing rate (fpm) over accepted flights
total_distanceintegerNautical miles flown on accepted flights
pointsintegerLifetime points
rankstring | nullCurrent 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.

Retry safety

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 precision

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):

FieldTypeRequiredDescription
latitudefloatYesCurrent latitude
longitudefloatYesCurrent longitude
altitudeintNoAltitude MSL (feet)
altitudeAglintNoAltitude AGL (feet)
headingintNoTrue heading (degrees)
groundSpeedintNoGround speed (knots)
indicatedAirspeedintNoIAS (knots)
trueAirspeedintNoTAS (knots)
verticalSpeedintNoVS (fpm)
flightPhasestringNoCurrent phase
onGroundboolNoOn ground flag
landingRatefloatNoLanding VS (fpm)
aircraftTypestringNoAircraft ICAO type code (e.g. B738)
acarsVersionstringNoTop-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:

FieldTypeDescription
engines[].n1floatN1 percentage
engines[].n2floatN2 percentage
engines[].throttlefloatThrottle position
engines[].mixturefloatMixture lever position
engines[].propellerfloatPropeller lever position
engines[].firingboolEngine running
engines[].existsboolEngine physically present on aircraft

Autopilot fields:

FieldTypeDescription
autopilot.masterboolAutopilot master switch
autopilot.headingfloatSelected heading
autopilot.altitudefloatSelected altitude
autopilot.vsfloatSelected vertical speed
autopilot.speedfloatSelected speed
autopilot.approachboolApproach mode active
autopilot.navboolNAV mode active

Radio & navigation fields:

FieldTypeDescription
radios.com1floatCOM1 frequency
radios.com2floatCOM2 frequency
radios.nav1floatNAV1 frequency
radios.nav2floatNAV2 frequency
radios.nav1ObsfloatNAV1 OBS setting
radios.nav2ObsfloatNAV2 OBS setting
radios.transponderintTransponder code
radios.transponderStateintTransponder state (0=off, 1=standby, 2=on, 3=test)

Lights fields:

FieldTypeDescription
lights.beaconboolBeacon light
lights.strobeboolStrobe lights
lights.landingboolLanding lights

Controls fields:

FieldTypeDescription
controls.elevatorfloatElevator deflection
controls.aileronfloatAileron deflection
controls.rudderfloatRudder deflection
controls.spoilersfloatSpoilers position

APU fields:

FieldTypeDescription
apu.switchOnboolAPU master switch
apu.rpmfloatAPU RPM percentage
apu.genSwitchboolAPU generator switch
apu.genActiveboolAPU generator active

Doors fields:

FieldTypeDescription
doors[].openfloatDoor 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.

FieldTypeDescription
runway.ends[].designatorstringRunway end identifier, e.g. 09L
runway.ends[].latitudefloatThreshold latitude
runway.ends[].longitudefloatThreshold longitude
runway.ends[].altitudefloatThreshold elevation (ft). Both ends report the same value — neither simulator source gives a separate elevation per end.
stand.namestringParking stand name, e.g. A12
stand.latitudefloatStand latitude
stand.longitudefloatStand 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:

FieldTypeDescription
wind.directionfloatWind direction (degrees true)
wind.speedfloatWind speed (knots)
sensors.pressureQNHfloatQNH pressure setting
sensors.gForcefloatCurrent G-force
sensors.stallWarningboolStall warning active
sensors.overspeedWarningboolOverspeed warning active
sensors.pausedboolSimulation paused
sensors.slewboolSlew mode active
sensors.crashedboolAircraft crashed
simulationRatefloatSimulation rate multiplier
totalWeightfloatAircraft total weight (lbs)
fuelWeightfloatFuel 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."
}
Reduced ASCII character set

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.

ParameterDefaultDescription
page, per_page1, 50Pagination (per_page capped at 100)
sortcreated_atOne of created_at, blocks_off, blocks_on, landing_rate, distance, flight_time
sort_dirdescasc 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 }
}
FieldTypeDescription
statusstringPIREP status: pendingStaff, pendingUser, pendingSystem, acceptedSystem, acceptedStaff, rejectedSystem, rejectedStaff, warningSystem, warningStaff
flight_time, block_timeintegerMinutes
distanceintegerNautical miles
landing_rateintegerfpm (negative)
fuel_usedinteger | nullkg burned, once the flight has been processed
scoreinteger | nullFlight score on a 0–100 scale (composite_score × 10), once rated
composite_scorefloat | nullThe same score on the platform's 0–10 scale
departure_time, arrival_timestring | nullBlocks 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.

ParameterDefaultDescription
page, per_page1, 25Pagination (per_page capped at 100)
sortcreated_atOne of created_at, starts_at, ends_at, title
sort_dirdescasc 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.

ParameterDefaultDescription
page, per_page1, 100Pagination (per_page capped at 100)
sortsort_orderOne of sort_order, title, created_at, updated_at
sort_dirascasc 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 }
}
FieldTypeDescription
typestringhtml, pdf or folder; auto-generated documents are html with is_auto_generated: true
sourcestring | nullFor auto-generated documents, a readable label of what they were generated from: Airport briefing, Load profile or FDM profile
source_typestring | nullThe same as a stable key: airport, load_profile or fdm_profile
contentstring | nullSanitised HTML for html documents
file_urlstring | nullAbsolute URL of the PDF download below for pdf documents; null otherwise
visibilitystringpublic, 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]
}
FieldTypeDescription
monthsstring[]6 month labels in YYYY-MM format (oldest first)
flight_hoursfloat[]Total accepted flight hours per month
flightsint[]Total accepted flight count per month
pointsint[]Total points earned per month
avg_landing_ratefloat[]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:

CodeMeaning
401Missing or invalid Bearer token — the client signs the pilot out
403The record exists but is not available to this pilot
404Resource not found (no active booking, no such route or record, etc.)
410Device code expired
422Validation error
429Rate limited (polling too fast)