Recommendations Guide

When searching for public transport tickets, the API returns numerous offers for the same journey, varying by flexibility, fare class, accommodation, and other properties. Recommendations group these offers into categories and select the best offer in each category. For journeys with multiple legs, recommendations can also combine offers across legs to cover the entire journey.

The API does not guarantee recommendations for every requested category. A category may be empty if, for example, the product does not exist for that departure (e.g., sleeper is only available on night trains), capacity is sold out, or your organisation is not authorised to sell a particular operator's products.

Any search endpoint that takes geographical input supports recommendations (e.g., POST /trip-patterns, POST /stop-places, POST /zones). Add a recommendationConfig to your search request to receive recommendations organised by the requested criteria.

Table of Contents

  1. Recommended Configuration
  2. RuleSpec
  3. CategorySpec
  4. Traveller Assignment
  5. Examples
  6. Troubleshooting

(back to Table of contents)

Minimal configuration

To get started, add a recommendationConfig to your search request. Without this config, the API returns offers but no recommendations.

This returns the cheapest offer for the entire journey. All other settings use defaults.

Production configuration

For production use, enable the recommended flags. This configuration covers most use cases.

These are the recommended flag values for most integrations. See CategorySpec and RuleSpec for details on each field, and Examples for behaviour illustrations.

If you sell long-distance train tickets, also add fareClasses and facilitySet to the categorySpec to get recommendations split by fare class (e.g., standard vs. premium) and accommodation type (e.g., sleeper, couchette). See CategorySpec for available values.

Response structure

The API response contains two related arrays:

  • offers[] — All offer objects with full details (price, conditions, validity)
  • recommendations[] — Categories pointing to offers via offersToBuy

In this example, the recommendation selects offer-1 as the cheapest option in the FLEXIBLE + STANDARD_CLASS category. Both offers cover the same journey with the same flexibility, but differ in price.

Key fields in a recommendation:

  • typeOfRecommendation — The flexibility category (CHEAPEST, NON_FLEXIBLE, etc.)
  • geographicalValidityCovered — Which part of the journey this covers:
    • coversEntireJourney — Boolean indicating if this recommendation covers the full journey
    • serviceJourneys — List of service journey IDs covered, only applicable when requesting offers for a specific TripPattern
    • pointToPointValidity — Start and end stop places, for tickets valid between specific stops
    • zonalValidity — Zone information, for tickets valid in specific zones
  • offersToBuy — The offers to reserve in the Sales API, with quantity and possible traveller assignments.
    • selectableProductIds — When populated, include these selectable IDs in the reservation request. They refer to optional products (found in the response under optionalProducts) that are available with the recommended offers.
    • possibleTravellerIds — Who can be assigned to each purchase. See Traveller Assignment.
  • combineWithToCoverEntireJourney — When this recommendation doesn't cover the entire journey, other recommendations are provided that can be combined with this one to cover the full journey. Select one item from the list, then purchase all OffersToBuy from all items in recommendationsToCoverEntireJourney, together with the parent recommendation. Only populated when onlyIncludeMostCompleteInCategorySpec: true. More details can be found in Partial journey coverage.

To display a recommendation, look up each offersToBuy[].id in the offers[] array to get price and details.

RuleSpec

(back to Table of contents)

The ruleSpec controls how recommendations are calculated and filtered.

SettingDefaultRecommendedDescription
journeyOrganizeAlgorithmSUBSEQUENT_COMBINATIONSSUBSEQUENT_COMBINATIONSHow multi-leg journeys are split into recommendation groups.
priceComparisonAlgorithmBEFORE_SDRTOTAL_PRICEHow offers are compared when selecting the best offer.
onlyIncludeRecommendedOffersfalsetrueOnly return offers referenced by at least one recommendation.
onlyIncludeRecommendationsWithOffersToBuytruetrueOmit recommendation categories with no matching offers. When true, every returned recommendation is guaranteed to have a non-empty offersToBuy.
mixinOffersWithHigherFlexibilityfalsetrueAllow higher-flexibility offers in lower categories if cheaper or the requested flexibility is unavailable on some legs. At least one offer in the recommendation must match the requested level. See example.
mixInOffersWithOtherFacilitySetsfalsetrueAllow mixing facility types to cover the full journey. For example, a SLEEPER recommendation may include a SEATING offer for legs where sleeper isn't available. At least one offer must match the requested facility. See example.
removeSplitRailReplacementOffersfalsetrueWhen a rail ServiceJourney has been partially replaced by alternative transport, recommendations will only be provided for the full ServiceJourney.
onlyKeepUpgradeProductSalesPackagesfalsefalseOnly include upgrade products. Use when client has an entitlement (e.g., monthly pass) and searches for upgrades.
onlyIncludeMostCompleteInCategorySpecfalsefalseBETA: Filter out subset recommendations. See details below.
Why do defaults differ from recommended values?
Several flags default to false because they were introduced incrementally — enabling new behaviour requires an explicit opt-in to avoid breaking existing integrations. In a future major version of the API, the recommended values will become the new defaults, and some flags may be removed entirely.

