Journeys API reference
Every endpoint currently exposed by the Edge Journeys API. All routes are versioned under:
/api/journeys/v1
The same contract is available machine-readable: OpenAPI 3.1 specification — import it into Postman, generate a client, or hand it to an AI coding agent.
Examples preserve the Meridian Voyages cruise reference implementation. The same API also supports group-touring catalogs; neutral route aliases and touring-specific semantics are called out below.
Conventions
Tenant scoping. Every request requires a tenantId — as a query parameter for GET/DELETE, or a body field for POST/PUT/PATCH. Requests without one receive 400. Unknown tenants receive 404.
Response envelope. Successful responses wrap payloads in a data key:
{ "data": { } }
Basket lifecycle. A basket is a short-lived commerce session, not a persistent cart:
| Property | Behaviour |
|---|---|
| Expiry | Baskets expire 15 minutes after creation. Expired baskets return 404; the UI should recreate the basket and replay selections if a guest returns late. |
| Status | open → priced (after a pricing calculation) → booked (after checkout). Status only moves forward. |
| Inventory holds | Selecting an inventory-backed room or cabin places a temporary inventory hold with its own expiry, carried through checkout. The exact hold granularity follows the configured catalog model. |
| One booking per basket | A basket can be booked exactly once. A second POST /bookings on the same basket returns 409 — double-submission cannot create a duplicate itinerary. |
Idempotency and retries. GET requests are always safe to retry. PUT and PATCH requests are idempotent by design (they replace state). The two money-adjacent operations have explicit protection: booking creation is guarded by basket status (409 on replay, as above), and POST /payment-requests refreshes the existing open payment request for the basket rather than creating a duplicate. An explicit Idempotency-Key request header (as used by Edge Webforms) is being extended to the Journeys booking and payment endpoints during private beta.
Currency and localization. Every pricing operation is explicit about currency (currency_code on the basket, currency on catalog reads) — multi-currency is first-class. Catalog content (trip names, room or cabin categories, activities) is served in the language of the tenant's product catalog. Multi-language catalog delivery — a language parameter resolving localized content per request — is scoped per tenant during onboarding for markets that require it; raise it with your implementation team as part of private beta configuration.
Neutral catalog routes. New integrations should use /catalog/trips and /catalog/rooms, which speak a product-neutral dialect: tripCode, tripName, and roomCode, with vessel and deck fields present only when the product actually has them. The original /catalog/voyages and /catalog/cabins routes are unchanged and keep the established voyageCode, voyageName, and cabinNumber wire fields for existing integrations.
Error contract. Errors return a JSON body with an error message and a meaningful status:
| Status | When |
|---|---|
| 400 | Missing/invalid parameters (tenantId, required fields) |
| 403 | Basket belongs to a different tenant; disabled feature |
| 404 | Tenant not configured; basket or payment request not found |
| 409 | Basket already booked; payment not completed before booking |
| 422 | Booking attempted on a basket with no component selections |
| 429 | Rate limit exceeded (quote endpoint) |
| 502 | An upstream platform service (KTAPI, Basket Service) returned an error |
| 503 | Tenant's live configuration is incomplete; feature not configured |
Catalog
List trips
GET /api/journeys/v1/catalog/trips?tenantId=meridian
GET /api/journeys/v1/catalog/voyages?tenantId=meridian # legacy cruise dialect
Returns every trip (Kaptio package departure) in the tenant's configured catalog, with departure dates and from-prices per currency. In live mode the source depends on live.catalog_model: cruise uses cruise details and cabin inventory; touring prices required room options from the package. Results are cached.
{
"data": [
{
"tripCode": "ANT-2728",
"tripName": "Antarctic Horizons",
"region": "Antarctica",
"durationDays": 15,
"vesselName": "Meridian Explorer",
"departureDates": [
{ "label": "Nov 2027", "date": "2027-11-18" },
{ "label": "Dec 2027", "date": "2027-12-14" }
],
"fromPrice": 13995,
"currency": "USD",
"fromPriceUSD": 13995,
"fromPriceAUD": 21295,
"fromPriceGBP": 11095,
"fromPriceEUR": 12895,
"fromPriceNZD": 22995,
"fromPriceCAD": 18995,
"sellingStatus": "Open",
"season": "2027-2028"
}
]
}
fromPrice is null (never 0) when no valid price could be resolved for a trip. vesselName appears only for products operated on a vessel — a group-touring trip carries no vessel fields at all. The legacy /catalog/voyages route returns the same rows with the original voyageCode, voyageName, and shipName field names.
List room categories
GET /api/journeys/v1/catalog/categories?tenantId=meridian
Returns room or cabin categories available across the catalog, grouped into the tier structure defined in journey.yaml (catalog.category_tiers). Cruise rows describe cabin grades. Touring rows describe required package room options, commonly shared and single occupancy.
{
"data": [
{
"kaptioName": "Balcony Stateroom C",
"label": "Balcony Stateroom",
"tier": "Balcony",
"tierGroup": "Staterooms",
"isSolo": false,
"isPopular": true,
"sortOrder": 3
}
]
}
List rooms
GET /api/journeys/v1/catalog/rooms?tenantId=meridian&tripCode=ANT-2728&categoryKaptioName=Balcony%20Stateroom%20C¤cy=USD
GET /api/journeys/v1/catalog/cabins?tenantId=meridian&voyageCode=ANT-2728&categoryKaptioName=Balcony%20Stateroom%20C¤cy=USD # legacy cruise dialect
Returns bookable room options for a trip and category, with live availability and per-person pricing. In cruise mode, a row identifies a physical cabin with its deck and location. In touring mode, a row represents a room configuration such as Twin or Single, and no deck field is returned.
| Query parameter | Required | Description |
|---|---|---|
tenantId | Yes | Tenant slug |
tripCode | Yes | Trip reference from the catalog response (the legacy route uses voyageCode) |
categoryKaptioName | Yes | Category kaptioName from the categories response |
currency | Yes | ISO 4217 currency code |
{
"data": [
{
"id": 512,
"roomCode": "512",
"deck": 5,
"location": "Midship",
"priceAmount": 14495,
"priceCurrency": "USD",
"priceBasis": "per person",
"berthsAvailable": 2,
"componentId": "a1B0000000000AAAA",
"optionId": 3,
"itemOptionId": "a1C0000000000AAAA",
"cabinTypeId": "a1D0000000000AAAA"
}
]
}
The selection-context fields are opaque — pass the values returned for that row back verbatim via Update components. Cruise rows can include cabinTypeId; touring rows are selected through their package component, option, and item-option identifiers.
List activities
GET /api/journeys/v1/catalog/activities?tenantId=meridian&voyageCode=ANT-2728¤cy=USD
Returns optional package activities for a trip (e.g. a guided excursion or workshop) with pricing and remaining availability. Externally quoted ancillaries are separate; see Extending journeys with third-party services.
{
"data": [
{
"activityName": "Sea Kayaking Programme",
"priceAmount": 1250,
"priceCurrency": "USD",
"unitsAvailable": 8
}
]
}
Configuration
Get journey config
GET /api/journeys/v1/config?tenantId=meridian
Returns the tenant's validated journey.yaml — steps, channels, branding, behavior, and catalog metadata. The hosted tenant UI calls this on load; a custom frontend can use it to drive step order and branding from the same configuration. See the journey.yaml reference for the shape.
Advisors (B2B)
Look up advisor
POST /api/journeys/v1/advisors/lookup
Identifies a travel advisor before a B2B booking flow begins (channels with requires_advisor_id: true).
{
"tenantId": "meridian",
"iata_number": "91234567",
"email": "advisor@agency.example"
}
Response is a discriminated union on status:
{
"data": {
"status": "matched",
"account": {
"account_id": "0010000000000AAAA",
"agency_name": "Polar Travel Co",
"iata_number": "91234567",
"email": "advisor@agency.example",
"country": "US"
}
}
}
status is one of matched, not_found, or ambiguous (multiple accounts share the IATA number). For not_found and ambiguous, account is absent.
Baskets
Create basket
POST /api/journeys/v1/baskets
Starts a commerce session for a package departure. Returns 201.
{
"tenantId": "meridian",
"package_id": "a0P0000000000AAAA",
"departure_id": "a0Q0000000000AAAA",
"occupancy": [{ "adults": 2, "children": 0 }],
"channel_id": "b2c-direct",
"currency_code": "USD"
}
| Field | Type | Required | Description |
|---|---|---|---|
package_id | string | Yes | KTAPI package ID (from the trip catalog) |
departure_id | string | Yes | Departure ID for the selected date |
occupancy | array | Yes | One entry per room or cabin: { adults, children } |
channel_id | string | Yes | Journey channel ID (mapped to a Salesforce channel via live.channel_map) |
currency_code | string | Yes | ISO 4217 currency |
account_id | string | No | Salesforce account (defaults to live.default_account_id) |
tax_profile_id | string | No | Overrides live.tax_profile_id |
service_level_id | string | No | Overrides live.service_level_id |
{
"data": {
"id": "bkt_01HZXX0000000000000000",
"tenant_id": "meridian",
"channel_id": "b2c-direct",
"package_id": "a0P0000000000AAAA",
"departure_id": "a0Q0000000000AAAA",
"currency_code": "USD",
"occupancy": [{ "adults": 2, "children": 0 }],
"components": [
{ "component_id": "a1B0000000000AAAA", "selections": [] }
],
"travellers": [],
"promotions": [],
"status": "open",
"created_at": "2027-06-01T10:00:00Z"
}
}
Get basket
GET /api/journeys/v1/baskets/:basketId?tenantId=meridian
Returns the current basket state — components, selections, travellers, promotions, status.
Delete basket
DELETE /api/journeys/v1/baskets/:basketId?tenantId=meridian
Abandons the session. Returns 204 with no body.
Update component selections
PATCH /api/journeys/v1/baskets/:basketId/components
Sets option selections on basket components — this is how an inventory-backed room, cabin, or other package option is added. The selection context comes verbatim from the rooms response.
{
"tenantId": "meridian",
"components": [
{
"component_id": "a1B0000000000AAAA",
"selections": [
{
"option_id": "3",
"item_option_id": "a1C0000000000AAAA",
"cabin_id": "512",
"travellers": [1, 2]
}
]
}
]
}
Returns the updated components array.
Update travellers
PUT /api/journeys/v1/baskets/:basketId/travellers
Replaces the traveller list. One traveller should carry contact: true as the lead passenger.
{
"tenantId": "meridian",
"travellers": [
{
"id": "1",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+1 555 0100",
"date_of_birth": "1980-04-12",
"contact": true
},
{
"id": "2",
"first_name": "Alex",
"last_name": "Smith",
"email": "alex@example.com"
}
]
}
Returns the updated travellers array.
Promotions
Apply promotion
POST /api/journeys/v1/baskets/:basketId/promotions
{ "tenantId": "meridian", "code": "EARLYBIRD15" }
Validates the code against the platform promotion engine and attaches it to the basket. Returns the updated basket. Invalid or non-combinable codes return an upstream validation error.
Remove promotion
DELETE /api/journeys/v1/baskets/:basketId/promotions/:code?tenantId=meridian
Removes the code and returns the updated basket.
Pricing and payments
Get pricing
GET /api/journeys/v1/baskets/:basketId/prices?tenantId=meridian
Calculates the full price for the current basket state — selections, occupancy, and applied promotions — via the platform pricing engine.
{
"data": {
"subtotal": 28990,
"taxes": 2029,
"total": 31019,
"currency": "USD",
"deposit_amount": 6204,
"deposit_percentage": 20,
"balance_due": 24815,
"balance_due_date": "2027-08-20",
"line_items": [
{ "label": "Balcony Stateroom C — Cabin 512 x 2", "amount": 28990, "currency": "USD" }
]
}
}
Commission fields are optional. An eligible trade basket can additionally return:
{
"commission_amount": 3101.9,
"commission_lines": [
{ "label": "Agency commission", "amount": 3101.9, "currency": "USD" }
]
}
It is present only when KTAPI returns positive price lines with applicable_to: reseller_commission for the basket's account and channel. Journeys aggregates those lines across passengers without adding them to or subtracting them from subtotal, taxes, total, or line_items.
commission_lines groups the same KTAPI commission lines by display label. It does not allocate commission to individual line_items; customer sales lines remain separate.
Commission terminology
| Term | Meaning in Journeys |
|---|---|
| Sales amount | Customer-facing price. KTAPI price lines with applicable_to: sales contribute to total and line_items. |
| Agent or reseller commission | Commercial entitlement produced by the matching reseller agreement and commission-group assignment. It is exposed as aggregate commission_amount when present. |
| Net | A channel-specific commercial view often calculated as sales less agent commission. BasketPricing does not expose net_amount; do not treat supplier cost, payment settlement, or a sales line as the net value. |
Commission is configured in Kaptio, not in journey.yaml. Commission groups classify commissionable packages, price categories, items, or fees. A reseller agreement assigns rates to those groups and is eligible only when its account and channel assignments match the basket. A B2B journey therefore needs the advisor/account resolution and live.channel_map context to match the intended agreement.
Quote and select an external service
External services are configured per tenant and remain outside Basket Service. Quote requests accept only basket traveller IDs and bounded scalar metadata; Journeys loads trip, departure, currency, and date-of-birth context from the basket.
POST /api/journeys/v1/baskets/:basketId/external-ancillaries/:providerId/quotes?tenantId=meridian
{
"traveller_ids": ["1", "2"],
"metadata": { "market": "AU" }
}
The response contains normalized items with a stable provider_ref and an opaque, basket-bound quote_token. Select one item:
POST /api/journeys/v1/baskets/:basketId/external-ancillaries/:providerId/selections?tenantId=meridian
{
"quote_token": "{opaque-token}",
"item_id": "standard-cover",
"quantity": 2,
"traveller_ids": ["1", "2"]
}
The response includes a normalized selection and selection_token. The signed selection fixes its amount, currency, source, provider reference, traveller applicability, and bounded metadata.
To include external selections in the review presentation, repeat externalAncillaryToken on the pricing request:
GET /api/journeys/v1/baskets/:basketId/prices?tenantId=meridian&externalAncillaryToken={token}
The response keeps total authoritative for the Kaptio basket and adds external_ancillary_lines, external_ancillary_total, and display-only presentation_total.
Get payment schedules
GET /api/journeys/v1/baskets/:basketId/payment-schedules?tenantId=meridian
Returns the deposit/balance options for the basket, driven by the tenant's payment schedule rules.
{
"data": [
{ "id": "ps_deposit", "label": "Deposit (20%)", "amount": 6204, "due_date": "2027-06-01", "type": "deposit" },
{ "id": "ps_balance", "label": "Balance", "amount": 24815, "due_date": "2027-08-20", "type": "balance" }
]
}
Create payment request
POST /api/journeys/v1/baskets/:basketId/payment-requests
Creates a payment request against a chosen schedule. Returns 201.
{
"tenantId": "meridian",
"pay_per_person": false,
"passenger_payments": [
{ "payment_schedule_rule_id": "ps_deposit" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
passenger_payments | array | Yes | One entry per payment; passenger_id optional when paying per person |
pay_per_person | boolean | Yes | Split the payment per passenger |
{
"data": {
"id": "pr_01HZXX0000000000000000",
"basket_id": "bkt_01HZXX0000000000000000",
"amount": 6204,
"currency": "USD",
"status": "pending",
"payment_url": "https://pay.example.com/session/abc123"
}
}
Create embedded payment session
POST /api/journeys/v1/baskets/:basketId/payment-requests/:paymentRequestId/embed-session
{ "tenantId": "meridian", "payment_option": "deposit" }
Edge Pay creates the hosted session. When tenant payment methods carry a surcharge, the response includes method-specific previews calculated by Edge Pay from the current payment.yaml:
{
"data": {
"session_id": "session_123",
"embed_url": "https://pay.example.com/pay/session_123",
"expires_at": "2027-06-01T12:00:00Z",
"surcharge_options": [
{
"method_id": "card",
"method_label": "Credit card",
"percentage": 1.5,
"amount": 93.06,
"currency": "USD",
"source": "edge_pay",
"authoritative": false
}
]
}
}
Complete payment
PUT /api/journeys/v1/baskets/:basketId/payment-requests/:paymentRequestId/complete
Marks the payment request complete after Journeys verifies the hosted session server-to-server with Edge Pay. The browser supplies only the session ID.
{ "tenantId": "meridian", "session_id": "session_123" }
Returns the payment request with status: "completed". If Edge Pay recorded a surcharge, surcharge_breakdown contains its authoritative amount and currency.
Payment-method surcharges
Payment-method surcharges are owned and applied by Edge Pay. Each enabled payment method in the tenant's payment.yaml can define surcharge_percent; Edge Pay uses the selected method and server-authoritative amount breakdown to show and collect the exact fee.
Journeys does not add a payment surcharge to BasketPricing, and consumers must not model the fee as a KTAPI basket price line. surcharge_options on the embed session are previews calculated by Edge Pay. The selected method and completed transaction remain authoritative; surcharge_breakdown.authoritative is true only after completion.
An itinerary-level KTAPI Fee line is different: it is part of itinerary pricing and can affect basket sales totals regardless of how the guest pays. A payment-method surcharge is selected and calculated at payment time by Edge Pay and is governed by payment.yaml.
Checkout
Create booking
POST /api/journeys/v1/baskets/:basketId/bookings
Converts the basket into a booking — a Kaptio itinerary in the tenant's Salesforce org. Returns 201.
{
"tenantId": "meridian",
"external_ancillary_selection_tokens": ["{selection-token}"]
}
{
"data": {
"itinerary_id": "a0R0000000000AAAA",
"booking_reference": "MER-104233",
"status": "Booked"
}
}
Guards:
| Status | When |
|---|---|
| 422 | The basket has no required component selections (for example, no room chosen) |
| 409 | The basket is already booked, or a required payment is not completed |
| 501 | External selections were supplied but the configured Edge Composite persistence boundary is unavailable |
| 502 | Booking or external ancillary persistence failed upstream |
When external selections are present, Journeys verifies them and checks persistence configuration before creating the booking. After the platform returns the itinerary ID, Journeys writes the normalized selections through Edge Composite. A write failure is returned explicitly; retry resumes the booking and repeats the idempotent ancillary write.
Booking lifecycle scope
POST /baskets/:basketId/bookings creates a new itinerary. The current public Journeys API does not expose execution routes to amend an existing itinerary, cancel a booking, or issue a refund.
The underlying platform includes pricing, cancellation-rule, supplier-fee, and orchestration capabilities that can support future lifecycle routes. Those capabilities are an extension surface, not callable Journeys execution endpoints. Do not build a client around an amendment, cancellation, or refund path until it appears in this reference and the OpenAPI contract.
AI quote letter
Generate quote letter (SSE)
POST /api/quote
Streams a personalised welcome/quote letter for the confirmation screen. Only available when the tenant has behavior.ai_quote_letter: true in journey.yaml. Note: this route is mounted at /api/quote, not under /api/journeys/v1.
{
"tenantId": "meridian",
"prompt": "Write a warm welcome letter for Jane Smith, booked on Antarctic Horizons departing 18 Nov 2027..."
}
The response is a raw server-sent-event stream (Content-Type: text/event-stream) of Anthropic Messages API events — the UI renders content_block_delta text as it arrives, and falls back to static copy on any failure.
Guardrails on this public endpoint:
| Guard | Limit |
|---|---|
| Prompt size | 6,000 characters |
| Response size | 500 tokens |
| Per-IP rate limit | 5 requests / minute |
| Per-tenant rate limit | 30 requests / minute |
| Feature gate | 403 unless enabled in journey.yaml |
Health and operations
| Method | Path | Purpose |
|---|---|---|
| GET | /health | Service liveness |
| GET | /ready | Readiness (config repo reachable) |
| GET | /api/journeys/v1/health-check | API-level health ({ "status": "ok", "api": "journeys-v1" }) |
Prometheus metrics (baskets created, bookings completed, quote outcomes — all labelled per tenant and channel) are exposed on a separate metrics port and are not part of the public surface.