---
title: "Fare alerts for travel brands | Snowplow Signals"
description: "Snowplow Signals tells your CRM which route and dates each traveler keeps pricing and the fare they last saw, so it sends the alert when that fare moves."
source: https://signals.snowplow.io/solutions/travel/fare-alerts
---

[Travel](https://signals.snowplow.io/solutions/travel) / Fare alerts 

# A fare alert for the route they keep pricing. 

Signals tells your CRM which route and dates each traveler keeps pricing, how often, and what fare they last saw, so the alert can go out the moment your fares system moves that price.

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

More bookings from fare alerts

## Book more of the travelers waiting on a fare by alerting them when that fare moves. 

Fare alerts move bookings when they reach the traveler who is waiting on a fare, about the route they are waiting on. Signals gives your CRM the route and dates each traveler keeps pricing, how many times they have priced it this week, the fare they last saw and whether they have booked since. Your CRM and your fares system decide which price move is worth an alert and who gets it.

Four ways travel brands use it

01 The traveler who priced one route three times Chicago to Lisbon, priced three times this week, no form filled in. Set the watch for them. 02 The traveler watching two routes Lisbon priced four times, Porto twice, same week. Alert on either, lead with Lisbon. 03 The member who priced a route, then booked it Priced Denver five times, booked it on Tuesday. Stop the alert. 04 The traveler whose dates are now a week away Priced it all month for dates six days out, no booking. Send the last-chance note. 

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

### Set the fare watch without asking them to fill in a form

A traveler has priced Chicago to Lisbon for the same November dates three times this week and left without booking each time. The fare alert on your site is a form, and the alert job hears about a traveler only once they fill it in. The traveler who priced a route three times and left has told you the same thing without the form. Three prices in a week for one route is the watch, set by what they did. If your CRM holds an email for this browser, the alert can go out the moment your fares system prices that route below what they last saw.

What Signals knows

`watched_route_7d` 

The route they priced most in the last seven days

`routes_priced_7d` 

How many times they priced each route in the last seven days

`watched_depart_7d` 

The departure date on their most recent search

`last_fare_seen` 

The fare they last saw, in your currency

What your CRM does

Opens a fare watch for this browser once a route has been priced three times in a week, with the fare they last saw as the line to beat, and sends the alert when your fares system next prices that route and date below it. The email is one your CRM holds under your consent rules, and a browser without one gets the watch only, ready for when they leave an address.

Define it · PythonUse it · TypeScript

fare-alerts.ts · 22 lines Copy Copied 

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

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

watched_route_7d = Attribute(
    name="watched_route_7d",
    type="string",
    events=[flight_search],
    aggregation="most_frequent",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
    period=timedelta(days=7),
)

routes_priced_7d = Attribute(
    name="routes_priced_7d",
    type="dict",
    events=[flight_search],
    aggregation="category_count",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
    period=timedelta(days=7),
)

watched_depart_7d = Attribute(
    name="watched_depart_7d",
    type="string",
    events=[flight_search],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="depart_date"),
    period=timedelta(days=7),
)

last_fare_seen = Attribute(
    name="last_fare_seen",
    type="double",
    events=[fare_viewed],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="fare_viewed", major_version=1, path="price"),
    period=timedelta(days=7),
)