Algorithm Details

Journey Organize Algorithms

Recommendation
Use SUBSEQUENT_COMBINATIONS — it enables full-journey recommendations for multi-operator journeys where no single offer covers all legs.

The algorithm controlling how recommendations should be combined spanning multiple legs.

Example: For a journey with three legs (SJ1 → SJ2 → SJ3):

AlgorithmRecommendation groups created
SUBSEQUENT_COMBINATIONS[SJ1], [SJ2], [SJ3], [SJ1, SJ2], [SJ2, SJ3], [SJ1, SJ2, SJ3] (not [SJ1, SJ3])
FOR_EACH_AND_GROUPED_COMBINATIONS[SJ1], [SJ2], [SJ3], [SJ1, SJ2, SJ3]
COMBINATIONS_FROM_OFFERSOnly combinations matching actual offers

The choice of algorithm matters most on multi-operator journeys. Usually, there exists no tickets covering all operators, hence no single offer covers the full journey.

Consider a two-leg journey: A → B with Operator X, then B → C with Operator Y.

With COMBINATIONS_FROM_OFFERS, the API only creates groups matching existing offers — one per leg, but no full-journey group:

The client receives two separate CHEAPEST recommendations but must figure out on its own that buying both covers the full journey. With onlyIncludeMostCompleteInCategorySpec (BETA), each recommendation gets combineWithToCoverEntireJourney links pointing to the other — but the client still does the composition.

With SUBSEQUENT_COMBINATIONS, the API also creates a group for the full journey and fills it with the cheapest combination of offers:

The third recommendation has coversEntireJourney: true and lists both offers — the client gets a ready-made full-journey recommendation without combining offers itself.

Key behaviours:

  • SUBSEQUENT_COMBINATIONS only creates consecutive leg groups — non-adjacent combinations like [SJ1, SJ3] are never generated
  • FOR_EACH_AND_GROUPED_COMBINATIONS may miss optimal price combinations (no intermediate groups like [SJ1, SJ2])
  • COMBINATIONS_FROM_OFFERS only creates groups matching actual product scopes — no full-journey group is created unless a single product covers it. This is useful when your client handles journey composition itself. Combined with onlyIncludeMostCompleteInCategorySpec (BETA), partial recommendations get combineWithToCoverEntireJourney links that help clients find complementary offers

Price Comparison Algorithms

AlgorithmComparesUse case
TOTAL_PRICEWhat the user actually pays (after discounts)Recommended for most use cases
BEFORE_SDROriginal price before discountsEmployee tickets (personalbillett)

For both algorithms, the price comparison is done per traveller.

BEFORE_SDR is useful for employee tickets where all offers cost the same after discount. It selects the cheapest original product for a lower tax basis.

Example: A customer with an employee ticket searches Oslo → Bergen.

OfferOriginal priceAfter discount
Offer Akr 200kr 0
Offer Bkr 100kr 0
  • TOTAL_PRICE: Both are kr 0 — tie, so selection is arbitrary
  • BEFORE_SDR: Selects Offer B (kr 100 < kr 200) — lower tax basis

Filtering Subset Recommendations (BETA)

BETA
This feature may change or be removed in future versions.

When onlyIncludeMostCompleteInCategorySpec: true, the API filters out recommendations that are strict subsets of other recommendations within the same category specification.

For a multi-leg journey with recommendations for [A], [B], and [A, B] (all with identical typeOfRecommendation, durationType, fareClass, and facilitySet), only the recommendation for [A, B] is returned.

