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

FieldDescription
serviceIdUnique ID. Used when attaching selected seats to the booking.
segmentKeyWhich flight leg this seat belongs to. Used to filter seats per segment.
metadata.seat.seatColumnLetter column: "A", "B", "C", etc.
metadata.seat.seatRowNumeric row: 1, 2, 12, etc.
metadata.seat.seatNumberCombined: "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.availablefalse = seat exists but cannot be selected

Fallback: If metadata.seat is absent, the column/row/number are parsed from the name field using the pattern /\b(\d+)([A-Za-z]+)\b/ (e.g., "Seat 12C" → row 12, col C). In this case available defaults to true.


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_legroomexit_rowstandard.


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:

StateConditionVisual
Unavailableservice === null (no data for this position)Gray, × symbol, disabled
Unavailableservice.metadata.seat.available === falseGray, × symbol, disabled
OccupiedAnother passenger already selected this seatColored, shows passenger initials, disabled
SelectedCurrent passenger selected this seatWhite + colored border, shows seat number + price
AvailableNone of the aboveColored 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:

  1. Fetch prebook → extract servicesAttachable.groups[].services[] where category === 'seat'
  2. Filter by segmentKey for each flight leg
  3. Extract unique columns (alpha sort) and rows (numeric sort) from seat metadata
  4. Build a lookup map: "row-col" → service
  5. For each (row, col) pair: look up service — if null, mark position as unavailable
  6. Detect aisle position: consecutive aisle-dominant columns → insert gap between them
  7. Detect exit rows: seatType === 'exit_row'
  8. Render each cell with correct state (null/unavailable/occupied/selected/available)
  9. On confirm: POST selected serviceIds with passengerIndex to attach services

8. Known Edge Cases

CaseBehavior
metadata.seat absentParse row/col from name field. Treat as available.
available: false in metadataShow as unavailable (× symbol).
Missing seat in a row (API gap)Grid position has service = null → show as unavailable.
Single aisle aircraftFallback: gap inserted at midpoint of columns.
Exit row spans multiple rowsEach row in the range is independently marked isExitRow. Exit indicator only shown once (before the first row in sequence).
Price = 0Display "Free" instead of $0.
Unknown currency stringFalls back to raw "USD 25" format.


Did this page help you?