Previous posts can be used in conjunction with this one. The 2024 Guide is also listed as part of this guidance and is available under the following link (we’ve since updated our service):
July 31st 2025, by @cebratec, @Mark_Schuster, @Peter_Hammel, @Tushar_Jadhav, @rsack
Universal Search and Browse, Account Login & Personalization
Fire TV supports searching and launching content using a Fire TV remote, touch, or voice. The modality agnostic Content Launcher API supports searching, browsing, and playing content using different interfaces.
Image 1: Universal Search and Browse
This guide will focus on a step by step walkthrough to achieve the following three areas:
-
- Universal Search and Browse
-
- Adding Account Login
-
- Adding Personalization (Continue Watch) Feature
Your entry point to anything Content Launcher in the documentation on our Dev Portal is here.
Before you start (Prerequesite)
All catalog integrated apps must integrate the Content Launcher API. Catalog integration is the process of describing your app’s media according to Amazon’s Catalog Data Format (CDF), and then regularly uploading the catalog to an S3 bucket at Amazon. Contact your Amazon representative if you have any questions around that specifically.
We also recommend reading the Content Launcher Overview before diving. in.
1. Universal Search and Browse
Universal Search and Browse allows users to discover your app’s content on Fire TV, even if they don’t have the app installed. Catalog file integrates your app’s media’s metadata into the Fire TV home screen and global search results. This catalog file allows your app’s content to appear in users searches and in watch options on the content details pages.
This guide will cover a real life example, the general guidance can be found here:
Step 1: Update your manifest file
This part can be also found as part of our documentation on the developer portal, here.
In your app’s manifest.toml file, include Content Launcher support. This example assumes your package ID is com.amazondeveloper.keplervideoapp
. Replace this package ID with your app’s package ID.
Your app must be properly configured to interact with the Vega Media Content Launcher APIs.
If you are unsure about your PartnerID contact your Amazon representative.
schema-version = 1
[package]
title = "<Your app title>"
id = "com.amazondeveloper.media.sample"
[components]
[[components.interactive]]
id = "com.amazondeveloper.media.sample.main"
runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0"
launch-type = "singleton"
# The category "com.amazon.category.kepler.media" is only necessary for the primary component, which is identified in the [[extras]]
# section of the manifest using the "component-id" value.
categories = ["com.amazon.category.main", "com.amazon.category.kepler.media"]
[processes]
[[processes.group]]
component-ids = ["com.amazondeveloper.media.sample.main"]
[offers]
[[offers.interaction]]
id = "com.amazondeveloper.media.sample.main"
[[message]]
uri = "pkg://com.amazondeveloper.media.sample.main"
# Match the privileges used in [[offers.interaction]]. If privileges are not added, then use "*".
sender-privileges = ["*"]
receiver-privileges = ["self"]
[[offers.module]]
id = "/com.amazondeveloper.media.sample.module@ISomeUri1"
includes-messages = ["pkg://com.amazondeveloper.media.sample.main"]
[[extras]]
key = "interface.provider"
component-id = "com.amazondeveloper.media.sample.main"
[extras.value.application]
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentLauncherServer"
# The "override_command_component" field is only necessary when the "component-id" specified in the [[extras]]
# section differs from the one you're providing here. This allows you to use a different component for
# command execution than the one initially defined in the extras, giving you flexibility in your
# configuration setup.
# For example, if you're using Account Login interface in addition to the Content Laucher interface
# you would need to override the Account Login status attribute to be directed to the service
# component with the following line:
# override_command_component = { Status = "com.amazondeveloper.media.sample.interface.provider" }
attribute_options = ["partner-id"]
static-values = { partner-id = "<Your partner id>" }
Step 2: Include the necessary package dependencies in your app:
(...)
"dependencies": {
"@amzn/react-native-kepler": "~2.0.0",
"@amzn/kepler-media-content-launcher": "^2.0.0",
(...)
},
(...)
Step 3: Implement the Content Launcher API
In your App.tsx
you will want to include the following setup:
import {
usePreventHideSplashScreen,
useHideSplashScreenCallback,
useComponentInstance,
IComponentInstance,
} from '@amzn/react-native-kepler';
import {
ILauncherResponse,
IContentLauncherHandler,
ContentLauncherStatusType,
ContentLauncherServerComponent,
IContentSearch,
ILaunchContentOptionalFields,
} from '@amzn/kepler-media-content-launcher';
// (...)
const AppInitialization = () => {
const componentInstance: IComponentInstance = useComponentInstance();
// Content Launcher setup
useEffect(() => {
if (componentInstance) {
const factory = new ContentLauncherServerComponent();
const contentLauncherHandler: IContentLauncherHandler = {
async handleLaunchContent(
contentSearch: IContentSearch,
autoPlay: boolean,
_optionalFields: ILaunchContentOptionalFields
): Promise<ILauncherResponse> {
try {
const searchParameters = contentSearch.getParameterList();
if (searchParameters.length > 0) {
for (const searchParameter of searchParameters) {
const paramType = searchParameter.getParamType();
const searchString = searchParameter.getValue();
const additionalInfoList = searchParameter.getExternalIdList();
for (const additionalInfo of additionalInfoList) {
const searchName = additionalInfo.getName();
const entityID = additionalInfo.getValue();
if (searchName === 'amzn_id') {
// Use this entity ID to get exact content from your catalog.
console.log('Found Amazon ID:', entityID);
// Content Launcher should always navigate directly to video player
console.log('Content Launcher - loading and playing content:', entityID);
// Load the content first, then navigate to video player
try {
// IMPLEMENT HERE
// load content and navigate to your video player and play content
} catch (error) {
console.log('Content Launcher - error loading content:', entityID, error);
}
} else {
// ex:
// entityID= "ENTITY_ID" :
// searchName = "amzn1.p11cat.merged-video.087c9371-6bb7-5edb-bcef-f8717fde0a8a"
//
console.log('Other search identifier:', searchName, entityID);
// For other search identifiers, also load and play content
console.log(
'Content Launcher - loading and playing content for identifier:',
entityID
);
try {
const content = await ContentHelper.loadContent(
entityID,
Content.movieDetailRequiredFields,
false
);
if (content !== undefined && content !== null) {
ContentHelper.handlePlay(
refNavigation.current as unknown as NavigationProp<RootStackParamList>,
content,
VideoType.Default,
false
);
console.log(
'Content Launcher - successfully initiated playback for identifier:',
entityID
);
} else {
console.log(
'Content Launcher - failed to load content for identifier:',
entityID
);
}
} catch (error) {
console.log(
'Content Launcher - error loading content for identifier:',
entityID,
error
);
}
}
}
}
if (autoPlay) {
console.log('Content_Launcher_Sample: Quickplay');
// Handle play logic here using the data retrieved above.
} else {
console.log('Content_Launcher_Sample: In-App search');
// Handle search logic here using the data retrieved above.
}
} else {
console.log('Content_Launcher_Sample: Error fetching search string');
}
const launcherResponse = factory.makeLauncherResponseBuilder()
.contentLauncherStatus(ContentLauncherStatusType.SUCCESS)
.build();
return Promise.resolve(launcherResponse);
} catch (error) {
console.log('Error handling content launch:', error);
const launcherResponse = factory.makeLauncherResponseBuilder()
.contentLauncherStatus(ContentLauncherStatusType.URL_NOT_AVAILABLE)
.build();
return Promise.resolve(launcherResponse);
}
},
};
const contentLauncherServer = factory.getOrMakeServer();
contentLauncherServer.setHandlerForComponent(contentLauncherHandler, componentInstance);
console.log('Content Launcher initialized successfully');
return () => console.log('Content Launcher cleanup');
}
}, [componentInstance, refNavigation]);
// (...)
}
2. Adding Account Login (with Headless)
If you are integrating into an existing headless setup, this guide will be most applicable to you. If you don’t have an existing headless implementation, you can refer to the docs directly on the Developer Portal.
The Vega Media Account Login API provides a way for apps to report their authentication status to the system. Apps need to refresh the user’s login status upon each launch, which is done by the Account Login API. There are two account login states: signed in, and signed out. By keeping the login status up-to-date, apps make sure that users have access to appropriate content and features.
Image 2: Example of an authenticated state for Universal Search
Image 3: Example of a non-authenticated state for Universal Search
Apps should update the system with their login status in the following situations:
- During the app’s initial launch
- When there’s a change in the user’s subscription status
- Upon request from other services
Step 1: Update the app manifest
schema-version = 1
[package]
title = "<Your app title>"
id = "com.amazondeveloper.media.sample"
[components]
[[components.interactive]]
id = "com.amazondeveloper.media.sample.main"
runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0"
launch-type = "singleton"
# The category "com.amazon.category.kepler.media" is only necessary for the primary component, which is identified in the [[extras]]
# section of the manifest using the "component-id" value.
categories = ["com.amazon.category.main", "com.amazon.category.kepler.media"]
+
+[[components.service]]
+id = "com.amazondeveloper.media.sample.interface.provider"
+runtime-module = "/com.amazon.kepler.headless.runtime.loader_2@IKeplerScript_2_0"
+launch-type = "singleton"
[processes]
[[processes.group]]
component-ids = ["com.amazondeveloper.media.sample.main"]
+
+[[processes.group]]
+component-ids = ["com.amazondeveloper.media.sample.interface.provider"]
[offers]
[[offers.interaction]]
id = "com.amazondeveloper.media.sample.main"
+
+[[offers.service]]
+id = "com.amazondeveloper.media.sample.interface.provider"
+required-privileges = ["com.amazon.multimedia.privilege.session.manage"]
[[message]]
uri = "pkg://com.amazondeveloper.media.sample.main"
# Match the privileges used in [[offers.interaction]]. If privileges are not added, then use "*".
sender-privileges = ["*"]
receiver-privileges = ["self"]
[[offers.module]]
id = "/com.amazondeveloper.media.sample.module@ISomeUri1"
includes-messages = ["pkg://com.amazondeveloper.media.sample.main"]
[[extras]]
key = "interface.provider"
-
component-id = "com.amazondeveloper.media.sample.main"
[extras.value.application]
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentLauncherServer"
-
-# The "override_command_component" field is only necessary when the "component-id" specified in the [[extras]]
-# section differs from the one you're providing here. This allows you to use a different component for
-# command execution than the one initially defined in the extras, giving you flexibility in your
-# configuration setup.
-
-# For example, if you're using Account Login interface in addition to the Content Laucher interface
-# you would need to override the Account Login status attribute to be directed to the service
-# component with the following line:
-# override_command_component = { Status = "com.amazondeveloper.media.sample.interface.provider" }
-attribute_options = ["partner-id"]
-static-values = { partner-id = "<Your partner id>" }
+attribute_options = ["partner-id"]
+static-values = { "partner-id" = "<Your partner id>" }
+
+[[extras.value.application.interface]]
+interface_name = "com.amazon.kepler.media.IAccountLoginServer"
+attribute_options = ["Status"]
+# In this manifest example, we have both Content Launcher and Account Login interfaces defined.
+# Since the Account Login cluster should be handled by a service component instead of an
+# interactive one for responsiveness, we use "override_attribute_component" to redirect
+# calls for the "Status" attribute to the service component.
+override_attribute_component = { Status = "com.amazondeveloper.media.sample.interface.provider" }
+
+[needs]
+[[needs.module]]
+# The dot (.) after "media" is intentional in this format. This notation will be changed in a
+# future release.
+id = "/com.amazon.kepler.media.@IAccountLogin1"
+
+[[needs.module]]
+id = "/com.amazon.kepler.media@IContentLauncher1"
Step 2: Include package dependencies
The Vega Media Account Login API is offered to TypeScript developers using the system turbo module amzn/kepler-media-account-login
. This turbo module is part of the SDK package. To use the API, update the package.json file to take the turbo module as a dependency, and add amzn/headless-task-manager
to create a headless service app.
(...)
"dependencies": {
"@amzn/kepler-media-account-login": "^1.1.0",
"@amzn/headless-task-manager": "^1.1.0",
(...)
}
(...)
Step 3-7: Add the Vega Media Account Login business logic
Step 3 to 7 are combined in this example, if you want to go through them one by one, you can read up about it here.
Create a file named AccountLoginWrapper.ts to contain the core business logic for the Vega Media Account Login API. In this file, create and export a class named AccountLoginWrapper
. This class has a single member variable: an instance of the IAccountLoginServerAsync
interface, used to interact with the Account Login server. This file will also globally create an instance of AccountLoginServerComponent
and store it in a variable named accountLoginServerComponent
.
import {
AccountLoginServerComponent,
IAccountLoginHandlerAsync,
IAccountLoginServerAsync,
IStatus,
StatusType,
} from '@amzn/kepler-media-account-login';
import { IComponentInstance } from '@amzn/react-native-kepler';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const accountLoginServerComponent = new AccountLoginServerComponent();
const getLoginStatus = async (): Promise<string> => {
const getItemStatus = await AsyncStorage.getItem('loginStatus');
console.log('get login status: ', getItemStatus);
return getItemStatus;
};
export class AccountLoginWrapper {
accountLoginServer?: IAccountLoginServerAsync;
onStart(componentInstance: IComponentInstance): Promise<void> {
this.setupAccountLoginServer();
this.setupAccountLoginHandler(componentInstance);
return Promise.resolve();
}
onStop(): Promise<void> {
// Add any cleanup code here.
return Promise.resolve();
}
async getAccountLoginStatus(): Promise<IStatus> {
return accountLoginServerComponent
.makeStatusBuilder()
.status(
// Fetch status from persistent storage.
(await getLoginStatus()) ? StatusType.SIGNED_IN : StatusType.SIGNED_OUT
)
.build();
}
createAccountLoginHandler(): IAccountLoginHandlerAsync {
return {
handleReadStatus: async (): Promise<IStatus> => {
console.log('handleReadStatus() invoked.');
return await this.getAccountLoginStatus();
},
};
}
setupAccountLoginServer() {
console.log('setupAccountLoginServer() invoked.');
try {
this.accountLoginServer = accountLoginServerComponent.getOrMakeServer();
} catch (error) {
this.accountLoginServer = undefined;
console.error('setupAccountLoginServer() failed creating Account Login server: ', error);
return;
}
}
setupAccountLoginHandler(componentInstance: IComponentInstance) {
if (this.accountLoginServer === undefined) {
console.log('AccountLoginServer is undefined');
return;
}
try {
this.accountLoginServer.setHandlerForComponent(
this.createAccountLoginHandler(),
componentInstance
);
} catch (error) {
console.log(error);
}
}
async updateStatus(loginStatus: boolean) {
console.log('updateStatus() invoked.');
const status = accountLoginServerComponent
.makeStatusBuilder()
.status(loginStatus ? StatusType.SIGNED_IN : StatusType.SIGNED_OUT)
.build();
try {
this.accountLoginServer?.updateStatus(status);
} catch (error) {
console.error('updateStatus() Failed updating login status: ', error);
}
}
}
export const AccountLoginWrapperInstance = new AccountLoginWrapper();
export const onStartService = (componentInstance: IComponentInstance): Promise<void> => {
return AccountLoginWrapperInstance.onStart(componentInstance);
};
export const onStopService = (): Promise<void> => {
return AccountLoginWrapperInstance.onStop();
};
Step 8: Adjust your headless service
Assuming you are already using headless for your playback, you service.js
file will most likely look like this:
import { HeadlessEntryPointRegistry } from "@amzn/headless-task-manager";
import { onStartService, onStopService } from "./src/services/PlayerService";
import {
onStartService as onStartAccountLoginService,
onStopService as onStopAccountLoginService,
} from "./src/util/other/AccountLoginWrapper";
HeadlessEntryPointRegistry.registerHeadlessEntryPoint(
"<your-app-package-name>.service::onStartService",
() => onStartService
);
HeadlessEntryPointRegistry.registerHeadlessEntryPoint(
"<your-app-package-name>.service::onStopService",
() => onStopService
);
HeadlessEntryPointRegistry.registerHeadlessEntryPoint2(
"<your-app-package-name>.interface.provider::onStartService",
() => onStartAccountLoginService
);
HeadlessEntryPointRegistry.registerHeadlessEntryPoint2(
"<your-app-package-name>.interface.provider::onStopService",
() => onStopAccountLoginService
);
Fire TV system components automatically invoke onStartService()
to initialize the Account Login server as a headless service. During this process, the handleReadStatus()
function is registered for the specific service component instance.
When the system invokes this handler later, handleReadStatus()
returns the current login state (signed-in or signed-out) as reported by the app.
Step 9: Initialize the Account Login server to report the login state
The interactive component (UI app) must also initialize the Account Login server to report the user’s login state using the updateStatus()
method. - E.g. within your App.tsx
// These imports are probably already there in your app and just serve a visual anchor point
import {
NavigationContainer,
NavigationState,
useNavigationContainerRef,
} from '@amzn/react-navigation__native';
import { createStackNavigator } from '@amzn/react-navigation__stack';
import React, { useCallback, useEffect, useState } from 'react';
// (...)
// start here
import {
usePreventHideSplashScreen,
useHideSplashScreenCallback,
useComponentInstance,
IComponentInstance,
} from '@amzn/react-native-kepler';
import { AccountLoginWrapperInstance } from './util/other/AccountLoginWrapper';
// (...)
const AppInitialization = () => {
// (...)
const componentInstance: IComponentInstance = useComponentInstance();
useEffect(() => {
AccountLoginWrapperInstance.onStart(componentInstance);
return () => {
AccountLoginWrapperInstance.onStop();
};
}, []);
// (...)
}
Step 10: Send login status updates
The interactive component (UI app) must call the updateStatus()
method with the current login status in the following scenarios:
- When the app is launched.
- When the user’s login status changes
Each time, the status must be stored in persistent storage before calling updateStatus()
, so that handleReadStatus()
retrieves and returns the correct value. This way, the system consistently reflects the user’s authentication state, providing a seamless user experience.
This setup allows your app to manage login status requests, even when the main app is not actively running.
AccountLoginWrapperInstance.updateStatus('<send login status>');
Troubleshooting & Testing
3. Adding Personalization (Continue Watching) Feature
Sharing customer activity with Fire TV through the Content Personalization API enables customers to discover more personalized movie and TV show content. By providing information on watch activity, watchlist additions, DVR recordings, and purchases, Fire TV is able to help the customer more easily resume in-progress content, and make more accurate recommendations for new movies and TV shows.
Image 4: Personalization Continue Watching
Developer Portal Documentation Links:
Step 1: Include the package dependencies in your app
Add the kepler-content-personalization
and headless-task-manager
as well as amzn/kepler-epg-provider
dependencies in your package.json file.
"dependencies": {
"@amzn/react-native-kepler": "~2.0.0",
"react": "18.2.0",
"react-native": "0.72.0",
"@amzn/kepler-content-personalization": "^1.2.0",
"@amzn/kepler-epg-provider": "^1.0.0",
"@amzn/headless-task-manager": "^1.0.0",
(...)
}
(...)
- The
kepler-content-personalization
package provides APIs for sending your content personalization data to the system. - The
amzn/kepler-epg-provider
package would provide the dependencies used forchannelDescriptor
inPlaybackEvent
data model. - The
headless-task-manager
package provides APIs to register your data-pull background service with the system. More details are given below.
Step 2. Update your manifest file
+## Define your app's service component which can handle data request.
+[[components.service]]
+id = "com.amazondeveloper.media.sample.content.dataRefresh.provider"
+runtime-module = "/com.amazon.kepler.headless.runtime.loader_2@IKeplerScript_2_0"
+launch-type = "singleton"
+[[processes.group]]
+component-ids = ["com.amazondeveloper.media.sample.content.dataRefresh.provider"]
+[wants]
+## Defines that your app has a dependency on the Content Personalization data service
+[[wants.service]]
+id = "com.amazon.tv.developer.dataservice"
+## Defines the privilege your app needs in order to use the Content Personalization interface to provide data
+[[needs.privilege]]
+id = "com.amazon.tv.content-personalization.privilege.provide-data"
+## Defines the data refresh service component of your app
+[[offers.service]]
+id = "com.amazondeveloper.media.sample.content.dataRefresh.provider"
+## Defines support for the Content Personalization interface and the attributes
+[[extras.value.application.interface]]
+interface_name = "com.amazon.kepler.media.IContentPersonalizationServer"
+attribute_options = ["SupportedCustomerLists", "DataRefreshComponentId"]
+[extras.value.application.interface.static_values]
+SupportedCustomerLists = ["Watchlist"]
+DataRefreshComponentId = "com.amazondeveloper.media.sample.content.dataRefresh.provider"
Warning: Each interface configuration must be kept separate. Do not combine or mix attributes between different interfaces in the manifest file.
Here is an example of the correct way to define multiple interfaces:
[extras.value.application]
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentPersonalizationServer"
attribute_options = ["SupportedCustomerLists", "DataRefreshComponentId"]
[extras.value.application.interface.static_values]
SupportedCustomerLists = ["Watchlist"]
DataRefreshComponentId = "com.amazondeveloper.keplervideoapp.content.dataRefresh.provider"
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentLauncherServer"
override_command_component = { LaunchContent = "com.amazondeveloper.keplervideoapp.main" }
attribute_options = ["partner-id"]
[extras.value.application.interface.static_values]
partner-id = "<Your partner id>"
And here is an incorrect way to do this. Do not do it this way.
[extras.value.application]
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentPersonalizationServer"
attribute_options = ["SupportedCustomerLists", "DataRefreshComponentId"]
[[extras.value.application.interface]]
interface_name = "com.amazon.kepler.media.IContentLauncherServer"
override_command_component = { LaunchContent = "com.amazondeveloper.keplervideoapp.main" }
attribute_options = ["partner-id"]
[extras.value.application.interface.static_values]
partner-id = "<Your partner id>"
SupportedCustomerLists = ["Watchlist"]
DataRefreshComponentId = "com.amazondeveloper.keplervideoapp.content.dataRefresh.provider"
Step 3. Make a sample API call
Begin with a sample/mock event generated at the app launch. To construct the event and send it, use the following code - make these changes where applicable in your app (e.g. when sending play-beacon events from the player:
const playbackEvent: IPlaybackEvent = new PlaybackEventBuilder()
.playbackPositionMs(1000)
.playbackState(PlaybackState.PLAYING)
.durationMs(2000)
.eventTimestamp(new Date())
.contentId(
new ContentIdBuilder()
.id('content_CDF_ID')
.idNamespace(ContentIdNamespaces.NAMESPACE_CDF_ID)
.build(),
)
.profileId(
new ProfileIdBuilder()
.id('myProfileId')
.idNamespace(ProfileIdNamespaces.NAMESPACE_APP_INTERNAL)
.build(),
)
.buildActiveEvent();
// Send the event
ContentPersonalizationServer.reportNewPlaybackEvent(playbackEvent);
Step 4. Validate the integration
Trigger the sample event code you constructed to run inside your app. After you run the code successfully, view the logs to validate the SDK has been linked to your app and is processing the message.
You can validate the steps by searching your app logs:
journalctl --follow |grep -Ei 'kepler.tv.personalization'
The log message you receive for the event shown in Step 3 should contain at least one of the following logs:
Dec 15 00:53:15.705114 carat-3fc19fb5ae526e4d local0.info keplerscript-ru[22814]: 1483317178 INFO kepler.tv.personalization.ancp.turbomodule:reportNewPlaybackEvent called
Dec 15 00:53:15.711945 carat-3fc19fb5ae526e4d local0.info keplerscript-ru[22814]: 1483317178 INFO kepler.tv.personalization:PlaybackEventBuilder buildActiveEvent() called
Dec 15 00:53:15.711990 carat-3fc19fb5ae526e4d local0.info keplerscript-ru[22814]: 1483317178 INFO kepler.tv.personalization:reportNewPlaybackEvent async call received
Additional Integration Steps
To make API calls as part of in-app functionality or to implement a pull data service for background or off-device data, please follow our in depth documentation on the Developer Portal.
Wrap-Up
By integrating your catalog, deep linking, account login, and callback handlers, you can fully support Fire TV’s Universal Search and Browse experience. This makes it easier for users to find and watch your content seamlessly, whether through voice, search, or remote control.