Key behaviours:

  • Only applies to multi-leg journeys; single-leg journeys are unaffected
  • Subset filtering only considers recommendations with offersToBuy
  • Recommendations without offers are preserved when onlyIncludeRecommendationsWithOffersToBuy: false
  • Enables combineWithToCoverEntireJourney on incomplete recommendations to help clients find complementary offers (see Partial journey coverage)

CategorySpec

(back to Table of contents)

The categorySpec defines which types of recommendations to generate.

FieldRequiredValues
typesOfRecommendationYesFLEXIBLE, SEMI_FLEXIBLE, NON_FLEXIBLE, CHEAPEST
durationTypesNoSINGLE_TRIP, RETURN_TRIP, CARNET, DAY_PASS, WEEKLY_PASS, WEEKEND_PASS, MONTHLY_PASS, SEASON_TICKET, PROFILE_MEMBERSHIP, OPEN_ENDED, OTHER
fareClassesNoFIRST_CLASS, SECOND_CLASS, THIRD_CLASS, ECONOMY_CLASS, BUSINESS_CLASS, TURISTA, PREFERENTE, PREMIUM_CLASS, ANY, STANDARD_CLASS, UNKNOWN
facilitySetNoSLEEPER, SEATING, COUCHETTE, RECLINING_SEAT, ANY_FACILITY_SET

When a field is omitted from categorySpec, it acts as an implicit wildcard — recommendations will include all offers regardless of that attribute. You can also use explicit wildcard values (ANY, ANY_FACILITY_SET) to include a wildcard alongside specific values.

For example, requesting fareClasses: ["STANDARD_CLASS", "ANY"] generates separate recommendations for STANDARD_CLASS offers and for the cheapest across all fare classes. typesOfRecommendation is the only required field and must contain at least one value.

These two requests are functionally equivalent — omitting a field has the same effect as specifying its wildcard value:

Flexibility Levels

LevelRefundableExchangeableDescription
CHEAPESTLowest price regardless of flexibility
NON_FLEXIBLENoNoCannot be refunded or exchanged
SEMI_FLEXIBLEEitherEitherEither refundable or exchangeable, but not both
FLEXIBLEYesYesBoth refundable and exchangeable
Mixing flexibility levels
When mixinOffersWithHigherFlexibility: true, a lower-flexibility category may include offers with higher flexibility if they are cheaper or the requested flexibility is unavailable. At least one offer must match the requested level. See RuleSpec for details and Examples for an illustration.

Traveller Assignment

(back to Table of contents)

The possibleTravellerIds field in offersToBuy tells you who to assign to each purchase. It is a list of lists — each inner list represents one purchase, and the number of inner lists always equals numberToBuy.

Individual tickets — each inner list contains one traveller:

Buy offer-a twice: once for traveller-1, once for traveller-2.

Group tickets — the inner list contains all travellers sharing one ticket (e.g. a sleeper compartment):

Buy offer-b once for all four travellers together. The underlying product may have per-user-type constraints (e.g. minimum 1 adult, up to 3 children) — the recommendation system has already solved the assignment for you.

Mixed — the system may combine individual and group tickets when that is cheapest:

Summary:

possibleTravellerIdsnumberToBuyMeaning
[["A"], ["B"]]2Two individual purchases
[["A", "B"]]1One group purchase
[["A"], ["B", "C", "D"]]2One individual + one group

You do not need to inspect the offer's travellerMapping to use the recommendation — possibleTravellerIds is the ready-made grouping.

Examples

(back to Table of contents)

How categorySpec dimensions combine

Each categorySpec dimension multiplies the number of recommendation slots. The API generates one recommendation per unique combination and filters out empty slots (when onlyIncludeRecommendationsWithOffersToBuy: true). Note that the same offer can appear in multiple recommendations — for example, the cheapest NON_FLEXIBLE offer may also be the overall CHEAPEST.

Example offers:

IdPriceFlexibilityDurationTypeFacilitySet
NonFlexibleSeating10NON_FLEXIBLESINGLE_TRIPSEATING
NonFlexibleCouchette20NON_FLEXIBLESINGLE_TRIPCOUCHETTE
SemiFlexibleSeating30SEMI_FLEXIBLESINGLE_TRIPSEATING
SemiFlexibleSleeper40SEMI_FLEXIBLESINGLE_TRIPSLEEPER
FlexibleCouchette50FLEXIBLESINGLE_TRIPCOUCHETTE
FlexibleSleeper60FLEXIBLESINGLE_TRIPSLEEPER
WeekPass70NON_FLEXIBLEWEEKLY_PASS
MonthPass80NON_FLEXIBLEMONTHLY_PASS

