Flight Seat Map — Implementation Guide
How the seat map works in the whitelabel frontend, from API data to rendered grid. Use this as a reference if you're building a seat map from the same data source.
1. Data Source
Seats come from the prebook response as a flat list of PrebookService objects with category: 'seat'. Each service represents one specific seat on one specific flight segment for one passenger type.
POST /v1/flights/prebook
→ response.data.servicesAttachable.groups[].services[]
Each seat service looks like this:
{
"serviceId": "abc123",
"category": "seat",
"name": "Seat 12C",
"segmentKey": "LAX-JFK-20250610",
"passengerType": "ADT",
"pricing": {
"display": {
"amount": 25,
"currency": "USD"
}
},
"metadata": {
"seat": {
"seatColumn": "C",
"seatRow": 12,
"seatNumber": "12C",
"seatType": "standard",
"position": "aisle",
"available": true
}
}
}Key fields
| Field | Description |
|---|---|
serviceId | Unique ID. Used when attaching selected seats to the booking. |
segmentKey | Which flight leg this seat belongs to. Used to filter seats per segment. |
metadata.seat.seatColumn | Letter column: "A", "B", "C", etc. |
metadata.seat.seatRow | Numeric row: 1, 2, 12, etc. |
metadata.seat.seatNumber | Combined: "12C". If absent, parsed from name field. |
metadata.seat.seatType | "standard", "extra_legroom", "exit_row" |
metadata.seat.position | "window", "middle", "aisle" — used to detect aisles |
metadata.seat.available | false = seat exists but cannot be selected |
Fallback: If
metadata.seatis absent, the column/row/number are parsed from thenamefield using the pattern/\b(\d+)([A-Za-z]+)\b/(e.g.,"Seat 12C"→ row12, colC). In this caseavailabledefaults totrue.
2. Grid Building Algorithm
The flat list is converted into a 2D grid in 7 steps:
Step 1 — Extract unique columns and rows
const columnSet = new Set<string>();
const rowSet = new Set<number>();
for (const s of seats) {
if (m.seatColumn) columnSet.add(m.seatColumn);
if (m.seatRow !== undefined) rowSet.add(m.seatRow);
}
const columns = [...columnSet].sort(); // alpha: ["A","B","C","D","E","F"]
const rowNumbers = [...rowSet].sort((a, b) => a - b); // numeric: [1, 2, 3, ...]Columns come from the union of ALL seats across ALL rows. If row 3 is missing column B but other rows have it, column B still exists in the grid — row 3 position B will be null (rendered as unavailable).
Step 2 — Detect dominant position per column
For each column, count how often each position (window, middle, aisle) appears across available seats. The most frequent one wins and determines whether that column is treated as an aisle column.
// dominant: 'window' | 'middle' | 'aisle'
colPositionMap.set(col, dominant);Step 3 — Calculate aisle gaps
Aisle gap = visual empty space between seat groups (left group | aisle | right group).
Rule: if two consecutive columns are both aisle-dominant, insert a gap before the second one.
Fallback: if no consecutive aisles are found (data has no position info), insert one gap at the midpoint of the columns array.
if (gapBeforeColIndices.size === 0 && columns.length >= 2) {
gapBeforeColIndices.add(Math.floor(columns.length / 2));
}Step 4 — Build seat lookup map
const lookup = new Map<string, PrebookService>();
// key: "12-C" → service object
lookup.set(`${row}-${col}`, s);Step 5 — Detect exit rows
if (m.seatType === "exit_row" && m.seatRow !== undefined) {
exitRowNumbers.add(m.seatRow);
}Step 6 — Build the grid
For every (rowNumber, column) combination, look up the service. If nothing found, service = null.
const rows: SeatGridRow[] = rowNumbers.map((row) => ({
row,
isExitRow: exitRowNumbers.has(row),
cells: columns.map((col, colIdx) => ({
col,
service: lookup.get(`${row}-${col}`) ?? null, // null = no data for this position
color: seatTypeColor(service ? meta(service).seatType : undefined),
isGapBefore: gapBeforeColIndices.has(colIdx),
})),
}));Step 7 — Build legend
One entry per seat type actually present in the data, in this order: extra_legroom → exit_row → standard.
3. Data Structures
interface SeatGridResult {
columns: string[]; // ["A","B","C","D","E","F"]
rows: SeatGridRow[];
colorRanges: Array<{ color: string; label: string }>; // for legend
}
interface SeatGridRow {
row: number;
cells: SeatCell[];
isExitRow: boolean;
}
interface SeatCell {
col: string;
service: PrebookService | null; // null = no seat at this position
color: string;
isGapBefore: boolean; // render aisle gap before this cell
}4. Seat Type Colors
const SEAT_TYPE_COLORS = {
extra_legroom: "var(--theme-success-400, #4ade80)", // green
exit_row: "#F2C200", // gold
standard: "var(--theme-primary, #4951EF)", // project primary
};Cells with service = null or unknown type get standard color (though they render as unavailable, so color is not visible).
5. Visual States
Each cell in the grid has one of five visual states, determined in order:
| State | Condition | Visual |
|---|---|---|
| Unavailable | service === null (no data for this position) | Gray, × symbol, disabled |
| Unavailable | service.metadata.seat.available === false | Gray, × symbol, disabled |
| Occupied | Another passenger already selected this seat | Colored, shows passenger initials, disabled |
| Selected | Current passenger selected this seat | White + colored border, shows seat number + price |
| Available | None of the above | Colored background, shows price |
Note on null vs unavailable: The API only returns seats it knows about. If a grid position has no service (
null), it means the airline didn't provide data for that slot — it should be treated as unavailable, not ignored.
6. Attaching Selected Seats to Booking
After the user selects seats, call:
POST /v1/flights/prebooks/:prebookId/services
Body:
{
"selectedServices": [
{ "passengerIndex": 0, "serviceId": "abc123", "quantity": 1 },
{ "passengerIndex": 1, "serviceId": "xyz789", "quantity": 1 }
]
}passengerIndex = zero-based index matching order in the prebook passengers array.
7. Minimal Implementation Checklist
If you're building a seat map from scratch using this same data:
- Fetch prebook → extract
servicesAttachable.groups[].services[]wherecategory === 'seat' - Filter by
segmentKeyfor each flight leg - Extract unique columns (alpha sort) and rows (numeric sort) from seat metadata
- Build a lookup map:
"row-col"→ service - For each
(row, col)pair: look up service — if null, mark position as unavailable - Detect aisle position: consecutive aisle-dominant columns → insert gap between them
- Detect exit rows:
seatType === 'exit_row' - Render each cell with correct state (null/unavailable/occupied/selected/available)
- On confirm: POST selected serviceIds with passengerIndex to attach services
8. Known Edge Cases
| Case | Behavior |
|---|---|
metadata.seat absent | Parse row/col from name field. Treat as available. |
available: false in metadata | Show as unavailable (× symbol). |
| Missing seat in a row (API gap) | Grid position has service = null → show as unavailable. |
| Single aisle aircraft | Fallback: gap inserted at midpoint of columns. |
| Exit row spans multiple rows | Each row in the range is independently marked isExitRow. Exit indicator only shown once (before the first row in sequence). |
| Price = 0 | Display "Free" instead of $0. |
| Unknown currency string | Falls back to raw "USD 25" format. |
Updated 23 minutes ago
