---
title: "Ancillary offers for travel brands | Snowplow Signals"
description: "Snowplow Signals tells your booking flow who is traveling, for how long and when they land, so it shows the seat, bag and hotel offers that fit this trip."
source: https://signals.snowplow.io/solutions/travel/extras
---

[Travel](https://signals.snowplow.io/solutions/travel) / Ancillary offers 

# Seat, bag and hotel offers that fit this trip. 

Signals tells your booking flow who is traveling, for how many nights and when the flight lands, so the seat, bag and hotel offers can fit the trip in front of it.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher ancillary revenue

## Sell more per booking by offering the extras that fit this trip. 

Ancillary revenue moves when the offers in the booking flow match the trip being booked rather than the default order every traveler gets. Signals gives your booking flow how many people are traveling, how many nights, when the flight lands, which fares this traveler compared and which hotels they looked at on an earlier trip, so it can choose which extras to show and in what order.

Four ways travel brands use it

01 A family of four for three nights Four passengers, three nights. Seats together and a hotel, not the lounge pass. 02 A solo traveler landing late One passenger, lands after eleven at night. The transfer and late check-in, not the bag. 03 A traveler who priced the bag fare and chose basic Picked the fare with a bag, went back, picked basic. Offer the bag at the gap they saw. 04 A traveler back to the same city this year Second trip to Lisbon since spring, looked at two hotels last time. Show those first. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Lead with seats together and a hotel for a family trip

A traveler searched for four passengers to Orlando, out on Friday and back on Monday, and has picked a fare. The extras page opens with the lounge pass, then priority boarding, then seats, in the order set for every booking. Those offers suit a traveler on their own, and the passenger count reaches the extras page once the booking is built, after the first offer has been shown. Four people on a three-night trip want to sit together and need somewhere to sleep, and both offers are ready to show.

What Signals knows

`passengers_session` 

Passengers on the last search this visit

`nights_session` 

Nights between the outbound and the return on the last search this visit

What your booking flow does

Puts seats together and the hotel offer for the destination at the top of the extras page when the last search this visit has three or more passengers and at least two nights, and leaves the lounge pass further down.

Define it · PythonUse it · TypeScript

extras.tsx · 11 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

flight_search = Event(name="flight_search", vendor="com.example", version="1-0-0")

passengers_session = Attribute(
    name="passengers_session",
    type="int32",
    events=[flight_search],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="passengers"),
)

nights_session = Attribute(
    name="nights_session",
    type="int32",
    events=[flight_search],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="nights"),
)

