← Back to API docs

Embedding the live animation widget

A live match animation you can embed in a web page or an app: the stage shows a 2.5D animation or a 3D pitch, framed by a score bar and data tabs. Both views share the same layouts, so switching the view is a configuration change and the embed code stays as it is. The data is the platform's realtime animation stream: ball position, play situation and events are pushed as the match goes on.

Two views

ViewDescriptionGood for
2.5dA top-down animation in perspective: ball position, situation labels (dangerous attack / attack / possession), trail, attack zones, event cards. A few dozen KB, smooth on any phone.Home page cards, list pages, inside an app
3dA real pitch with 22 players moving with the live ball position, kits in each team's real colours, crowd, sound and camera cuts for goals, cards and corners. About 11 MB on first load (fast once the browser has cached it).Match detail pages, feature pages

Layouts and data modules

LayoutDescription
standardStandard: stage + score bar + data tabs
wideWide: stage on the left, data panel always visible on the right (stacks vertically below 560px)
compactCompact: stage and score bar only
mobileMobile portrait: 390 wide, tabs to switch
ModuleDescription
scorebarScore bar: crests, team names, score, match clock
sceneLabelSituation label: dangerous attack / attack / possession, with the side (2.5D only)
eventCardEvent card: goals, corners, cards, substitutions; a banner in the middle of the picture in 3D
trailTrail: the last 5 situation points (2.5D only)
zoneAttack zone shading (2.5D only)
statsStatistics: possession, attacks, dangerous attacks, shots, shots on target, corners, yellow and red cards
timelineEvent timeline: goals, cards, substitutions, corners
sceneSituation summary: dangerous attacks, attacks, possession and pressure index over the last 5 minutes
momentumMomentum: attacking pressure per minute (accumulated from when the widget was opened)
tliveText commentary, line by line (in Chinese)
lineupLineups: starters / substitutes, shirt numbers
ftPanelHalf-time / full-time panel: statistics laid over the stage at the break and at the end

Situation labels, the trail and attack zones exist in 2.5D only; 3D shows the pitch and the players, and those three modules switch off by themselves. In 3D the ball and the events are live data; player movement is simulated on the server from the ball position, the situation and the events, so every viewer sees the same picture, but it is not real tracking data. Modules and templates depend on your plan; what this endpoint returns is what you can use: /api/v1/widget/options

