TL;DR: Contract reference for the content-driven push notification payload. Routing is driven bycontentType—Document,WebLink,DeepLink,FlightNotification, or empty string (title/message rendered in a WebView) — withdestination,docType, andfileIdconditionally required. Deep links use thePageName?Param=Valueformat against a fixed set of supported pages.requiresLoginandiconUrlare required: the parser reads both unguarded, so omitting either throwsKeyNotFoundExceptionand drops the notification.
This document defines the payload contract used by ALPA Mobile for content-driven push notifications.
The app supports content payloads that route users to one of these content types:
DocumentWebLinkDeepLinkFlightNotification — flight-change alert; opens the Flight Finder notifications tab with the tapped alert expanded (PushNotificationHandler)The docType field further identifies render behavior for document content, such as pdf or htm.
| Field | Required | Type | Description |
|---|---|---|---|
id | Yes | string | Unique content identifier as string. |
title | Yes | string | Notification title shown to the user. |
message | Yes | string | Notification body text. |
contentType | Yes | string | One of Document, WebLink, DeepLink, FlightNotification, or empty string (displays title/message in WebView). |
destination | Conditional | string | URL or route target. Required for Document, WebLink, and DeepLink. |
docType | Conditional | string | Required for Document; typical values: pdf, htm, webpage. |
fileId | Conditional | string | Document file identifier when source is a managed file. |
source | No | string | Upstream content source identifier (e.g. sitecore). |
correlationKey | No — but required in practice on FlightNotification | string | Groups related notifications — used as the Android notification group (FirebaseService.SetGroup). Defaults to a fresh GUID when omitted. On a flight alert it carries the flight's schedule key, bare (no fs-), and that is what a tap routes by (see Flight alert routing below): with it, the tap opens that flight's detail screen; without it, the tap can only reach the alert list. |
portalId | No | number | Deprecated — Legacy portal identifier (not parsed by the app). |
requiresLogin | Yes | string | "true"/"false" — sets RequiresAuthentication. Read unguarded in PushNotification.PushNotificationFromFBM/FromAPNS; omission throws KeyNotFoundException. |
iconUrl | Yes | string | Read unguarded in both parse paths; omission throws KeyNotFoundException. Value may be empty but the key must be present. |
iconUrl2 | No | string | Optional secondary icon URL (parsed when present). |
iconUrl3 | No | string | Optional tertiary icon URL (parsed when present). |
Every payload must also carry the requiresLogin and iconUrl keys (values may be "false"/empty); the examples below predate that finding and omit them.
{
"id": "0",
"message": "Commemorating the Passage of Historic Pilot Qualification and Training Legislation",
"title": "FastRead",
"contentType": "Document",
"docType": "htm",
"source": "sitecore",
"destination": "~/media/F994CC65B5FB45ECABD1BF61F5C85E61.ashx?modified=20230803T2128Z",
"fileId": "{F994CC65-B5FB-45EC-ABD1-BF61F5C85E61}"
}
{
"id": "1005",
"message": "ALPA MAG Test",
"title": "Alpa Mag Test",
"contentType": "Document",
"destination": "~/media/D65CA7D4314B43C6B75A1C5181B1D475.ashx?modified=1589232930000",
"docType": "pdf",
"fileId": "{D65CA7D4-314B-43C6-B75A-1C5181B1D475}",
"source": "sitecore"
}
{
"id": "1000",
"message": "Live Article Test",
"title": "Test Article",
"contentType": "Document",
"destination": "https://mag.alpa.org/Articles/mag-hotline-april-9",
"docType": "webpage"
}
{
"id": "01",
"message": "Testing with google link",
"title": "Weblink Test",
"contentType": "WebLink",
"destination": "https://google.com"
}
Flight-change alerts for subscribed flights, sent by Flight Change Notifier (FlightNotifications.App/App.cs) to the fs-{ScheduleKey} audience via Azure Notification Hub. correlationKey is the bare schedule key — only the audience carries fs-. Tapping opens that flight's detail screen (see Flight alert routing).
{
"id": "188040",
"message": "DL5328 from JFK to SYR now arrives at 07/29/2026 15:38.",
"title": "Flight Changed",
"contentType": "FlightNotification",
"correlationKey": "9fceb91f-0488-41ac-a2ab-40619d8d155c"
}
When contentType is empty, the app displays the title and message directly in a WebView:
{
"id": "01",
"message": "Testing message goes here",
"title": "Testing title goes here",
"contentType": ""
}
The MAUI app uses a simplified deep link format with the page name followed by optional query parameters.
Format: PageName?QueryParam1=Value1&QueryParam2=Value2
Supported pages (the Blazor Hybrid app resolves the same strings via DeepLinkResolver.cs, which maps each page name to a Blazor route — 27 mapped names plus two pseudo-pages):
| Page name | Blazor route |
|---|---|
HomePage | /home |
UserProfilePage | /profile |
SettingsPage | /profile (settings live in a section of the profile page) |
FavoritesPage | /favorites |
DocumentCenterPage | /documents |
DocumentsListPage | /documents |
DocumentsPage | /documents |
FlightDutyPeriodListPage | /ftdt/dashboard |
JumpseatFlightFinderSavedFlightsPage | /jumpseat/saved |
JumpseatFlightFinderFrequentSearchesPage | /jumpseat/recent |
PushNotificationsListPage | /notifications |
PushNotificationsSettingsPage | /notifications/settings |
MemberResourcesPage | /member-resources |
MyPilotGroupPage | /mec |
PilotGroupCommitteePage | /mec/committees |
PilotGroupEventsPage | /mec/events |
PilotGroupHotelsPage | /mec/hotels |
PilotGroupRepresentativesPage | /mec/reps |
KCMHomePage | /kcm |
KCMAirlinesPage | /kcm/airlines |
KCMAirportsPage | /kcm/airports |
JumpseatInfoPage | /jumpseat/search |
JumpseatFlightFinderSearchPage | /jumpseat/search |
JumpseatAirlinePolicyPage | /jumpseat/airline-policies |
JumpseatEtiquettePage | /jumpseat/etiquette |
JumpseatMissionPage | /jumpseat/mission |
AdvocacyPage | /advocacy |
Pseudo-pages (intercepted, not navigated to):
Launcher?Uri=tel:2027974180 — hands the URI to the OS (Launcher.OpenAsync); used for tel:/mailto: destinationsWebViewPage?SourceString=… — shows the URL; on Blazor Hybrid this resolves to the native
auto-login modal (IAuthenticatedWebView → AuthenticatedWebViewPage, AB#2591 —
not the system browser, so the member stays signed in) and the native chrome hints below
are dropped. Native's AppShell.OnNavigating handler renders the same pseudo-page string with
the full chrome-hint set below. The WebViewPage?... string itself is a stable wire-format
token, unrenamed by AB#2591 even though the native page it now builds is
AuthenticatedWebViewPage.Query keys are carried across with keys lowercased (native binds Scope, Blazor binds scope). An unmapped page name is reported as unresolved rather than guessed.
WebViewPage?... parameters (native AppShell.OnNavigating only):
SourceString — URL to loadBannerTitle — Optional banner titleBannerSubtitle — Optional banner subtitleShowCompact — Set to True for compact viewShowFAB — Set to True to show floating action buttonImageGlyph — Icon glyph (e.g., Gear)ImageGlyphFontFamily — Font family for icon (e.g., FontAwesome)Since AB#2591, BannerSubtitle/ImageGlyph/ImageGlyphFontFamily
are still parsed but no longer rendered anywhere — AuthenticatedWebViewPage's header has one
title slot (falls back BannerTitle → page title), no subtitle or glyph slot.
{
"id": "1000",
"message": "DeepLink notification",
"title": "DeepLink notification",
"contentType": "DeepLink",
"destination": "DocumentCenterPage?Scope=ALPA"
}
contentType drives routing behavior and must match one of the supported values: Document, WebLink, DeepLink, FlightNotification, or empty string.destination must be non-empty and valid for the selected content type (not required when contentType is empty or FlightNotification).Document payloads, include both docType and fileId where available.requiresLogin and iconUrl keys are required on every payload — both are read unguarded (PushNotification.cs), so omitting either throws KeyNotFoundException. iconUrl2, iconUrl3, and correlationKey are optional to the parser; portalId is not parsed. On FlightNotification, however, omitting correlationKey silently downgrades the tap destination — see Flight alert routing.A tapped FlightNotification opens the flight the alert is about, not a list of alerts.
PushNotificationHandler reads the schedule key from correlationKey
(FlightNotificationText.ScheduleKeyFrom(PushNotification); a fs--prefixed key is accepted too) and
routes to /jumpseat/flight-detail/alert/{ScheduleKey}?notif={id}; the screen resolves that key against the
loaded search results, then against Saved Flights, and when the device holds neither — a flight unsaved since the alert
was sent, or saved on another device — asks the service (POST /api/FlightSearchV2/FindLegsByScheduleKey,
IFlightSearchQueries.FindFlightByScheduleKeyAsync). That answer is the one leg the key names, so the card shows
one leg. If the service cannot supply it, the empty state says why: no longer in the schedule, offline, signed out, or
could not load (pull down retries). Back from there returns to the alert list on the Flight Finder tab with the tapped
alert focused.
When the payload carries no correlationKey, the handler looks the alert up in the Flight Finder alert feed
by id and takes the key from its fs-{ScheduleKey} audience. That feed holds only the flights subscribed
now and must answer within 5 seconds, so it is a fallback, not a route: when it misses, the tap lands on
/notifications?tab=flight&focus={id}.
Correction (AB#3059). Until 2026-10-02 the handler required correlationKey to start with
fs-. The sender has sent the bare key since 2025-04-02 (Flight Change Notifier commit 81b136f), so the
payload never matched and every tap depended on the feed lookup — which is why taps opened the alert list
intermittently (after a subscription change, a slow feed, or an alert the feed did not yet list). The App Logs line
[NavigateToFlightNotificationAsync] … keySource:payload|feed|none route:… now says which path a tap took,
and [ScheduleKeyFromFeedAsync] logs why a feed lookup found nothing.
Absent vs. fabricated. PushNotification.CorrelationKey makes up a GUID when the field is
unset (Android groups notifications by it). That GUID is shaped like a real key, so the handler trusts only a key the
payload actually carried (PushNotification.HasCorrelationKey).
Hub template note. The field only reaches the device if the registration template carries it. The
Android and silent-iOS templates always have; the visible-iOS template did not until 2026-08-14
(Constants.FSNotificationHubRegistrationTemplateiOS), so an iOS tap had nothing to route by regardless of
what the sender supplied. Devices pick the added field up on their next hub registration, which happens at app start.