As part of Integration for FANXP it is quite often required to implement native visual for the campaign inside mobile application. The simplest option would be to use web view and native url for the campaign or WPA like implementation, however our server supports and provides API for fully native implementation
Overall in order to build FanXP experience into native app you need to integrate following components
- Authentication with email/password
- Handler for forgotten password
- Handler for new registration
- Initialization of WebSocket Connection (Socket.IO)
- Video Player integration (Native control for HLS feed)
- Handler for Quiz/Clicker/Static Overlay events
- Chat Integration
- Style and Look&Feel synchronization between Web and Native FanXP
Examples are written in python, but obviously appropriate language has to be used in real implementation
Last Update March 4, 2026Authentication Flow
Authentication allows you to obtain a session_uid, which is used for all subsequent
API calls. The full authentication flow uses the Sessions/Connect API
(POST /api/v1/sessions/connect) and supports:
- Checking if a fan exists (
network=emailornetwork=phone) - Sending verification codes (
network=verify_emailornetwork=verify_sms) - Submitting verification codes (
network=submit_verification_code) - Registering new fans (
network=register) - Password login (
network=password)
For complete documentation, code examples, and the full step-by-step flow, see the Website Integration page (Sessions/Connect API section).
Campaign Configuration
Once you have the credentials sorted out, next step will be to get a list of available campaigns. Campaigns are grouped in the entity called Business, which represents a team. Calls below allow you to fetch all businesses configured on the account and campaigns, which are currently scheduled to be included (e.g. needs to be presented to the user). MediaMap includes a map to all assets uploaded as part of campaign. Assets can be rendered via adding link https://nest.tradablebits.com/fb_media/11111111-1111-1111-11111111111111
import requests
data = {"api_key":"9eb875ac-6baf-4ee9-b189-afc725cba7f6"}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/businesses",params=data)
if res.status_code == 200:
result = res.json()
for row in result:
print(row["business_id"])
print(row["business_name"])
print(row["legal_terms"])
print(row["legal_rules"])
print(row["legal_privacy"])
print(row["scheduler_props"])
print(row["media_map"])
if row["media_map"].get("header_image",{}).get("1",None):
header_image = "https://nest.tradablebits.com/fb_media/%s" % row["media_map"].get("header_image",{}).get("1",None)
else:
print("Error")
data = {"api_key":"9eb875ac-6baf-4ee9-b189-afc725cba7f6","app_type":"fanxp","is_scheduled":"true", "business_id":12345}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/apps",params=data)
if res.status_code == 200:
result = res.json()
for row in result:
print("campaign",row)
else:
print("Error")
Once you choose a specific campaign, your application can start integration into dependencies for full FanXP experience. In particular, connect into Socket IO for control messages and chat, fetch video configuration and setup a listener for quiz push.
First, we need to check authorization. This will check CRM gate and provide back details for campaign to connect
import requests
data = {"session_uid":"28c02479-068d-4600-bcab-fc4fcfbc43af"}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/fanxp/12272",params=data)
if res.status_code == 200:
result = res.json()
account_id = result["account_id"]
server_timestamp = result["current_timestamp"]
ws_rooms = result["rooms"]
memberships = result["memberships"]
chat_rooms = [x for x in ws_rooms if x["room_type"] == "global_chat"]
permanent_rooms = [x for x in ws_rooms if x["room_type"] == "permanent_rooms"]
public_rooms = [x for x in ws_rooms if x["room_type"] == "public_rooms"]
control_rooms = [x for x in ws_rooms if x["room_type"] == "control"]
if len(control_rooms) > 0: control_room = control_rooms[0]
if len(chat_rooms) > 0: chat_room = chat_rooms[0]
print(account_id)
print(chat_room)
else:
print(res.text)
print("Error")
WebSocket Connection
Once, authorization, it obtained we need to initialize the connection to Socket IO Server. Please keep in mind, websocket server allows only a single connection per fan and any following connection will eliminate the past one.
import requests
import socketio
import asyncio
sio = socketio.AsyncClient(request_timeout=1)
account_id = 1
session_uid = "28c02479-068d-4600-bcab-fc4fcfbc43af"
room_name = "global_chat:room:12272"
def emit_callback(res):
print("Result:", res)
async def async_run():
await sio.connect("https://websockets.tradablebits.com")
data = {"account_id": account_id, "session_uid": session_uid, "room_name": room_name}
await sio.emit("join", data, callback=emit_callback)
message = "Hello World"
data = {"account_id": account_id, "room_name": room_name, "message": message}
await sio.emit("chat_message", data, callback=emit_callback)
await sio.wait()
asyncio.run(async_run())
With websocket connection you need to listen for the following events
chat_message: This event includes a new chat message. Chat from the user is generated as the same event.
{
chat_message_uid: "[uuid]",
message: "html escaped message",
room_name: "[room_name]",
display_name: "user handle for the creator",
unix_timestamp: "creation timestamp",
fan_id: "ID for the user, who created a message",
moderator_status: "null|moderator"
}
remove_chat_message: This event sets "is_removed" value in "log_chat_messages" table to true, hiding the message on the front-end and flagging it in the chat moderation panel.
{
chat_message_uid: "[uuid]"
}
unflag_chat_message: This event sets "is_removed" value in "log_chat_messages" table to false, showing the message on the front-end and unflagging it in the chat moderation panel.
{
chat_message_uid: "[uuid]"
}
tbits_live_update: This event is sent on the control room and will include a state change for the performance
// 1. Status change for the campaign:
{ "action": "state_change",
"state": "off|pending|active|finished",
"items": {
"vods": [{
"activity_id": null,
"clicks": 0,
"creation_timestamp": 1602874546,
"description": null,
"fan_id": null,
"field_idx": 0,
"impressions": 0,
"is_approved": null,
"is_correct": true,
"media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"negative_vote_count": 0,
"option_id": 1,
"option_name": "first vod",
"option_type": "vimeo",
"page_tab_id": 1
}],
"home_items": {
"live_overlay": {
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"overlay_type": "static",
"overlay_public_name": "Temporary item in home tab",
"body_text": "Title for temporary item in home tab",
"target_url": "https://example.com",
"button_text": "Click here",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "https://example.com/example.png",
"sponsor_name": "tradablebits",
"last_update_timestamp": 1602872546
},
"pinned_overlays": [
{
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"overlay_type": "static",
"overlay_public_name": "Item pinned in home tab",
"body_text": "Item pinned in home tab title",
"target_url": "https://example.com",
"button_text": "Click here",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "https://example.com/example.png",
"sponsor_name": "tradablebits"
}
]
}
}
}
// 2. Live Feed update (new Feed):
{ "action": "update_live_feed",
"live_feed": {
"feed_name": "character feed",
"video_endpoint_uid": "XXXX-XXXX-XXXX-XXXXXXXX",
"vimeo_key": null,
"vimeo_event_key": 1234,
}
"live_overlay": {
}
}
// 3. Live Overlay update (new Clicker):
{ "action": "update_live_overlay",
"live_feed": {},
"live_overlay": {
"overlay_type": "clicker",
"clicker_instance_uid": "XXXX-XXXX-XXXX-XXXXXXXX",
"overlay_public_name": "test,
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "http://www.google.ca",
"sponsor_name": "google",
"overlay_timer": 30,
"overlay_delay": 5,
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
}
}
// 4. Live Overlay update (new Static Overlay):
{ "action": "update_live_overlay",
"live_feed": {},
"live_overlay": {
"overlay_type": "static",
"overlay_public_name": "testing",
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "http://www.google.ca",
"button_text": "click me",
"body_text": "some test message",
"target_url": "https://www.tradablebits.com",
"sponsor_image_url": "http://www.google.ca",
"sponsor_name": "google",
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
}
}
// 5. Live Overlay update (new Question):
{ "action": "update_live_overlay",
"live_feed": {},
"live_overlay": {
"overlay_type": "quiz_question",
"quiz_question_uid": 123121,
"overlay_public_name": "what is the answer to the ultimate question in the universe",
"options": [{"answer_idx":1,"answer":42}],
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "http://www.google.ca",
"sponsor_name": "google",
"show_results": true,
"overlay_timer": 30,
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
}
}
// 6. Live Overlay Update (new Custom Field Overlay)
{ "action": "update_live_overlay",
"live_feed": {},
"live_overlay": {
"overlay_type": "custom_field"
"overlay_public_name": "Tell me something"
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "http://www.google.ca",
"sponsor_name": "google",
"crm_field_key": "some_key"
"custom_field":{
crm_field_key: "some_key",
field_type: "select",
},
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"overlay_timer": 10
}
}
// 7. Live Overlay update (new Banner Overlay):
{ "action": "update_live_overlay",
"live_feed": {},
"live_overlay": {
"overlay_type": "banner",
"overlay_public_name": "some name",
"sponsor_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX",
"sponsor_image_url": "http://www.google.ca",
"sponsor_name": "google",
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
}
}
//8. Pin/unpin Static Overlay:
{ "action": "update_pinned_overlays",
"overlay": {
"body_text": "some description"
"button_text": "click here"
"content_media_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"is_pinned": false
"last_push_timestamp": 1603409607
"overlay_log_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"overlay_public_name": "some name!"
"overlay_type": "static"
"app_overlay_uid": "XXXXX-XXXX-XXXXX-XXXXXXXXX"
"sponsor_image_url": null
"sponsor_media_uid": null
"sponsor_name": null
"target_url": "https://example.com"
}
}
With websocket connection you are able to emit the following events
overlay_interaction: This event stores overlay_log_uid, fan_id, action and ip_address into raw_overlay_actions table.
{
action: "expand"
fan_id: "ID for the user"
overlay_log_uid: "[uid]"
account_id: "[account_id]"
}
Communication with campaign elements
Following are API calls, which allow to communicate various activities: clicker, quiz, etc. They all follow a similar pattern, so examples are grouped together
POST https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/clicker Push clicker counter into the server
GET https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/answer Get quiz question result for a given question
POST https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/answer Post Result on the question
GET https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/leaderboard Return public leaderboard
GET https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/event_stats Return public sport stats
GET https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/player_stats Return public sport stats for players
GET https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/chat_messages Get Last N messages for a given channel
POST https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/report_chat Report a chat message
POST https://nest.tradablebits.com/api/v1/fanxp/[page_tab_id]/points Push new points
# get leaderboard
data = {"session_uid":"28c02479-068d-4600-bcab-fc4fcfbc43af",api_key="28c02479-068d-4600-bcab-fc4fcfbc43af"}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/fanxp/12272/leaderboard",params=data)
result = res.json()
print("res",result)
Video Stream integration
Video stream is currently delivered in the form of HLS feed. Authenticated request to following endpoint will return either redirect or raw origin manifest file, which can and should be integrated into native player. Depending of the device you may want to include "prefetch" parameter to obtain origin manifest directly in the body of the response versus redirect
First, you can and should get a list of available video feeds
import requests
data = {"session_uid":"28c02479-068d-4600-bcab-fc4fcfbc43af","api_key":"28c02479-068d-4600-bcab-fc4fcfbc43af"}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/fanxp/12272/live_endpoints",params=data)
if res.status_code == 200:
result = res.json()
print("feeds",result)
else:
print(res.text)
print("Error")
Getting the manifest itself
import requests
data = {"session_uid":"28c02479-068d-4600-bcab-fc4fcfbc43af","prefetch":"true","video_endpoint_uid":"xxxxxx" }
res = requests.request("GET","https://nest.tradablebits.com/application/hls/12345",params=data)
if res.status_code == 200:
result = res.text
print("manifest",result)
else:
print(res.text)
print("Error")
Getting a list of video on demand files. In addition to live feeds fanxp can support a set of prerecorded videos hosted on vimeo. The file itself can be played with vimeo SDK using vimeo_key as a key.
import requests
data = {"session_uid":"28c02479-068d-4600-bcab-fc4fcfbc43af","api_key":"28c02479-068d-4600-bcab-fc4fcfbc43af",
"page":"waiting"}
res = requests.request("GET","https://nest.tradablebits.com/api/v1/fanxp/12272/vod",params=data)
if res.status_code == 200:
result = res.json()
print("feeds",result)
else:
print(res.text)
print("Error")