🚌 BFM Seat Widget Nezasa integration demo

Seat selection, embedded β€” the way SmartPlanner will use it

This page simulates the SmartPlanner seat drawer. The panel on the right is BFM's seat widget, embedded exactly the way we recommend Nezasa embed it: as a web component. SmartPlanner loads one script (bfm-seat-map.es.js) and places a <bfm-seat-map> element scoped by the itinerary ref with a short-lived, origin-bound token. Seat edits write straight back to the BFM API. The older iframe embed still works and remains a supported fallback.

The end-to-end case is already live. Booking NEZ-DEMO-01 in the picker did not come from a form β€” it arrived as a signed booking_completed webhook from a simulated TripBuilder, which BFM verified (HMAC), enriched, and turned into 3 reserved seats on bus HUM-01. What you edit below is that real reservation.
  1. Sign in to BFM Admin with the Nezasa demo account (top-right link).

    Email rene.otto@nezasa.com β€” password shared separately. Open Bookings to find NEZ-DEMO-01, or Buses β†’ HUM-01 β†’ seat map to see the fleet side.

  2. Pick the itinerary ref in the drawer on the right.

    NEZ-DEMO-01 is pre-selected. Choosing one mints a demo token and mounts the <bfm-seat-map> web component in place.

  3. Assign or move a passenger in the seat map.

    Click a reserved seat to name a passenger. Every change is written live to the BFM API and is immediately visible in the admin.

  4. That's the whole surface.

    In production the only difference: SmartPlanner's backend mints the token with a shared key (POST /v1/widget/token) instead of the open demo endpoint.

How the embed works (for the integration)

1 Β· SmartPlanner's backend mints a 15-min, origin-bound token per drawer-open. The scope is the itinerary ref β€” the TripBuilder itineraryId or the itinerary's externalRefId (our SG-BOOKING-####). BFM resolves either server-side:

POST https://bfm.p2p.travel/v1/widget/token Header: X-Mint-Key: <shared key> Body: { "ref": "<itineraryId | externalRefId>", "origin": "https://<embedding-origin>" } // legacy { "bookingRef": … } is still accepted as an alias of ref β†’ 200 { "token": "<jwt>", "exp": <unix> }

2 Β· Load the widget bundle once and place the element β€” it self-registers <bfm-seat-map>:

<script type="module" src="https://bfm.p2p.travel/assets/bfm-seat-map.es.js"></script> <bfm-seat-map api-base="https://bfm.p2p.travel" itinerary-ref="<itineraryId | externalRefId>" token="<jwt>" lang="en"></bfm-seat-map> // emits: new CustomEvent('bfm-seat-map',{ detail:{ action:'ready'|'assigned', … }})

Every seat call carries that token, is scoped to its ref, and the request origin must match the token. lang defaults to en; set lang="de" for German.

iframe fallback (still supported). Where a host can't run the custom element, embed <iframe src=".../widget.html"> and hand the token over via postMessage({ type:'bfm:set-token', token, ref, apiBase }) (bookingRef also accepted). Same token, same API, same events.
Widget API β€” attributes, events & the calls it makes

The element is self-contained: give it a scope ref + token and it renders the grid, lets the agent (re)assign seats, and writes back on its own. Full surface:

// ATTRIBUTES <bfm-seat-map api-base="https://bfm.p2p.travel" // BFM API origin (required) itinerary-ref="<itineraryId | externalRefId>" // scope ref (preferred) booking-ref="<internal ref>" // legacy alias of itinerary-ref token="<jwt>" // minted, origin-bound (required) lang="en" // 'en' (default) | 'de' poll-ms="15000"> // live-refresh interval; 0 = off </bfm-seat-map>
// EVENTS β€” a bubbling CustomEvent('bfm-seat-map') (also postMessage'd to an iframe parent) el.addEventListener('bfm-seat-map', (e) => { const d = e.detail; // { source:'bfm-seat-map', action, … } // action:'ready' β†’ widget loaded & rendered // action:'assigned' β†’ { seatId, ref, name } (a passenger was set) // action:'resize' β†’ { height } (iframe host only) });
// CALLS THE WIDGET MAKES (token-scoped to the ref β€” the host does NOT call these) GET /v1/widget/seat-map?ref=<ref> // bus + instance + seat grid + assignments POST /v1/widget/seat-map/assign // { seatId, passenger:{ title, firstName, lastName } } POST /v1/widget/seat-map/move // { assignmentId, toSeatId } POST /v1/widget/seat-map/remove // { seatId } // each carries Authorization: Bearer <token>; only seats belonging to this ref are editable