Developers

Our platform was built to be flexible and intuitive for developers like you. You own all the data you collect with Tradable Bits. Our API and SDK are designed to make it easy to access your fan data, tickets and campaigns at any time, on your terms. Customize the front end of any of your campaigns with the help of our documentation, complete with examples. Our RESTful API helps you access, send or receive fan data from our system to your endpoints quickly and securely. Enrich your Fan CRM and Tickets Analytics with data from other CRM systems through our many integrations. Have any questions about our developers platform? Our support team is happy to help.

FANXP Integration into Native/Mobile Application

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, 2021

Authentication 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")