Passenger information capture in reservation sales

Passenger information capture in reservation sales can run through different paths depending on product configuration, check-in configuration, and selected trip data.

Please make sure you read the Conventions before continuing with this guide.

Prerequisites

You will need an X-API-KEY and a Basic Access Authentication token generated from your username and password.

What this guide solves

This guide explains how to decide, from API responses, which passenger-capture path to use when selling a reservation product:

  • Common passenger form (no dynamic form, no check-in capture)
  • Product dynamic form
  • Dynamic form + people lookup
  • Check-in capture during sale (simple and complex)
  • Check-in combined with dynamic forms

It also lists the endpoints and response properties you must inspect to make each decision.

Endpoints you need

Use these endpoints in your decision flow:

  1. GET https://api.betterez.com/inventory/products/{productId}
  • Purpose: detect if the selected product uses a passenger dynamic form.
  • Check: product.dynamicForms (first entry usually used by sales flow).
  1. Provider check-in configuration source
  • Integrator-friendly source: user login response from:
    • POST https://api.betterez.com/accounts/users
  • In that response, read:
    • provider.preferences.checkIn
  • The old shorthand GET /accounts/:providerId is not a public external endpoint in this context.
  • Internal/private equivalent exists as:
    • GET https://api.betterez.com/accounts/accounts/{id} (private/internal use).
  • Fields needed for decisioning:
    • provider.preferences.checkIn.enable
    • provider.preferences.checkIn.captureDuringSale
    • provider.preferences.checkIn.checkInFlow (simple or complex)
    • provider.preferences.checkIn.salesFlowFields.properties
    • provider.preferences.checkIn.salesFlowDynamicFormId
  1. GET https://api.betterez.com/inventory/trips (trip search)
  • Purpose: detect whether selected trips require check-in capture.
  • Check in selected departure and return trips:
    • segments[].requiresCheckIn
  1. GET https://api.betterez.com/accounts/dynamic-forms/{dynamicFormId}
  • Purpose: load the form definition when a product dynamic form is configured, or when check-in complex flow uses a dynamic form.
  • Check:
    • definition
    • mappedStandardBtrzFields
    • mappingSeparators
    • peopleLookupEnabled (product dynamic form use case)
    • peopleLookupFields (product dynamic form use case)
    • peopleLookupTrigger (product dynamic form use case)
  1. GET https://api.betterez.com/accounts/people-lookups (dynamic-form path)
  • Purpose: fetch a person profile for dynamic-form sales flows.
  • Typical filters: documentTypeId, documentNumber, optionally dynamicFormId, providerId.
  • Response to inspect: people[].
  1. POST https://api.betterez.com/accounts/people-lookups or PUT https://api.betterez.com/accounts/people-lookups/{personId} (dynamic-form path)
  • Purpose: create/update lookup records and persist data mapped from dynamic forms.
  1. GET https://api.betterez.com/operations/passenger-check-in-info (check-in path)
  • Purpose: fetch existing check-in information by document data in check-in flows.
  • Required query: documentType, documentNumber.
  • Optional query: providerId.
  • Response to inspect: passengerCheckInInfos[].

Decision model (runtime)

Use this order of evaluation after selecting the product and searching/selecting trips:

  1. Evaluate whether check-in capture is enabled for sale.
  2. Evaluate whether at least one selected trip segment requires check-in.
  3. If both are true, use check-in passenger capture.
  4. Otherwise, evaluate product dynamic form.
  5. If product has dynamic form, use dynamic form flow.
  6. If neither applies, use common/basic passenger capture.

The check-in path has priority over the product dynamic-form path when requiresCheckIn is true in selected trips.

Case 1: Common passenger capture (baseline)

Use this when:

  • Product has no passenger dynamic form (product.dynamicForms empty or no usable id), and
  • Check-in capture path is not active for selected trips.

Behavior:

  • Capture standard passenger fields in the reservation payload sent to POST https://api.betterez.com/sales/cart.
  • Passenger object format (common flow, no dynamic form/check-in override):
    • Minimum practical fields:
      • fareId (string, ObjectId)
      • firstName (string)
      • lastName (string)
    • Fields that can actually carry user/business data in common flow:
      • firstName (string)
      • lastName (string)
      • email (string|null)
      • comments (string)
      • fare (string)
      • fareId (string)
      • fareIds (object)
      • fareClassIds (object)
      • ssrs (array)
      • items (array)
      • seats (array)
      • extra (object, includes gender and folios)
      • syncEntryId (string)
      • extraInput (string)
  • Example passenger object:
{
  "fareId": "665f5d9f3f3b4b0012345678",
  "fare": "Adult",
  "fareIds": {"outbound": "665f5d9f3f3b4b0012345678"},
  "fareClassIds": {"outbound": ""},
  "ssrs": [],
  "items": [],
  "seats": [],
  "firstName": "Jane",
  "lastName": "Doe",
  "email": null,
  "comments": "",
  "extra": {"gender": "female", "folios": []},
  "syncEntryId": "a8f9e7b2-9fcb-4f0f-b4f4-11b8d60c1d0a",
  "extraInput": ""
}
  • Do not call people lookup as a mandatory step.
  • Do not use check-in-specific lookup endpoint.

