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:
- Scheduled. The SDK accepts the trip and queues one notification for it. A newer travel context replaces the pending one.
- Delivered. After the user leaves your app and the Stay22-managed delay elapses, the notification is shown.
- Opened. The user taps it and lands in a curated booking experience for their destination and dates, attributed to your
aid. - 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:
launchImageNamemust be a drawable resource name, and renders as the notification's large icon.targetContentIdentifierbecomes the notification's launcher shortcut ID.- The SDK renders one attachment, as the expanded image, and the URI must be a
file://one — acontent://URI, which is what a file picker usually hands you, is ignored. relevanceScoreis 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:
| Placeholder | Replaced 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.
| Event | Fires when |
|---|---|
locationUpdated | A travel context was accepted |
notificationScheduled | A notification was queued (destination, delay) |
notificationShown | The notification was delivered |
notificationClicked | The user tapped it (destination, booking URL) |
notificationSkipped | Scheduling was skipped (reason) |
notificationBlocked | Delivery was blocked (reason) |
notificationCancelled | A pending notification was cancelled (reason) |
enabledChanged | Stay22.isEnabled was toggled |
travelContextCleared | The travel context was cleared |
Skip and block reasons
When the SDK holds back, the reason tells you why — most are expected behavior, not errors:
| Reason | Meaning |
|---|---|
sdk_disabled | Stay22.isEnabled is false |
missing_destination | The travel context has no usable destination |
app_foreground | The user was still in your app at delivery time |
home_destination | The destination looks like the user's home area |
partner_config_disabled | Scheduling is currently disabled for your account |
local_cooldown | A notification was shown too recently (frequency cap) |
duplicate_pending_notification | A notification for this trip is already pending |
notification_permission_denied | The 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.