Notifications & Events

How the Stay22 notification lifecycle works, how to customize the copy, and how to observe every decision the SDK makes.

The notification lifecycle

Once you set a travel context, the notification moves through a simple lifecycle — and the SDK tells you about every step through events:

  1. Scheduled. The SDK accepts the trip and queues one notification for it. A newer travel context replaces the pending one.
  2. Delivered. After the user leaves your app and the Stay22-managed delay elapses, the notification is shown.
  3. Opened. The user taps it and lands in a curated booking experience for their destination and dates, attributed to your aid.
  4. Skipped or cancelled. If conditions aren't right — the user is still in the app, the destination is their home city, a cooldown is active, permission is missing — the SDK holds back and emits the reason instead.

The delivery delay and frequency caps are configured by Stay22 for your account, not in code — that keeps timing tuned without app releases. Contact your Stay22 account manager or support@stay22.com to adjust them.

Customize the notification

The default copy works out of the box.

import com.stay22.sdk.NotificationConfig
import com.stay22.sdk.NotificationInterruptionLevel

Stay22.notificationConfig = NotificationConfig(
    title = "Hotels in {destination}",
    subtitle = "{checkin} - {checkout}",
    message = "Tap to find the best deals nearby",
    categoryId = "stay22_destinations",
    threadId = "stay22",
    interruptionLevel = NotificationInterruptionLevel.active
)

NotificationConfig also takes badge, launchImageName, targetContentIdentifier, relevanceScore, image attachments (local files only) and custom actions buttons. On Android:

  • launchImageName must be a drawable resource name, and renders as the notification's large icon.
  • targetContentIdentifier becomes the notification's launcher shortcut ID.
  • The SDK renders one attachment, as the expanded image, and the URI must be a file:// one — a content:// URI, which is what a file picker usually hands you, is ignored.
  • relevanceScore is an iOS field. Android accepts it and nothing acts on it.
Stay22.notificationConfig = NotificationConfig(
    title: "Hotels in {destination}",
    subtitle: "{checkin}-{checkout}",
    message: "Find stays near {destination}.",
    categoryId: "stay22_destinations",
    threadId: "stay22",
    badge: 1,
    interruptionLevel: .active,
    relevanceScore: 0.8
)
await Stay22.setNotificationConfig(NotificationConfig(
  title: 'Hotels in {destination}',
  subtitle: '{checkin} - {checkout}',
  message: 'Tap to find the best deals nearby',
  categoryId: 'stay22_destinations',
  threadId: 'stay22',
  interruptionLevel: NotificationInterruptionLevel.active,
  actions: [NotificationAction(identifier: 'book', title: 'See deals')],
));

Omitting a field restores the SDK default rather than keeping whatever a previous call set.

The plugin does not expose NotificationConfig. Title, subtitle, and message stay on the native defaults.

The Android README lists every field's default. The iOS, Flutter and Capacitor READMEs carry the rest of each platform's API surface.

Text placeholders

Title, subtitle, and message support placeholders that the SDK fills in from the travel context:

PlaceholderReplaced with
{destination}The trip's destination name
{checkin}Check-in date, when provided
{checkout}Check-out date, when provided

Attribution

Bookings are attributed to the aid you initialized with. To segment where conversions come from — much like campaign on Allez links — set an optional campaign ID:

Stay22.campaignId = "spring_push"
Stay22.campaignId = "spring_push"
await Stay22.setCampaignId('spring_push');
await Stay22.setCampaignId({ campaignId: 'spring_push' });

Omit campaignId to clear it.

Events

The SDK emits an event for every decision it makes, which is the best window into what it's doing — wire them into your analytics or logging:

import com.stay22.sdk.Stay22Event

Stay22.advanced.setEventHandler { event ->
    when (event) {
        is Stay22Event.NotificationScheduled -> log("scheduled: ${event.destination}")
        is Stay22Event.NotificationClicked -> log("clicked: ${event.destination}")
        is Stay22Event.NotificationSkipped -> log("skipped: ${event.reason}")
        else -> Unit
    }
}
Stay22.advanced.setEventHandler { event in
    switch event {
    case .notificationScheduled(let destination, let delay):
        print("scheduled: \(destination), delay=\(delay)s")
    case .notificationClicked(let destination, let url):
        print("clicked: \(destination), \(url)")
    case .notificationSkipped(let reason):
        print("skipped: \(reason)")
    default:
        break
    }
}
Stay22.events.listen((event) {
  switch (event) {
    case NotificationScheduled(:final destination, :final delay):
      print('scheduled: $destination in ${delay.inMinutes} min');
    case NotificationClicked(:final destination, :final url):
      print('clicked: $destination, $url');
    case NotificationSkipped(:final reason):
      print('skipped: $reason');
    default:
      break;
  }
});

The event hierarchy is a sealed class, so an exhaustive switch tells you at compile time when a new event appears. Subscribing late is safe — events raised while your app was not running are replayed to the first subscriber.

Stay22.addListener('stay22Event', (event) => {
  console.log(event.type, event.description);
});

type is one of the names in the table below. description is plain text. notificationClicked also carries the opened url.

EventFires when
locationUpdatedA travel context was accepted
notificationScheduledA notification was queued (destination, delay)
notificationShownThe notification was delivered
notificationClickedThe user tapped it (destination, booking URL)
notificationSkippedScheduling was skipped (reason)
notificationBlockedDelivery was blocked (reason)
notificationCancelledA pending notification was cancelled (reason)
enabledChangedStay22.isEnabled was toggled
travelContextClearedThe travel context was cleared

Skip and block reasons

When the SDK holds back, the reason tells you why — most are expected behavior, not errors:

ReasonMeaning
sdk_disabledStay22.isEnabled is false
missing_destinationThe travel context has no usable destination
app_foregroundThe user was still in your app at delivery time
home_destinationThe destination looks like the user's home area
partner_config_disabledScheduling is currently disabled for your account
local_cooldownA notification was shown too recently (frequency cap)
duplicate_pending_notificationA notification for this trip is already pending
notification_permission_deniedThe user hasn't granted notification permission

Manual scheduling

By default the SDK schedules automatically when a travel context is set.

Stay22.advanced.scheduleNotification()

Call this if you need to wait until after checkout. It still runs the relevance checks.

Stay22.advanced.scheduleNotification()

Call this if you need to wait until after checkout. It still runs the relevance checks.

await Stay22.advanced.scheduleNotification();

Call this if you need to wait until after checkout. It still runs the relevance checks.

The pipeline runs when you call setTravelContext. Delay that call until after checkout if you need to wait.

On this page