GSX stand feed
GET /api/gsx/stand publishes the stand a callsign currently holds so that GSX,
running in the pilot’s simulator, can select that parking without the pilot
doing anything. It is disabled by default; set ENABLE_GSX_STAND_FEED=true to
expose it.
Why it is unauthenticated
Section titled “Why it is unauthenticated”GSX’s scripting API can only issue a plain HTTP GET. Its one network call is
fetchJson(url, timeout, etag), which sends no headers, so the
Authorization: Bearer token every other web API depends on cannot reach it.
Requiring a token would mean every pilot pasting one into a Python file before
their first flight.
The endpoint is therefore public, and kept safe by disclosing as little as possible rather than by authenticating:
- It answers only
{ "stand": ..., "pushback": ..., "revision": ... }. No route, no CID, no PDC state, no flight plan, no aircraft type. - It reads
LIVEsessions only. A sweatbox or playback stand never reaches a pilot’s simulator. - An unknown callsign, a callsign with no strip, and a strip with no stand are
all the same answer:
{ "stand": null }. The endpoint does not reveal whether a callsign is known to FlightStrips.
Callsign and position are already published on the VATSIM datafeed, and the stand is passed to the pilot over the air, so the feed discloses nothing that is not already available.
Request
Section titled “Request”| Parameter | Required | Notes |
|---|---|---|
callsign |
yes | Alphanumeric, up to 12 characters. Normalised to upper case. |
icao |
no | 3-4 characters. Restricts the lookup to the LIVE session at that airport. |
scenery |
no | Which add-on the pilot is running. Without it the answer uses the controller’s own stand name and carries no pushback point. |
GET /api/gsx/stand?callsign=SAS1401&icao=EKCH&scenery=Simnord-Sonnich// arriving at EKCH{ "stand": "Gate A31", "pushback": null, "revision": "stand:Gate A31" }
// departing EKCH{ "stand": null, "pushback": ["Z2 Face E"], "revision": "push:Z2 Face E" }400 for a missing or malformed callsign or icao. 503 when the lookup
fails, which tells the script to keep its current stand and retry. Everything
else is 200.
revision and ETag
Section titled “revision and ETag”revision is the value the handler script compares between polls, and the
response carries it as an ETag so the script can poll with
fetchJson(url, etag=True) and get a zero-byte 304 Not Modified when nothing
has changed.
revision is the assignment itself, deliberately, not strips.version. A
strip’s version changes for reasons that have nothing to do with parking, and
each of those would otherwise look like a reassignment and make the script
re-apply what is already applied.
Arrivals and departures
Section titled “Arrivals and departures”The two halves are never both live. A stand is where arriving traffic is told to park; a pushback point is how departing traffic comes off the stand it is already sitting on. They belong to opposite ends of a turnaround — often to two different callsigns, since the inbound SAS1401 leaves again as SAS1402.
The endpoint decides which applies, using the same rule as the PDC lookup —
strip.Origin == session.Airport means the flight is leaving:
| Leg | stand |
pushback |
|---|---|---|
| arriving here | the assigned stand | null |
| departing here | null |
the assigned route |
So a release point sitting on an inbound strip is never published: it belongs to
that aircraft’s later departure. And a departing aircraft is never sent a stand,
because it is already parked and loading — selectGate would refuse anyway
while ground services are active, and moving it would be wrong even if it
worked.
Source of truth
Section titled “Source of truth”The endpoint reads strips.stand, which is the authoritative operational value:
SAT’s automatic allocation and a controller’s manual override both write there.
It does not read stand_assignments, so it needs no knowledge of SAT stages,
provenance or acknowledgement, and it keeps working when
ENABLE_STAND_ASSIGNMENT is false and stands are being set by hand.
Nothing is written back. A pilot who parks somewhere else is something the controller should see and resolve on the board, not something the simulator silently overwrites.
Identity
Section titled “Identity”FlightStrips keys strips on (session_id, callsign) and holds no registration
used for matching, so the script has to reproduce the callsign the pilot
connected to the network with. It tries three sources in order:
- SimBrief —
getSimbrief().callsignis the ATC callsign from the filed plan, the same string the pilot gives vPilot. GSX itself falls back toicao_airline + flight_numberwhen the plan omits it. This is the reliable path, and most pilots on the network file a SimBrief plan. - The livery’s
aircraft.icaoAirlinejoined to theATC FLIGHT NUMBERSimVar. ATC ID, the tail number, for anyone flying without either.
A pilot the script cannot identify gets { "stand": null } and no stand — the
same as not installing the script at all. It fails quiet, never wrong.
GSX loads SimBrief data when the aircraft is parked with engines off and the GSX
menu is opened, and the plan stays loaded for the session, so it is available in
onEnterAirport on arrival. Note that GSX only accepts a plan whose aircraft
type matches the loaded aircraft and whose ETD has not passed, unless the pilot
enables Simbrief Ignore Time in GSX settings.
Gate and scenery configuration
Section titled “Gate and scenery configuration”The same physical gate is not the same object in every add-on. Developers number
stands differently and each authors their own pushback route names, so what a
controller calls F91 is Parking 91 in Simnord’s EKCH, and “push onto Z2
facing east” is a string that only exists in that one profile. Nothing resolves
this centrally, so it is declared per airport in
backend/config/<icao>/gsx_sceneries.json:
{ "icao": "EKCH", "gates": { "A31": { "Simnord-Sonnich": { "stand": "Gate A31", "points": { "Z/L": ["Z2 Face E"], "Y/L": ["Z3 Face W"], "K/J": ["J1 Face S"] } } } }}Read it as gate → scenery → what that scenery calls it:
- The gate key is the stand as controllers know it — the value in
strips.stand. - The scenery key is whatever the handler script sends as
scenery. standis the name handed toselectGate(). Omit it when the scenery uses the controller’s own name.pointsmaps a FlightStrips release point to every GSX route that reaches it. It is a list because a stand often offers the same taxiway in two facings; order is preferred-first, so a known default can lead.
Everything is optional and everything degrades quietly. A missing file, an
unknown scenery, an unmapped release point: the feed still publishes the stand,
just without translation or a pushback point. Lookups ignore case and extra
spacing, which matters because scenery labels are hand-typed — one EKCH profile
contains both Y1 Face E and Y1 Face E.
Maintaining the file
Section titled “Maintaining the file”It is edited by hand. There is deliberately no generator: the file is the source of truth rather than a derivation of one, so hand-tuned entries cannot be lost to a regeneration.
The EKCH file was seeded by matching GSX labels against the frontend’s
RELEASE_POINTS — the set PushbackMapDialog writes to strips.release_point.
The two vocabularies share a token, since a GSX label is
<release point> Face <direction>, so Z2 Face E is the route for a controller
who assigned Z2. That relationship is still the rule to follow when adding a
stand by hand.
Two fields exist only to help whoever edits it, and are ignored by the loader:
availablelists every route the scenery offers at that stand, so you can see what there is to map without opening the.ini.reviewrecords what is still unresolved there.
To add a stand, find its section in the GSX .ini, take pushbacklabels (the
two defaults, left then right) plus the label of each pushbackaddpos entry,
and map each to the release point a controller would assign for it. Where a
stand offers the same taxiway in two facings, list both unless local knowledge
says the stand only ever leaves one way — the pilot then picks the facing, and
cannot leave via a taxiway the controller did not name.
What is still unresolved at EKCH
Section titled “What is still unresolved at EKCH”| Category | Count | What it needs |
|---|---|---|
| Facing rule leaves no route | 1 | A17 always faces east, but its only Y0 route faces west |
| Two facings published | 2 | A34 Z1, E20 S1 — a local rule would narrow each to one |
| Bare taxiway | 22 stands | scenery offers “onto J/V/W”; controllers only have numbered points there |
| Stand ident unresolved | 32 | mostly E110–E137 and E15–E19, which FlightStrips does not track |
| No named routes | 1 | F95 offers only generic Nose/Tail — nothing to map |
Each one is recorded in that stand’s review note.
Pushback points
Section titled “Pushback points”For departing traffic only. getGate().pushback, pushbackLabels and pushbackAddPos are all writable at
any time, so the handler can narrow the pushback menu to the assigned route:
- Extra slots defined by the profile: keep the wanted entries in
pushbackAddPosand drop the rest. - The two defaults:
pushbackLabelsis left then right, and the direction enum is a bitmask -1left,2right,3both,0neither.
GSX has no selectPushback(). A script cannot answer the menu on the
pilot’s behalf; it can only remove the routes that were not assigned, leaving
the assigned one as the only routed choice. GSX always offers Straight Pushback
and Pull Straight regardless of the parking preference, so the menu does not
disappear — the pilot still confirms.
The route applies to the stand the aircraft is already parked on, which is why this is a departure-only concern and why the script never pairs it with a stand selection.
Client
Section titled “Client”The handler script lives in gsx-client/. It is named after the GSX .ini
profile it accompanies, which binds it to one airport and one scenery, and is
distributed alongside that profile. Set SCENERY in it to match the scenery key
in gsx_sceneries.json.
Adding this to your GSX profile
Section titled “Adding this to your GSX profile”If you build and publish GSX profiles, you can ship stand assignment with the
profile you already distribute. Pilots install it the same way they install
everything else — copy the files into %APPDATA%\Virtuali\GSX\MSFS\ — and there
is nothing for them to configure.
Naming
Section titled “Naming”GSX loads an airport handler only when its filename is the active .ini
profile’s name plus _handler. The stem must match exactly:
| Your profile | Your handler script |
|---|---|
EKCH-Simnord-Sonnich.ini |
EKCH-Simnord-Sonnich_handler.py |
ekbi-simnord24.ini |
ekbi-simnord24_handler.py |
EGKK-MyScenery.ini |
EGKK-MyScenery_handler.py |
That binding is what makes the script scenery-specific. It loads only for the scenery whose profile is active, so the stand names it resolves are the ones that scenery actually has — you do not need to worry about another developer’s numbering for the same airport.
A complete working script
Section titled “A complete working script”Copy this, rename it to match your profile, and set API_BASE. It is the whole
feature in about fifty lines; the script in gsx-client/ is the same thing with
more logging and edge-case handling.
# -- coding: utf-8 --# Stand assignment from FlightStrips. Python 3.7.# Save next to your .ini as <profile name>_handler.py
API_BASE = "https://api.flightstrips.dk"POLL_INTERVAL_MS = 30000POLL_LIMIT = 240
def _standReadCallsign(self): """The callsign the pilot filed with, from their SimBrief plan.""" try: sb = getSimbrief() if sb is not None and not sb.last_error and sb.callsign: clean = "".join(c for c in sb.callsign.upper() if c.isalnum()) return clean[:12] except Exception as err: print("[stands] SimBrief unavailable: %s" % err) return ""
def _standCheck(self): """Fetch the current stand and select it if it has changed.""" if not self._standCallsign: return
airport = getAirport() payload = fetchJson("%s/api/gsx/stand?callsign=%s&icao=%s" % (API_BASE, self._standCallsign, airport.icao if airport else ""), timeout=8, etag=True)
# None is a network error, and an unchanged stand costs a 304. In both # cases there is nothing to do until the next poll. if not payload or payload.get("revision") == self._standRevision: return
stand = payload.get("stand") if not stand: self._standRevision = payload.get("revision") return
result = selectGate(stand) if isinstance(result, list) and result: result = selectGate(result[0]) # ambiguous name: take the first match if result is True: self._standRevision = payload.get("revision") showMessage("Stand %s assigned" % stand) # visible with the menu open
def onEnterAirport(self): self._standCallsign = _standReadCallsign(self) self._standRevision = None self._standPoll = None
if getGate() is not None: return # the pilot already picked a stand - leave them alone
_standCheck(self)
def loop(): for _ in range(POLL_LIMIT): truewait(POLL_INTERVAL_MS) # wall clock, unaffected by sim rate _standCheck(self)
self._standPoll = runAsync(loop)
def onGateReset(self, reason): """The pilot chose their own stand. Stop, for the rest of this visit.""" if reason in ("user_changed", "user_revoked"): cancelAsync(getattr(self, "_standPoll", None)) self._standPoll = NoneWhat to check before publishing
Section titled “What to check before publishing”- Set
API_BASEto the FlightStrips instance for your vACC, over HTTPS. Pilots never edit this — it ships already set. - Test with the profile active. If the filename stem is wrong the script is silently not loaded; GSX will not warn you.
- Watch the GSX Handler Editor output panel on your first run. The script
prints there, and
F5reloads it without restarting the sim. - Confirm your stand names resolve.
selectGatematches the BGL name and number first (Gate A12), then the UI name, then a suffix (A12matchesGate A12). If your scenery numbers stands unusually, check that the strings controllers type in FlightStrips actually resolve in your profile.
What pilots see
Section titled “What pilots see”Nothing, unless they have a stand. A pilot with no strip, no assignment, or no SimBrief plan gets one HTTP request on arrival and no further activity — GSX behaves exactly as it does without the script. It never overrides a stand the pilot picked themselves, and it stops for good the moment they change one.