Migrate to the unified events schema
Preliminary pre-release documentation, subject to change without notice. The full list of properties on each event isn't published yet.
Updated on September 16, 2026.
All platforms emit the same event names, properties, and values in the unified schema. This page is the reference for what changes in the payload: the envelope, the categories, the value remap, and the events and properties that were renamed, removed, or added.
Read 2026 events schema - breaking changes ahead first for the timeline.
The code changes your app needs are platform-specific, because the starting point differs per platform. Use the guide for your platform once you've read this page. Those guides include a list of changes not to make without a person, and they apply even when the change would clear a compile error:
The payload reference on this page applies to every platform.
API reference: iOS | Android | Web
Before you start
- One schema, every platform: the event names, properties, and values on this page are the same everywhere. Only the host app changes differ.
- Each SDK version runs one schema. iOS and Android 2.0.x, and Web 2.0.x, emit the unified schema. iOS and Android 1.21.x, and Web 0.37.x, keep the legacy schema. Upgrade and validate at your own pace. From January 2027, new SDK releases ship only the unified schema. Legacy events from older versions still process.
- There are no deprecation shims. Old names aren't aliased to new ones, so anything you miss goes silent rather than failing.
Incomplete mappings
The full list of properties on each event isn't published yet. Treat a mapping that isn't listed here as unknown, not as unchanged. A few values are still being confirmed against the tracking plan. The date in the banner tells you which version you're reading.
The envelope
These rules changed for every event, alongside the fields in the table below:
- Each event carries exactly its own properties. A missing property means the property doesn't apply to that event. It doesn't mean null. Fields you used to read off any Video event may only exist on the events that define them, so check before you read.
- Empty values are omitted. Nothing arrives as
{},"", or0x0.
Timestamps use the format yyyy-MM-dd HH:mm:ss.SSS. That's deliberate, and it isn't ISO 8601. Both timestamps on an event come from the same instant.
| Legacy | Unified | What to watch |
|---|---|---|
api_scheme_version: "2" | api_scheme_version: "2.1" | Accept both while legacy and unified traffic share a pipeline. Queued legacy events still arrive after you upgrade, so the tail is mixed for a while. |
container | tab | Read tab properties. The container object is gone. |
geo, page, wsc_internal | No replacement | These were always empty. |
Categories
| Legacy | Unified | What to watch |
|---|---|---|
Story, Moment, Video, Ad, Widget, Interaction, Search | story, moment, video, ad, widget, interaction, search | Every event_category value is lowercase. A filter on the capitalized value matches nothing. |
sdk_init under Widget | sdk_init under global | A session count filtered on category Widget loses every sdk_init. |
| None | tab | New category. |
| None | global | New category. |
Value remap
Every enum value is lowercase snake_case. Don't recase your old values and assume they match. Several sets were remapped, so the identity changed, not just the casing.
| Property | Legacy values | Unified values |
|---|---|---|
content_type | Story, Moment, Video | story, moment, video |
audio_state | Mute, Unmute | mute, unmute |
gesture_type | Click, Swipe-Up | click, swipe_up |
viewing_mode | Fullscreen, Inline Preview, Inline Interactive | fullscreen, inline_preview, inline_interactive, plus picture_in_picture, casting, out_of_screen. Web emits embedded or fullscreen for the page surface. |
*_navigation_type | Automatic, Manual | automatic, manual |
*_navigation_direction | Forward, Backward, Close | forward, backward, close |
widget_type | Row, Grid | row, grid |
thumbnail_type | Circle, Rectangle, Main | circle, rectangle, main |
thumbnail_format | Animated, Static | animated, static |
seek_direction | Forward, Backward | forward, backward |
seek_type | Seek Bar, Double Tap, Seek Button | seek_bar, double_tap, seek_button |
playback_speed_type | Press Hold | press_hold |
device_orientation | Portrait, Landscape | portrait, landscape |
video_orientation_changed_trigger | Initial Request, Button, Device Rotation | initial_request, button, device_rotation |
stream_status | Live, Upcoming, Ended, and "" for unknown | live, upcoming, ended, unknown. The empty string is gone. |
transition_method | click, swipe, programmatic | tap, swipe, programmatic. A tab tap reports tap, and click is the call to action gesture. |
cc_state | Off, Unavailable, Unknown, or an ISO code | off, not_available, unknown, or an ISO code |
referring.origin_widget_type | Row, Grid | row, grid, matching widget_type on the same event |
tech.device_type | Phone, Tablet, Unknown | phone, tablet, unknown. Web also desktop. |
tech.connection_type | wireless, cellular | wifi, cellular, plus unknown when connectivity can't be queried. Web also wired. |
tech.operating_system, tech.device_brand | Capitalized, such as iOS and Apple | Lowercase, such as ios and apple |
tech.screen_resolution | 1170X2532, with a capital X | 1170x2532 |
sdk_type | Capitalized, such as iOS-RTN | Lowercase, such as ios-rtn. The values are platform-specific: ios, ios-rtn, ios-flutter, ios-rtn-flutter, android, android-rtn, android-flutter. |
Exit triggers
Don't recase these. Stories completed is not stories_completed. Swipe Down is not in the unified vocabulary.
| Legacy | Unified |
|---|---|
Auto Skip | auto_skip |
Skip | skip |
Swipe | swipe |
Swipe Down | No value. Swipe to dismiss reports user_swipe_to_dismiss. Swiping between playlists reports swipe. |
Video Finished | video_finished |
Close Button | close_button |
App Close | app_close |
URL Expiration | url_expiration |
Stories completed | story_completed |
App Background | app_background |
Viewing Mode Transition | viewing_mode_transition |
User Skip Next | user_skip_next |
User Skip Previous | user_skip_previous |
Inline | inline |
User Swipe To Dismiss | user_swipe_to_dismiss |
| None | focus_lost |
Start triggers
Don't recase these. Moments Container is not moments_container.
| Legacy | Unified |
|---|---|
Widget | widget |
Deeplink | deeplink |
Entry Point | entry_point |
Auto Play from Widget | auto_play_from_widget |
Notification | notification |
App Foreground | app_foreground |
Inline | inline |
Moments Container | moments_tabs |
Skip | skip |
Swipe | swipe |
Auto Skip | auto_skip |
Viewing Mode Transition | viewing_mode_transition |
| None | auto_advance |
| None | focus_regained |
Values that changed meaning
Stories completedbecamestory_completed, singular. It marks one Story finishing all its pages, not a playlist completing. For whole-playlist completion, checkremaining_unread_count = 0on the same event.Moments Containerbecamemoments_tabs. A standalone container and a tabs component now report the same start trigger. Tell them apart withtab.total_tabs_count, which is0for a standalone container.
Auto advance on Moments
Moments report auto_skip where Videos and Stories report auto_advance. A funnel built on auto_advance across all three content types misses Moments. This is under review, so check this page before you build on either value.
Renamed events
| Legacy event | Unified event | What to watch |
|---|---|---|
playback_play / playback_pause | user_initiated_playback_play / user_initiated_playback_pause | The name now states that system pauses are excluded. |
ad_playback_play / ad_playback_pause | The same action names under event_category: ad | Filter on the category to isolate ad pauses. |
container_data_load | tab_data_load | Also fires for tabbed containers, which emitted nothing here before, so volume is higher than the event it replaces. |
search_click | search_opened | |
video_orientation_changed | video_orientation_changed_trigger |
Removed events
| Removed event | Where the signal went |
|---|---|
moments_playlist_start / moments_playlist_exit | moment_start and moment_exit. Count unique moments_session_id for sessions, and read moment_start_trigger for what opened the feed. Duration is the first moment_start to the last moment_exit in one session. |
casting_started / casting_ended | viewing_mode_transition where viewing_mode is casting. Duration is cast_duration on the transition away, so you no longer pair a start and an end yourself. |
pip_enabled / pip_disabled | viewing_mode_transition where viewing_mode is picture_in_picture. There's no duration counterpart, so derive it from consecutive transitions. |
cta_visible / cta_dismissed | The cta_shown boolean on story_start, moment_start, and video_start, plus cta_config. cta_dismissed has no replacement. |
audio | The audio_state property on every Story, Moment, and Video event. A toggle between two events is invisible, so treat toggle counts as a lower bound. |
Interaction echoes of share_click, audio, play, and pause | interaction emits interaction_view and interaction_answer only. The Viewer action is still reported under story or moment, so correlate by content ID. |
forced_playback_play / forced_playback_pause, and their ad variants | No replacement. A pause caused by your app, a tab switch, an ad, or backgrounding produces no event, so a forced against user ratio is no longer computable. |
search_suggestions_shown | search_suggestions_clicked still fires. For suggestion click-through, count those clicks against search_opened, which fires when the search screen opens. |
buffer_start / buffer_end | Removed, no replacement. |
Removed properties
| Removed property | Was on | What to do |
|---|---|---|
playback_action_method | Story, Moment, Interaction, Ad | No replacement. Press and hold against tap pause is no longer distinguishable. |
current_mode, next_mode | The viewing_mode object | viewing_mode is a single value, the mode being moved to. Read the previous mode from the preceding event. |
interaction_text | Interaction | No replacement. This carried the poll or quiz question text, so join to your CMS by interaction_id if you need it. |
audio_state | Interaction | Gone from Interaction events only. Read it from the Story, Moment, or Video event covering the same content. |
gesture_type | Moment, Video | No data is lost: it was never populated on those players, which have a single call to action gesture. Still sent on Story and Ad events. |
cta_config.type | Story, Moment, Video | No action. It was never populated. |
story_composition_type | Story | No replacement. Nothing ever populated it. |
is_liked | Story | No replacement. Stories have no like state. Still sent on Moment and Video. |
from_tab_title, to_tab_title, tab_navigation_type | Moment | Read tab_transition, which carries current_tab_*, next_tab_*, and transition_method. |
placement_id, placement_name, widget_fold_position, and widget_page_type are not in this table because they are not removed. They are Web tracking-plan fields that never arrived on iOS or Android. Don't delete Web reads of them.
Renamed and moved properties
| Legacy property | Unified property | What to watch |
|---|---|---|
origin_widget_id | origin_source_id and origin_source_type | Both are lists now. See Referring origin is a list. |
labels_expression | data_source_type and data_source_value | |
search_session_id, query_id, result_id on Story, Moment, and Video | referring.search_session_id, referring.search_query_id, referring.search_result_id | The search_ prefix on the last two is new. The same names stay on Search events themselves. |
container_id and other container_* properties | tab_id and the matching tab_* properties | |
video_session_trigger | video_start_trigger | Per-item, not per session. |
story_id, story_title, moment_id, moment_title on Ad events | content_id, content_title | Told apart by content_type on the same event. |
widget_name | widget_id | The two carried the same value. |
Interaction content_title | interaction.content_name | Same value, the tracking plan's name for it. |
next_video_id | next_video_id | The trailing space in the legacy key is fixed. Remove the workaround if you trimmed it yourself. |
Referring origin is a list
origin_source_id and origin_source_type are index-aligned lists that name every surface on the way into the session, root first. A widget tap that lands in a Moments tabs component reports ["<widget_id>", "<tab_id>"] and ["widget", "moments_tab"], so the tabs journey stays attributable to the widget click instead of starting at the tab.
| Journey | origin_source_id | origin_source_type |
|---|---|---|
| Widget to player | ["<widget_id>"] | ["widget"] |
| Widget to Moments tabs | ["<widget_id>", "<tab_id>"] | ["widget", "moments_tab"] |
| Host-opened tabs | ["<tab_id>"] | ["moments_tab"] |
| Deep link or notification | ["<source_id>"] | ["entry_point"] |
- Join widgets on the first element. Use
origin_source_id[0]whereorigin_source_type[0]iswidget. - A missing identifier is reported, not dropped. A deep link or notification where you passed no source ID reports the literal value
not_provided, so pass one if you want the attribution. - Widget fields stay scalar.
origin_widget_type,widget_builder_metadata, and the search identifiers are single values whenever a widget is in the chain.data_source_typeanddata_source_valuedescribe the tab's own content.
Search attribution moved here too. search_session_id, search_query_id, and search_result_id now ride every player event for the rest of the session, rather than only the start event.
Properties that changed meaning
This is the group that nothing warns you about, because the property still arrives under the name your code already reads.
| Property | Was | Now |
|---|---|---|
cta_shown | true whenever the content carried a call to action, even when your player style had it switched off | true only when the content carries one and your style displays it |
cta_config | {} when the content carried an empty object | Omitted unless at least one of text, color, or url has a value |
follow_button_visible | true whenever the content had a followable entity | true only when it does and your style shows the follow control |
story_index, total_stories_count, video_index, total_videos_count, next_story_id, next_video_id, next_moment_id, remaining_unread_count | Counted the ads the player interleaves, so totals and indexes shifted once an ad preceded the current item. next_video_id could return an ad's ID | Describe content only |
widget_size, thumbnail_size, advertiser_name | 0x0 or "" when unknown | Omitted when unknown |
origin_source_id, origin_source_type | One string each, naming the single surface that opened the session | Lists naming every surface, root first |
origin_source_type for a Moments tab | moments_tabs | moments_tab, one surface per element. The start trigger value moments_tabs is unchanged. |
New events
| Event | Category | Fires when |
|---|---|---|
tab_data_load | tab | A tab's or a standalone container's content finishes loading. Once per tab ID per session, and not on a fetch that returns nothing. |
tab_transition | tab | The active tab changes. Not on the first selection, and not on tapping the active tab. Carries current_tab_*, next_tab_*, and transition_method. |
playback_initial_start | ad | The first rendered frame of a custom native ad in the Moments or Stories player. |
description_expanded and description_collapsed already existed on some platforms. They now fire at most once per Moment view. Don't add them as new events.
Standalone container vs tabs component
tab_data_load fires for both. A single-tab tabs component reports 1, so it stays distinguishable.
| Property | Standalone container | Tabs component |
|---|---|---|
total_tabs_count | 0 | 1 or more |
tab_id | The container's own ID | The tab's ID |
tabs_source_id | The container's own ID | The tabs component's ID |
tab_index | 0 | The tab's position, or -1 for a tab hidden from the visible list |
tab_title | Absent | The tab's title |
New properties
Don't add a handler for a property that's already listed under Properties that changed meaning or Renamed and moved properties. Those already arrived. This table is only fields that didn't.
| Now available | Where | Replaces |
|---|---|---|
remaining_unread_pages | Story | Computing remaining pages yourself |
is_liked | Moment, Video | A lookup against your own like state |
user.followed_entities | Start, exit, and load events only | A lookup against follow state. It's kept off high-frequency events on purpose. |
follow_subject_id, follow_subject_type, follow_subject_provider_name | Moment | Inferring which entity the follow control offered |
custom_action_button_shown, custom_action_button_config.* | Story, Moment | A CMS config lookup for custom action buttons. Not available on Video. |
content_ratio, is_last_page, story_page_type | Story | Content lookups. story_page_type is on page-scoped events, not on story_start. |
loop_settings.infinite_loop, loop_settings.loop_and_advance | Moment | Player config lookups for loop analysis |
tabs_enabled, tabs_configuration, initial_tab_title, active_tab_title | Moment | There was no way to know the tab context |
cast_duration | Video | Pairing a casting start and end yourself. It's wall-clock seconds, so a pause or a seek during the cast doesn't change it. |
back_to_live_trigger, video_time_start, video_time_end | Video | Values that were computed but never sent |
cc_state | Video | It was only on Story and Moment before |
gesture_type | Story | It was declared but never populated |
content_name | Widget | Fetching the clicked item's name separately |
content_page_id, story_page_content_extra_info | Interaction | Populated from the Story page |
data_source_config, widget_builder_metadata | referring | Inferring how a view was sourced. Recommendation sources report the generation the backend served, such as for_you_v1, falling back to for_you when the response carries no version. |
session_id, content_id, content_title, content_type, audio_state, content_extra_info, backoffice_campaign_data on ad_requested | Ad | Joining an ad request to the content that made it |
video.quality arrives null. The player doesn't expose rendition quality. Widget placement fields (placement_id, placement_name, widget_fold_position, widget_page_type) never arrive on iOS or Android. They stay in the Web tracking plan.
Ads in the tracking plan
Ads used to sit outside the plan and were unaffected by schema changes. Ad events now ride the same 2.1 envelope under event_category: ad, trimmed per event like every other domain, including custom native, IMA, and banner.
Ad pauses are reported only when the Viewer causes them, so the forced variants are gone. Banner events attribute to the Story the banner was created for, not whichever Story is on screen when the ad network calls back.
Behavioral rules
These rules hold on every platform. They're easy to miss reading a property list, and reports built on the old behavior can quietly disagree with them.
| Rule | What it means |
|---|---|
| Start and exit alternate | Per player, exactly one *_start is open at a time and one *_exit or video_end closes it. A loop restart of the same item is the only intended repeat start. |
focus_lost pairs with focus_regained | A player covered without closing, such as a host tab switch, a modal, or the call to action web view, exits with focus_lost and resumes with focus_regained in the same session. |
app_background and app_foreground are symmetric | An app_foreground start is sent only where an app_background exit was. Entering picture in picture or a route picker produces neither. |
| Load events fire once per session | widget_load fires once per widget_id, and tab_data_load once per tab_id. Refetches, retries, and re-renders don't re-fire, and a fetch that returns nothing fires nothing. |
| Navigation type comes from the action | Timer and auto-advance transitions report automatic. Taps, swipes, and accessibility navigation report manual. It's never derived from the viewed percentage. |
| Exit triggers say why playback ended | story_completed means the Story finished, whether or not another follows. |
| Start triggers describe the item, not the session | The first item carries the entry trigger. Later items report how they were reached: swipe, skip, or auto_advance. A loop start carries the trigger of the current viewing. |
| Viewed percent is per viewing segment | *_duration_viewed_percent covers the segment since the matching start, not the whole item across a background or focus split. Sum the segments of one session ID for total exposure. |
playback_initial_start fires once per item view | Never again on a loop or a replay of the same item. |
| Description events are capped per Moment view | description_expanded and description_collapsed fire at most once each. Loops don't reset the cap; moving to another Moment does. |
share_click fires when the sheet opens | Never on a tap that failed to open one. |
| Rapid seeks aggregate | A burst of seeks is one seek event carrying the accumulated seek_total_time. A burst closes after one second idle, on a direction reversal, or on an item change. |
| Fullscreen to inline is a transition | Leaving fullscreen back to inline is video_end then video_start, both with viewing_mode_transition, in the same video_session_id. It's never close_button. |
| Story event order is fixed | story_start precedes the first story_page_start, and the last page's story_page_exit precedes story_exit. |
onEventTriggered fires exactly once | Once per event, in emission order. Nothing is replayed. |
| Like and follow fire on the transition | Not as a toggle count. |
Related
- Migrate on iOS: the Swift API changes and the iOS data deltas
- Migrate on Android: the Kotlin API changes and the Android data deltas
- Migrate on Web: the DOM listener changes and the Web data deltas
- Reporting changes to expect: metrics that move without anything failing
- 2026 events schema - breaking changes ahead: timeline
- Events (legacy): the schema shipping until you upgrade
Updated 5 days ago