One dimension — flexibility only:

typesOfRecommendation: [CHEAPEST, NON_FLEXIBLE, SEMI_FLEXIBLE, FLEXIBLE]
typeOfRecommendationOffer recommended
CHEAPESTNonFlexibleSeating
NON_FLEXIBLENonFlexibleSeating
SEMI_FLEXIBLESemiFlexibleSeating
FLEXIBLEFlexibleCouchette

4 slots requested, 4 filled. Each picks the cheapest offer matching that flexibility level.

Two dimensions — flexibility × facilitySet:

typesOfRecommendation: [CHEAPEST, NON_FLEXIBLE, SEMI_FLEXIBLE, FLEXIBLE]
facilitySet: [ANY_FACILITY_SET, SEATING, COUCHETTE, SLEEPER]

This creates 4 × 4 = 16 possible slots. Only 13 have matching offers:

typeOfRecommendationfacilitySetOffer recommended
CHEAPESTANY_FACILITY_SETNonFlexibleSeating
CHEAPESTSEATINGNonFlexibleSeating
CHEAPESTCOUCHETTENonFlexibleCouchette
CHEAPESTSLEEPERSemiFlexibleSleeper
NON_FLEXIBLEANY_FACILITY_SETNonFlexibleSeating
NON_FLEXIBLESEATINGNonFlexibleSeating
NON_FLEXIBLECOUCHETTENonFlexibleCouchette
SEMI_FLEXIBLEANY_FACILITY_SETSemiFlexibleSeating
SEMI_FLEXIBLESEATINGSemiFlexibleSeating
SEMI_FLEXIBLESLEEPERSemiFlexibleSleeper
FLEXIBLEANY_FACILITY_SETFlexibleCouchette
FLEXIBLECOUCHETTEFlexibleCouchette
FLEXIBLESLEEPERFlexibleSleeper

Three combinations have no matching offers and are omitted: NON_FLEXIBLE + SLEEPER, SEMI_FLEXIBLE + COUCHETTE, and FLEXIBLE + SEATING. With onlyIncludeRecommendationsWithOffersToBuy: false, these would appear as empty recommendations.

Adding a third dimension (e.g., durationTypes: [SINGLE_TRIP, WEEKLY_PASS, MONTHLY_PASS]) would multiply the slots further: 4 × 4 × 3 = 48 possible slots, with only the non-empty ones returned.

Mixing flexibility levels (mixinOffersWithHigherFlexibility)

Scenario: User searches Oslo → Bodø with typesOfRecommendation: ["NON_FLEXIBLE"], but a FLEXIBLE offer is cheaper than the NON_FLEXIBLE alternative on one leg.

OfferFlexibilityPriceLeg
Offer ANON_FLEXIBLEkr 200Oslo → Trondheim
Offer BFLEXIBLEkr 150Oslo → Trondheim
Offer CNON_FLEXIBLEkr 300Trondheim → Bodø

Result: NON_FLEXIBLE uses Offer A (kr 200) + Offer C (kr 300) = kr 500. Only NON_FLEXIBLE offers are considered.

Result: NON_FLEXIBLE uses the cheaper FLEXIBLE Offer B (kr 150) for leg 1 + NON_FLEXIBLE Offer C (kr 300) for leg 2 = kr 450. The user saves kr 50 and gets higher flexibility on one leg.

Key behaviours:

  • Flexibility hierarchy (low → high): NON_FLEXIBLESEMI_FLEXIBLEFLEXIBLE
  • Higher-flexibility offers can replace lower on individual legs, but not vice versa
  • At least one offer in offersToBuy must match the requested flexibility level — if no NON_FLEXIBLE offers exist for any leg, no NON_FLEXIBLE recommendation is created. This prevents recommendations labelled NON_FLEXIBLE that contain only flexible offers
  • The CHEAPEST category is not affected by this flag

Mixing facility types (mixInOffersWithOtherFacilitySets)

Scenario: User searches Oslo → Bodø with facilitySet: ["SLEEPER"], but sleeper is only available for part of the journey.

OfferFacilityLeg
Offer ASLEEPEROslo → Trondheim
Offer BSEATINGTrondheim → Bodø