Case 2: Product dynamic form (without people lookup)

Use this when:

  • Product has dynamic form id, and
  • Check-in capture path is not active for selected trips.

Behavior:

  1. Read product.dynamicForms[0]._id.
  2. Call GET https://api.betterez.com/accounts/dynamic-forms/{dynamicFormId}.
  3. Build your passenger UI from definition.
  4. Map data to standard fields using mappedStandardBtrzFields and mappingSeparators.

If peopleLookupEnabled is false or missing, you can skip people lookup endpoints.

Case 3: Product dynamic form with people lookup

Use this when:

  • Product dynamic form path is active, and
  • peopleLookupEnabled is true in the dynamic-form response.

Behavior:

  1. Use peopleLookupFields and peopleLookupTrigger to decide when to query lookup.
  2. Call GET https://api.betterez.com/accounts/people-lookups with the configured lookup keys.
  3. If people[] has data, prefill passenger fields.
  4. If no result, collect data and create/update lookup profile:
    • POST https://api.betterez.com/accounts/people-lookups, or
    • PUT https://api.betterez.com/accounts/people-lookups/{personId}.
  5. Keep peopleLookupId per passenger to reuse in cart payloads.

Case 4: Check-in capture during sale

Check-in capture is active only when all of the following are true:

  • provider.preferences.checkIn.enable is true
  • provider.preferences.checkIn.captureDuringSale is true
  • Check-in has configured fields for sales flow (provider.preferences.checkIn.salesFlowFields.properties not empty or provider.preferences.checkIn.salesFlowDynamicFormId present)
  • At least one selected trip segment has requiresCheckIn = true

If any condition fails, do not use check-in capture as the passenger form for that selection.

Simple vs complex check-in flow

Use provider.preferences.checkIn.checkInFlow to understand configuration mode:

  • simple

    • Uses provider default check-in configuration.
    • No custom advanced configuration should be expected from integrator perspective.
  • complex

    • Sales check-in fields can be customized through:
      • salesFlowFields (JSON schema-like definition), or
      • salesFlowDynamicFormId (check-in dynamic form definition loaded from dynamic-forms API).

For complex, if salesFlowDynamicFormId exists, prefer loading that dynamic form to build check-in field definitions.

Check-in lookup endpoint

For check-in forms, use:

  • GET https://api.betterez.com/operations/passenger-check-in-info?documentType=...&documentNumber=...&providerId=...

Then:

  • If passengerCheckInInfos[0] exists, prefill check-in capture fields.
  • If empty, continue with manual capture.

This is different from product dynamic-form people lookup, which uses https://api.betterez.com/accounts/people-lookups.

Case 5: Check-in combined with dynamic forms

There are multiple combinations to consider.

Combination A: Product dynamic form + check-in capture

When both are configured, selected-trip requirement decides which one drives passenger capture:

  • If any selected segment has requiresCheckIn = true, use check-in capture.
  • If no selected segment requires check-in, use product dynamic form.

This means the same product can use different passenger-capture UIs depending on trip selection.

Combination B: Complex check-in using salesFlowDynamicFormId

In this case, check-in itself is built from a dynamic-form definition (type check-in configuration), but runtime behavior is still check-in flow:

  • Trigger remains requiresCheckIn + provider check-in sale settings.
  • Lookup endpoint remains https://api.betterez.com/operations/passenger-check-in-info.
  • Do not treat this as product passenger dynamic-form people-lookup flow unless your own business rule explicitly combines both.

Combination C: Check-in plus additional paid-in item dynamic forms

If your sale includes paid-in item forms, these can coexist with check-in capture:

  • Passenger capture stays in check-in flow (when required by selected segments).
  • Additional item-level forms may still use dynamic-form + people-lookup behavior per item form configuration.

Treat passenger-level check-in and item-level dynamic forms as separate capture layers.

Backoffice UI examples (betterez-app/app/modules/sales-reservations-2):

  1. Check-in form confirms passengers and then persists paid-in item lookups before cart completion.

    • UI behavior:
      • When check-in form is active and paidInDynamicFormFields exists, CheckInPaxFormService.confirmPassengers() calls createOrUpdateLookupPaidIn() and only then calls finishCart().
    • This means paid-in people lookup persistence is part of the check-in confirmation step, not a separate checkout stage.
  2. Paid-in dynamic form is detected from inventory items and loaded with its own definition.

    • UI behavior:
      • getPaidInDynamicForm() picks the first item that matches type === "paid_in", delayAttachToTicket === true, and has dynamicForm._id.
      • It then loads dynamic-form metadata (dynamicFormId, dynamicFormDefinition, lookup flags) specifically for item-level capture.
  3. Paid-in people lookup create/update uses https://api.betterez.com/accounts/people-lookups with dynamicForms payload.

    • UI behavior:
      • For each selected paid-in item, the UI checks existing people lookup by document data.
      • If found, it updates; if not, it creates.
    • Payload shape used by the UI for each paid-in lookup upsert:
{
  "person": {
    "dynamicForms": {
      "{{paidInDynamicFormId}}": {
        "documentType": "{{documentTypeId}}",
        "documentNumber": "{{documentNumber}}",
        "...": "other paid-in dynamic form fields"
      }
    }
  }
}
  1. After lookup upsert, UI maps returned person data back into item formData.
    • UI behavior:
      • Returned person.dynamicFormsFields values are copied into passengers[].items[].formData.
      • peopleLookupId is stored at the item form level for later cart submission consistency.