# Keyed on the browser, so a watcher who has not logged in or filled in a form still has a watch.
trip_fares_watching = StreamAttributeGroup(
    name="trip_fares_watching",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[watched_route_7d, routes_priced_7d, watched_depart_7d, last_fare_seen],
)
```

```
// In the job that opens fare watches in your CRM
const traveler = await signals.getServiceAttributes({
  name: "trip_fares_watching",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { watched_route_7d: "ORD-LIS", routes_priced_7d: { "ORD-LIS": 3 }, watched_depart_7d: "2026-11-14", last_fare_seen: 412.0 }

// Your CRM holds the email, if they left one, under your consent rules. Signals holds none.
const contact = crm.contactFor(domainUserId);

const pricedThisRoute = traveler.routes_priced_7d[traveler.watched_route_7d] ?? 0;

if (pricedThisRoute >= 3) {
  crm.watchFare({
    browser: domainUserId,
    to: contact,
    route: traveler.watched_route_7d,
    depart: traveler.watched_depart_7d,
    alertBelow: traveler.last_fare_seen,
  });
}
```

### Alert on either route, and lead with the one they price most

A traveler is pricing Boston to Lisbon and Boston to Porto for the same week in March, Lisbon four times and Porto twice. A fare watch takes one route, so it covers whichever they saved, and a drop on the other route has no watch to fire. A traveler pricing two cities for one week will take whichever comes down first. The watch should cover both, and when both move in the same night, the Lisbon fare belongs at the top of the email.

What Signals knows

`routes_priced_7d` 

How many times they priced each route in the last seven days

`watched_route_7d` 

The route they priced most in the last seven days

What your CRM does

Opens a watch on every route this traveler has priced at least twice this week, and when more than one of them moves, leads the alert with the route they priced most. The fare each watch opens at comes from your fares system at the moment the watch is set.

Define it · PythonUse it · TypeScript

fare-alerts.ts · 19 lines Copy Copied 

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

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

routes_priced_7d = Attribute(
    name="routes_priced_7d",
    type="dict",
    events=[flight_search],
    aggregation="category_count",
    property=route,
    period=timedelta(days=7),
)

watched_route_7d = Attribute(
    name="watched_route_7d",
    type="string",
    events=[flight_search],
    aggregation="most_frequent",
    property=route,
    period=timedelta(days=7),
)

trip_fares_routes = StreamAttributeGroup(
    name="trip_fares_routes",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[routes_priced_7d, watched_route_7d],
)
```

```
// In the job that opens fare watches in your CRM
const traveler = await signals.getServiceAttributes({
  name: "trip_fares_routes",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { routes_priced_7d: { "BOS-LIS": 4, "BOS-OPO": 2 }, watched_route_7d: "BOS-LIS" }

const watched = Object.entries(traveler.routes_priced_7d)
  .filter(([, priced]) => priced >= 2)
  .map(([route]) => route);

if (watched.length > 1) {
  crm.watchFares({
    browser: domainUserId,
    routes: watched.map((route) => ({ route, alertBelow: fares.current(route) })),
    leadWith: traveler.watched_route_7d,
  });
}
```

### Close the watch once the traveler has booked

A loyalty member priced Boston to Denver five times over two weeks, and on Tuesday they booked it. On Thursday the fare drops, and the alert goes out to tell them the trip they have paid for is now cheaper. The pricing opened the watch, and the booking sits in another system, so the sync that would have closed the watch runs after the alert has gone. A booking on the watched route is the moment to close the watch, and that moment has to reach the alert job before the next price drop does.

What Signals knows

`watched_route_7d` 

The route they priced most in the last seven days

`bookings_7d` 

Bookings completed in the last seven days

`last_booked_route` 

The route of their most recent booking

What your CRM does

Closes the watch and skips the price-drop alert once a member has completed a booking on the route they were pricing, and leaves the watch open for the member whose booking was a different trip.

Define it · PythonUse it · TypeScript

fare-alerts.ts · 20 lines Copy Copied 

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

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

watched_route_7d = Attribute(
    name="watched_route_7d",
    type="string",
    events=[flight_search],
    aggregation="most_frequent",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
    period=timedelta(days=7),
)

bookings_7d = Attribute(
    name="bookings_7d",
    type="int32",
    events=[booking_completed],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

last_booked_route = Attribute(
    name="last_booked_route",
    type="string",
    events=[booking_completed],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="booking_completed", major_version=1, path="route"),
    period=timedelta(days=7),
)

# Members are logged in, so the searches from the site and the booking from your server share the user id.
trip_fares_booked = StreamAttributeGroup(
    name="trip_fares_booked",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[watched_route_7d, bookings_7d, last_booked_route],
)
```

```
// In the job that sends fare alerts from your CRM, before each send
const member = await signals.getServiceAttributes({
  name: "trip_fares_booked",
  attribute_key: "user_id",
  identifier: userId,
});
// { watched_route_7d: "BOS-DEN", bookings_7d: 1, last_booked_route: "BOS-DEN" }

const booked = member.bookings_7d > 0 && member.last_booked_route === member.watched_route_7d;

if (booked) {
  crm.closeWatch({ userId, route: member.watched_route_7d });
} else {
  crm.send({
    userId,
    campaign: "fare_dropped",
    route: member.watched_route_7d,
    fare: fares.current(member.watched_route_7d),
  });
}
```

### Say so when the dates they keep pricing are inside a week

A traveler has priced Seattle to Austin for the same dates on and off for a month, and departure is now six days away. The alert is written for a fare that fell, and this one has not, so the alert job has nothing to send. The traveler is still deciding with six days left, and the thing that would help them decide is the date itself, next to the fare as it stands. The days left come from your own arithmetic on the departure date, since the departure date lives in the searches and the calendar lives with you.

What Signals knows

`watched_route_30d` 

The route they priced most in the last 30 days

`searches_30d` 

Flight searches run in the last 30 days

`watched_depart_30d` 

The departure date on their most recent search

What your CRM does

Sends a note with the departure date and the fare as it stands once the date a traveler has been pricing for weeks is inside a week, and leaves the price-drop alert to do its job for the traveler whose dates are still months out.

Define it · PythonUse it · TypeScript

fare-alerts.ts · 21 lines Copy Copied 

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

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

watched_route_30d = Attribute(
    name="watched_route_30d",
    type="string",
    events=[flight_search],
    aggregation="most_frequent",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
    period=timedelta(days=30),
)

searches_30d = Attribute(
    name="searches_30d",
    type="int32",
    events=[flight_search],
    aggregation="counter",
    period=timedelta(days=30),
    default_value=0,
)

watched_depart_30d = Attribute(
    name="watched_depart_30d",
    type="string",
    events=[flight_search],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="depart_date"),
    period=timedelta(days=30),
)

trip_fares_departure = StreamAttributeGroup(
    name="trip_fares_departure",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[watched_route_30d, searches_30d, watched_depart_30d],
)
```

```
// In the job that sends fare alerts from your CRM
const traveler = await signals.getServiceAttributes({
  name: "trip_fares_departure",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { watched_route_30d: "SEA-AUS", searches_30d: 9, watched_depart_30d: "2026-10-13" }

const dayMs = 24 * 60 * 60 * 1000;
const daysUntilDepart = Math.ceil((Date.parse(traveler.watched_depart_30d) - Date.now()) / dayMs);
const contact = crm.contactFor(domainUserId);

if (contact?.email && traveler.searches_30d >= 3 && daysUntilDepart > 0 && daysUntilDepart <= 7) {
  crm.send({
    to: contact,
    campaign: "fare_last_chance",
    route: traveler.watched_route_30d,
    depart: traveler.watched_depart_30d,
    fare: fares.current(traveler.watched_route_30d, traveler.watched_depart_30d),
  });
}
```

## 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)