Result: The SLEEPER category is empty because sleeper alone can't cover the full journey.

Result: SLEEPER recommendation combines sleeper (where available) with seating (where necessary).

Key behaviours:

  • The algorithm tries the requested facility first, only mixing in others when necessary
  • Recommendations consisting entirely of non-requested facilities are never created
  • All other dimensions are preserved. For instance, a SEMI_FLEXIBLE recommendation will only look for offers also matching this dimension

Duration types

Scenario: Products are available as single trips, day passes, monthly passes and more. You want to present the cheapest option per duration type.

categorySpec:

typesOfRecommendation: [CHEAPEST]
durationTypes: [SINGLE_TRIP, DAY_PASS, MONTHLY_PASS]
typeOfRecommendationdurationTypeOffer recommended
CHEAPESTSINGLE_TRIPSingle ticket
CHEAPESTDAY_PASSDay pass
CHEAPESTMONTHLY_PASSMonthly pass

The API returns one recommendation per duration type, each pointing to the cheapest offer of that duration. This lets clients present options like "Single trip: kr 39", "Day pass: kr 109", "Monthly pass: kr 790" side by side.

You can combine durationTypes with other categorySpec dimensions. For example, adding typesOfRecommendation: [CHEAPEST, NON_FLEXIBLE, FLEXIBLE] generates up to 3 × 3 = 9 recommendations (one per flexibility level per duration type). Empty combinations (where no matching offers exist) are filtered out when onlyIncludeRecommendationsWithOffersToBuy: true.

Rail replacement filtering

Scenario: Service journey A is partially replaced by bus (BFT A), followed by another rail leg B.

LegTypeNote
ARailRemaining rail portion of original SJ
BFT ARail replacementBus replacing part of A
BRailSeparate service journey

With removeSplitRailReplacementOffers: false:

Recommendations for: [A], [BFT A], [B], [A, BFT A], [BFT A, B], [A, BFT A, B]

With removeSplitRailReplacementOffers: true:

Recommendations for: [A], [B], [A, BFT A], [BFT A, B], [A, BFT A, B]

The standalone [BFT A] recommendation is removed because it contains the rail-replacement leg without its corresponding rail leg (A).

Known issue
[BFT A, B] should also be removed — it contains BFT A without the corresponding A — but is currently kept due to a bug in the filtering logic.

Key behaviour: When a service journey is partially replaced by bus (i.e., both rail and rail-replacement portions exist), any recommendation group that contains a rail-replacement leg without its corresponding rail leg is removed. If the entire service journey is replaced by bus, no filtering occurs.

Partial journey coverage

Requires
onlyIncludeMostCompleteInCategorySpec: true in ruleSpec (BETA feature)

When a recommendation doesn't cover the entire journey (coversEntireJourney: false), the combineWithToCoverEntireJourney field lists other recommendations that can be combined to cover the full journey.

Scenario: A two-leg journey with two operators. No single ticket covers both legs.

How to use:

  1. Check if coversEntireJourney is false
  2. If so, look at combineWithToCoverEntireJourney for complementary recommendations
  3. Select one item from the list — each item covers a specific geographical gap
  4. Purchase offers from all recommendations in the selected item's recommendationsToCoverEntireJourney, together with the parent recommendation, to cover the full journey

Troubleshooting

(back to Table of contents)

Unexpected offers in recommendations

  1. Check if mixinOffersWithHigherFlexibility is enabled. This may place FLEXIBLE offers in NON_FLEXIBLE categories
  2. Check if mixInOffersWithOtherFacilitySets is enabled. This may mix SEATING into SLEEPER categories
  3. Verify priceComparisonAlgorithmBEFORE_SDR may select different offers than TOTAL_PRICE

Missing flexibility levels

Not all products support all flexibility options. If no FLEXIBLE offers exist for a route, no FLEXIBLE recommendation is returned.

NON_FLEXIBLE recommendation contains a mix of flexible and non-flexible offers

This happens when mixinOffersWithHigherFlexibility: true and a flexible offer is cheaper on one or more legs. This is expected behaviour; the system found a better deal by using a higher-flexibility offer where it's cheaper, while keeping at least one non-flexible offer in the recommendation. See example.

Large response size

Enable onlyIncludeRecommendedOffers: true to exclude offers not part of any recommendation.