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 February 4, 2021Authentication Flow
Authentication allows you to obtain session_uid, which is used for all following calls. First step is to draw some configuration information to help with visuals.
import requests
email = "test@example.com"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c"}
res = requests.request("POST","https://tradablebits.com/api/v1/status",data=data)
if res.status_code == 200:
result = res.json()
legal_terms = result.get("legal_terms")
legal_privacy = result.get("legal_privacy")
header_text = result.get("header_text")
image_url = result.get("image_url")
else:
print("Server Error")
Once the form is displayed, the next step is to check for email validity and define whether login or registration flow must be used. Keep in mind, this call is throttled to restrict crawling.
import requests
email = "test@example.com"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","network":"email","email":email}
res = requests.request("POST","https://tradablebits.com/api/v1/sessions/connect",data=data)
if res.status_code == 200:
result = res.json()
if result["status"] == "register":
print("Register")
elif result["status"] == "login":
print("Fan found, now must log in.")
has_phone = result.get("phone",False)
has_password = result.get("password",False)
terms_required = result.get("terms_required",False)
else:
print("Server Error")
If email is not known to the system, registration flow must be used.
import requests
email = "test@example.com"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","network":"register","email":email,"first_name":"Test","last_name":"Test"}
res = requests.request("POST","https://tradablebits.com/api/v1/sessions/connect",data=data)
if res.status_code == 200:
result = res.json()
print("Fan created, now must log in.")
has_phone = result.get("phone",False)
has_password = result.get("password",False)
terms_required = result.get("terms_required",False)
else:
print("Error", res.text)
Once you know that the email is legit, you can choose to log in via password (if configured), email verification, or SMS verification (if the fan has a phone number stored). Code below covers password login.
import requests
email = "test@example.com"
password = "password123"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","network":"email","email":email,"password":password}
res = requests.request("POST","https://tradablebits.com/api/v1/sessions/connect",data=data)
if res.status_code == 200:
result = res.json()
session_uid = result["session_uid"]
fan_id = result["fan_id"]
print("Valid Login")
else:
print("Invalid Login/Pass")
In the case of invalid password or nonexistent password, you can prompt to send email or sms with verification code
import requests
email = "test@example.com"
# set network to verify_sms for SMS notification, verify_email for email notification
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","network":"verify_email","email":email}
res = requests.request("POST","https://tradablebits.com/api/v1/sessions/connect",data=data)
if res.status_code == 200:
result = res.json()
status = result["status"]
message = result["message"]
request_uid = result["request_uid"]
print("Request", request_uid, message)
else:
print("Error",res.text)
Once user gets email/sms with code, login can be completed via following call
import requests
email = "test@example.com"
request_uid = "36e26cf6-432c-4197-8788-fd1dac98e5f3"
reset_code = "49270"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","network":"submit_verification_code","email":email,"verification_code":verification_code,"request_uid":request_uid}
res = requests.request("POST","https://tradablebits.com/api/v1/sessions/connect",data=data)
if res.status_code == 200:
result = res.json()
session_uid = result["session_uid"]
fan_id = result["fan_id"]
print("Login Success", fan_id, session_uid)
else:
print("Error", res.text)
Once the session is retrieved, you may prompt the fan to create or update their password.
import requests
email = "test@example.com"
data = {"api_key":"b0dbe529-19a6-4323-8a58-8a95ffcbfb1c","password":"newPassword"}
session_uid = "abcdefij-1111-2222-3333-444455556666"
res = requests.request("POST",f"https://tradablebits.com/api/v1/sessions/{session_uid}/fan",data=data)
if res.status_code == 200:
result = res.json()
print(result)
else:
print("Error", res.text)
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://tradablebits.com/fb_media/11111111-1111-1111-11111111111111
import requests
data = {"api_key":"9eb875ac-6baf-4ee9-b189-afc725cba7f6"}
res = requests.request("GET","https://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://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://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://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_id": 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://tradablebits.com/api/v1/fanxp/[page_tab_id]/clicker Push clicker counter into the server
GET https://tradablebits.com/api/v1/fanxp/[page_tab_id]/answer Get quiz question result for a given question
POST https://tradablebits.com/api/v1/fanxp/[page_tab_id]/answer Post Result on the question
GET https://tradablebits.com/api/v1/fanxp/[page_tab_id]/leaderboard Return public leaderboard
GET https://tradablebits.com/api/v1/fanxp/[page_tab_id]/event_stats Return public sport stats
GET https://tradablebits.com/api/v1/fanxp/[page_tab_id]/player_stats Return public sport stats for players
GET https://tradablebits.com/api/v1/fanxp/[page_tab_id]/chat_messages Get Last N messages for a given channel
POST https://tradablebits.com/api/v1/fanxp/[page_tab_id]/report_chat Report a chat message
POST https://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://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://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://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://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")