Cart payload patterns (what to send)

All cases below are sent to:

  • POST https://api.betterez.com/sales/cart

For complete cart body context, see:

A) Dynamic form flow payload

Use this when product dynamic form is active and check-in is not active for selected trips.

Key points:

  • Set items.reservation[0].dynamicFormId.
  • In each passenger, include standard fields.
  • If people lookup is used, include peopleLookupId per passenger.
{
  "items": {
    "reservation": [
      {
        "productId": "{{productId}}",
        "from": {"_id": "{{originStationId}}"},
        "to": {"_id": "{{destinationStationId}}"},
        "selectedTrips": {"departureTripId": "{{departureTripId}}", "returnTripId": ""},
        "roundTrip": false,
        "dateFrom": "{{YYYY-MM-DD}}",
        "dynamicFormId": "{{dynamicFormId}}",
        "passengers": [
          {
            "fareId": "{{fareId}}",
            "firstName": "Jane",
            "lastName": "Doe",
            "documentTypeId": "{{documentTypeId}}",
            "documentNumber": "A1234567",
            "email": "jane@example.com",
            "phone": "+14165550101",
            "peopleLookupId": "{{peopleLookupId}}",
            "ssrs": [],
            "seats": []
          }
        ]
      }
    ]
  }
}

B) Check-in flow payload

Use this when check-in is active and selected trip segments include requiresCheckIn = true.

Key points:

  • Send passenger capture in passengers[].checkInInfo.
  • Do not set reservation dynamicFormId for passenger capture in this case.
  • Keep standard passenger fields (firstName, lastName, documentTypeId, documentNumber, etc.) for compatibility and downstream processes.
{
  "items": {
    "reservation": [
      {
        "productId": "{{productId}}",
        "from": {"_id": "{{originStationId}}"},
        "to": {"_id": "{{destinationStationId}}"},
        "selectedTrips": {"departureTripId": "{{departureTripId}}", "returnTripId": ""},
        "roundTrip": false,
        "dateFrom": "{{YYYY-MM-DD}}",
        "passengers": [
          {
            "fareId": "{{fareId}}",
            "firstName": "Jane",
            "lastName": "Doe",
            "documentTypeId": "{{documentTypeId}}",
            "documentNumber": "A1234567",
            "ssrs": [],
            "seats": [],
            "checkInInfo": {
              "documentType": "{{documentTypeId}}",
              "documentNumber": "A1234567",
              "dateOfBirth": "1992-03-14",
              "nationality": "CA"
            }
          }
        ]
      }
    ]
  }
}

C) Product dynamic form + check-in both configured

There is no single combined passenger payload in runtime.

  • If selected segments require check-in:
    • Use payload pattern B (checkInInfo), and do not use reservation dynamicFormId for passenger capture.
  • If selected segments do not require check-in:
    • Use payload pattern A (set reservation dynamicFormId, optionally peopleLookupId).

Practical decision checklist

For each reservation sale:

  1. Load product (https://api.betterez.com/inventory/products/{productId}).
  2. Load provider check-in preferences from user login response (POST https://api.betterez.com/accounts/users), using provider.preferences.checkIn.
  3. Search/select trips (https://api.betterez.com/inventory/trips) and inspect selected segments[].requiresCheckIn.
  4. Apply precedence:
    • Check-in active + requiresCheckIn => check-in flow.
    • Else product dynamic form => dynamic-form flow.
    • Else common flow.
  5. In dynamic-form flow:
    • Load dynamic form by id.
    • If peopleLookupEnabled, execute people lookup flow.
  6. In check-in flow:
    • Resolve fields from configured sales-flow definition.
    • Use passenger check-in info lookup endpoint for prefill.

Notes and implementation recommendations

  • Always evaluate decisions after trip selection, not only after product selection, because requiresCheckIn is trip-dependent.
  • In round trips, evaluate departure and return segments; if either side requires check-in, choose check-in flow.
  • Keep lookup flows distinct:
    • Dynamic-form person profiles => https://api.betterez.com/accounts/people-lookups
    • Check-in passenger history => https://api.betterez.com/operations/passenger-check-in-info
  • When check-in is complex and field definitions are customized, rely on configured schema/dynamic-form definitions instead of hardcoding field lists.