Integration steps

  1. Get access: ask sales to enable the animation widget and to register the domains allowed to embed it (for example *.example.com), the views you may use and the concurrent viewer limit. Once enabled, your API key can call /api/v1/widget/*.
  2. Configure: in the widget console choose the view, template, style, data modules and layout, with a live preview of a real match on the right; saving and publishing gives you a configuration id (starting with wgt_).
  3. Exchange a token: your server calls POST /api/v1/widget/tokenwith its API key, the configuration id and the origin of the embedding page, and gets a token valid for 2 hours by default. The API key stays on the server; never put it in a page or an app package.
  4. Embed: put an iframe in the page with the address /widget/anim?cfg=<configuration id>&match=<match id or auto>&token=<token>. match is the platform's match id; auto picks a popular match in play.
  5. Transport: the widget receives live data over SSE by default; add &transport=ws to the address for WebSocket (it falls back to SSE when that cannot connect). The content is the same either way; the player movement frames of the 3D view come with it, about 10 frames a second.

No server of your own? See App Key embedding below. For Flutter, iOS and Android apps, see SDKs and client libraries →

Code

① Exchange a token on your server

Terminal
curl -X POST https://api.sports.csap.ai/api/v1/widget/token \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{"config":"wgt_1a2b3c4d","origin":"https://www.example.com","ttl":7200}'
# → {"code":0,"data":{"token":"…","expires_at":1794240000,"embed_url":"/widget/anim?cfg=wgt_1a2b3c4d&token=…"}}

② Embed in the page (your server hands out the token)

HTML
<iframe id="live-anim" style="width:100%;height:640px;border:0" allow="autoplay; fullscreen"
  src="https://api.sports.csap.ai/widget/anim?cfg=wgt_1a2b3c4d&match=auto&token=<token>"></iframe>
<script>
  // the widget tells the page how tall it needs to be
  addEventListener('message', (e) => {
    const f = document.getElementById('live-anim');
    if (e.source === f.contentWindow && e.data && e.data.type === 'anim-widget-size') f.style.height = e.data.height + 'px';
  });
</script>

No server? Embed directly with an App Key New

In the console under “Live animation widget → App Keys”, create an App Key (it starts with pk_) and enter your website’s domains. It can only load the widget — it can’t call data endpoints — so it may sit in your page source. The page passes a match ID and it plays; no token-exchange server is needed. Create an App Key in the console →

① iframe: paste one snippet

HTML
<!-- The App Key may sit in your page source; it only plays on allowlisted domains -->
<iframe src="https://api.sports.csap.ai/widget/embed?app_key=pk_3f9a…&match=4651805&view=3d"
        style="width:100%;aspect-ratio:16/9;border:0" allow="autoplay; fullscreen"></iframe>

② Script: sizes itself and turns goals / full time into page events

HTML
<!-- The script creates the iframe where it sits -->
<script src="https://api.sports.csap.ai/widget/embed.js" data-app-key="pk_3f9a…"
        data-match="4651805" data-view="2.5d" async></script>
<script>
  document.addEventListener('animwidget:goal', (e) => console.log('Goal', e.detail.score));
  document.addEventListener('animwidget:end', (e) => console.log('Full time', e.detail.score));
  document.addEventListener('animwidget:error', (e) => console.warn(e.detail.reason)); // why it could not play
</script>

Parameters

ParameterDescription
app_keyRequired. The App Key from the console (starts with pk_). In the script embed: data-app-key.
matchThe platform’s match ID. Omit it or pass auto and the platform picks a popular match in play (such a load can’t be pointed at another match afterwards). In the script embed: data-match.
viewOptional: 2.5d or 3d. Defaults to the view in the configuration; only views your plan includes can be chosen.
langOptional: zh or en. Defaults to the language in the configuration.
exp / sigOnly in signed mode: the expiry (Unix seconds) and the signature, see below.
data-heightScript embed only: a fixed height in pixels. Without it the height follows the widget’s content.

Page events of the script embed (listen on document; data is in e.detail)

animwidget:readyThe widget connected to the match: match_id, home, away
animwidget:goalA goal: side (1 home / 2 away), score, minute
animwidget:endFull time: score
animwidget:errorIt could not play: reason is one of the codes under “Refusal reasons” below

How an App Key stays safe

Domain allowlistRequired, and must lie within the widget domains the platform enabled for you. Enter host names only (letters, digits, hyphens and dots; *.example.com is allowed). The server checks the embedding page’s Origin / Referer, and the widget page sends Content-Security-Policy: frame-ancestors, so another site can neither frame it nor obtain a token. Don’t set referrerpolicy="no-referrer" on the iframe — the source can’t be verified then and the load is refused.
Short-lived tokenEvery load gets a token bound to your domain and valid for 2 hours, renewed automatically. The App Key itself can’t call any data endpoint. The token works only from the widget page itself: using it from another site’s page is refused.
Rate limits and capsRate-limited per key and per visitor IP. You can set a daily view cap and a concurrent cap; beyond them the widget area shows an “unavailable” notice. One view is one visitor opening the page once (reconnecting doesn’t count again); the same token played from another address counts as another view.
Disable / rotateAfter disabling, new loads are refused at once and pages that are playing stop within seconds. After rotating, the old key stops working at once. The same happens within seconds when your organization is suspended or the widget is turned off. A key the platform disabled is view-only: you can’t enable, edit, rotate or delete it, nor create another key for the same site — contact sales to restore it.
Signed mode (optional)When on, the page must carry a server signature: your server computes the hex HMAC-SHA256(secret, "matchID:expiry") with the App Secret and the page adds the exp and sig parameters. Change the match or the expiry and the signature no longer matches; the expiry may be at most 7 days ahead. The App Secret is shown once — on creation, rotation or regeneration — and belongs on your server only.

③ Signed mode: sign on your server (Node.js example)

Node.js
const crypto = require('crypto');
const secret = process.env.WIDGET_APP_SECRET;        // keep the App Secret on your server
const match = '4651805';
const exp = Math.floor(Date.now() / 1000) + 3600;    // expiry (Unix seconds), at most 7 days ahead
const sig = crypto.createHmac('sha256', secret).update(match + ':' + exp).digest('hex');
// write exp and sig into the page:
// https://api.sports.csap.ai/widget/embed?app_key=pk_3f9a…&match=${match}&exp=${exp}&sig=${sig}

Refusal reasons (the code on the “unavailable” notice, the X-Widget-Refused response header, and reason in animwidget:error)

unknownNo such App Key (mistyped, rotated or deleted)
domainThe embedding page’s domain isn’t on the allowlist, or the source can’t be verified; opening the embed address directly counts too
disabledThe App Key is disabled, the widget isn’t enabled, or the organization is suspended
quotaToday’s view cap has been reached
concurrentThe concurrent viewer cap has been reached
rateToo many loads (per key and per IP; one address is also limited across all keys)
signatureSigned mode: the signature is missing, wrong or expired
scopeThe match ID is invalid, or outside the range this key allows
configThe bound configuration isn’t published, or your plan doesn’t include that view

Messages to the page

The widget tells the embedding page through postMessage (sent for token embeds and App Key embeds alike; with the script embed, the script handles them for you and turns them into animwidget:* page events):

MessageDescription
anim-widget-sizeField height: the height of the widget's content in px. Sent whenever the layout or the tab changes; set the iframe's height to it and there is no scroll bar.
anim-widget-eventFields event and data. event is ready (attached to a match: match_id, home, away), goal (side is 1 for home or 2 for away, score, minute) or end (full time: score).
anim-widget-errorFields reason and height: the widget could not play or was stopped; reason is one of the refusal reasons listed above. Token embeds send it too: when the widget is turned off, the organization is suspended or the App Key is disabled / rotated while playing, the widget shows a notice in place of the stage and posts a message with reason disabled.

Security and limits

  • A token is bound to a configuration and an origin; the widget page may only be framed by that origin (response header Content-Security-Policy: frame-ancestors), so it does not work on another domain.
  • Tokens last 2 hours by default and 24 hours at most; when one expires the widget asks for a refresh, and the page simply asks your server for a new token.
  • When the number of concurrent viewers passes your limit, newly opened widgets get 429.
  • When the widget is turned off or the organization is suspended, both ways of embedding stop: new loads are refused, and pages that are playing stop within seconds and show a notice.
  • For a match without animation data (not started, finished, or no animation for this match) the widget does not wait in vain: the score bar shows the real score and the stage says "not started / finished / no animation data".
  • A configuration takes effect once published; after changing the view, template or layout, publish again and embedded pages show it after a reload.

Related endpoints

POST /api/v1/widget/tokenExchange an embed token (server side only)
GET /api/v1/widget/optionsViews, templates, layouts and modules available to your account
GET /api/v1/widget/configsConfigurations saved by your account
GET /api/v1/widget/statsViewers online now and views today
GET /api/v1/widget/app-keysApp Keys with today's views and refusals; create and update with POST on the same path