# Keyed on the visit, so the shape of this trip is ready before the booking is built.
trip_extras_family = StreamAttributeGroup(
    name="trip_extras_family",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[passengers_session, nights_session],
)
```

```
// In the code that renders the extras page
const trip = await signals.getServiceAttributes({
  name: "trip_extras_family",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { passengers_session: 4, nights_session: 3 }

if (trip.passengers_session >= 3 && trip.nights_session >= 2) {
  extras.order(["seats_together", "hotel", "bags", "lounge"]);
}
```

### Offer the transfer and late check-in to a traveler who lands late

One traveler has picked a fare that lands at twenty past eleven at night. The extras page opens with the checked bag, the way it does for every booking. A traveler arriving alone close to midnight has two problems the bag offer does not touch. They need to get from the airport, and they need the hotel to let them in. The arrival time reaches the extras page after the offers have been laid out, so the transfer and the late check-in sit below an offer this traveler is unlikely to want.

What Signals knows

`passengers_session` 

Passengers on the last search this visit

`arrival_hour_session` 

The local hour the last fare picked this visit lands

What your booking flow does

Moves the airport transfer and the late check-in to the top of the extras page when one passenger picks a fare landing after ten at night, and leaves the checked bag in its usual place.

Define it · PythonUse it · TypeScript

extras.tsx · 11 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

flight_search = Event(name="flight_search", vendor="com.example", version="1-0-0")
fare_selected = Event(name="fare_selected", vendor="com.example", version="1-0-0")

passengers_session = Attribute(
    name="passengers_session",
    type="int32",
    events=[flight_search],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="passengers"),
)

arrival_hour_session = Attribute(
    name="arrival_hour_session",
    type="int32",
    events=[fare_selected],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="fare_selected", major_version=1, path="arrival_hour"),
)

trip_extras_late = StreamAttributeGroup(
    name="trip_extras_late",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[passengers_session, arrival_hour_session],
)
```

```
// In the code that renders the extras page
const trip = await signals.getServiceAttributes({
  name: "trip_extras_late",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { passengers_session: 1, arrival_hour_session: 23 }

if (trip.passengers_session === 1 && trip.arrival_hour_session >= 22) {
  extras.order(["transfer", "late_checkin", "bags", "seats_together"]);
}
```

### Offer the bag at the price difference the traveler has seen

A traveler picked the fare that includes a checked bag, saw the total, went back and picked the basic fare instead. On the extras page the bag is offered at the standard add-on price, which is higher than the gap between the two fares they were comparing a minute ago. The traveler saw both numbers, so an offer above the gap reads as a markup and gets declined. Your pricing engine owns the price, and it can set the bag at the difference the traveler weighed, if it knows that comparison happened in this visit.

What Signals knows

`fare_families_session` 

Every fare family picked this visit, in any order

`fare_family_session` 

The fare family of the last fare picked this visit

What your booking flow does

Asks your pricing engine for the bag at the gap between the two fares when a visit has picked a bag-included fare and then settled on basic, and shows that price on the extras page in place of the standard add-on price.

Define it · PythonUse it · TypeScript

extras.tsx · 14 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

fare_selected = Event(name="fare_selected", vendor="com.example", version="1-0-0")
fare_family = EventProperty(vendor="com.example", name="fare_selected", major_version=1, path="fare_family")

fare_families_session = Attribute(
    name="fare_families_session",
    type="string_list",
    events=[fare_selected],
    aggregation="unique_list",
    property=fare_family,
)

fare_family_session = Attribute(
    name="fare_family_session",
    type="string",
    events=[fare_selected],
    aggregation="last",
    property=fare_family,
)

trip_extras_bag = StreamAttributeGroup(
    name="trip_extras_bag",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[fare_families_session, fare_family_session],
)
```

```
// In the code that renders the extras page
const trip = await signals.getServiceAttributes({
  name: "trip_extras_bag",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { fare_families_session: ["bag_included", "basic"], fare_family_session: "basic" }

const comparedBag = trip.fare_families_session.includes("bag_included");

if (comparedBag && trip.fare_family_session === "basic") {
  const price = await pricing.fareGap({ from: "basic", to: "bag_included", route });
  extras.offer({ item: "checked_bag", price });
}
```

### Show the hotels they looked at on the last trip to this city

A traveler is booking Lisbon again, their second trip there this year, on the same browser. Last time they opened two hotels near the river and booked neither through you. The hotel offer on the extras page shows the properties with the best margin, the same list every traveler to Lisbon gets, and the two hotels this traveler weighed in the spring sit somewhere in the search behind it. A hotel the traveler has looked at is a shorter decision than a new one, and the booking flow can put it first if it knows which ones they were.

What Signals knows

`hotel_cities_1y` 

Cities where this traveler viewed a hotel in the last year

`hotels_viewed_1y` 

The hotels they viewed in the last year

What your booking flow does

Puts the hotels this traveler viewed before, filtered to the destination of this booking, at the top of the hotel offer when the destination is a city they looked at hotels in this year, and shows the standard list to everyone else.

Define it · PythonUse it · TypeScript

extras.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_userid,
)

hotel_viewed = Event(name="hotel_viewed", vendor="com.example", version="1-0-0")

hotel_cities_1y = Attribute(
    name="hotel_cities_1y",
    type="string_list",
    events=[hotel_viewed],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="hotel_viewed", major_version=1, path="city"),
    ttl=timedelta(days=365),
)

hotels_viewed_1y = Attribute(
    name="hotels_viewed_1y",
    type="string_list",
    events=[hotel_viewed],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="hotel_viewed", major_version=1, path="hotel_id"),
    ttl=timedelta(days=365),
)

# Keyed on the browser, so the hotels from the spring trip follow the traveler into this one.
trip_extras_return = StreamAttributeGroup(
    name="trip_extras_return",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[hotel_cities_1y, hotels_viewed_1y],
)
```

```
// In the code that renders the extras page
const trip = await signals.getServiceAttributes({
  name: "trip_extras_return",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { hotel_cities_1y: ["LIS", "BCN"], hotels_viewed_1y: ["h-2281", "h-2304", "h-5190"] }

if (trip.hotel_cities_1y.includes(destination)) {
  const seen = hotels.inCity(destination).filter((h) => trip.hotels_viewed_1y.includes(h.id));
  extras.hotelOffer({ first: seen, then: hotels.standard(destination) });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
