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
| View | Description | Good for |
|---|---|---|
| 2.5d | A 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 |
| 3d | A 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
| Layout | Description |
|---|---|
| standard | Standard: stage + score bar + data tabs |
| wide | Wide: stage on the left, data panel always visible on the right (stacks vertically below 560px) |
| compact | Compact: stage and score bar only |
| mobile | Mobile portrait: 390 wide, tabs to switch |
| Module | Description |
|---|---|
| scorebar | Score bar: crests, team names, score, match clock |
| sceneLabel | Situation label: dangerous attack / attack / possession, with the side (2.5D only) |
| eventCard | Event card: goals, corners, cards, substitutions; a banner in the middle of the picture in 3D |
| trail | Trail: the last 5 situation points (2.5D only) |
| zone | Attack zone shading (2.5D only) |
| stats | Statistics: possession, attacks, dangerous attacks, shots, shots on target, corners, yellow and red cards |
| timeline | Event timeline: goals, cards, substitutions, corners |
| scene | Situation summary: dangerous attacks, attacks, possession and pressure index over the last 5 minutes |
| momentum | Momentum: attacking pressure per minute (accumulated from when the widget was opened) |
| tlive | Text commentary, line by line (in Chinese) |
| lineup | Lineups: starters / substitutes, shirt numbers |
| ftPanel | Half-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
- 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/*. - 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_). - 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.
- Embed: put an iframe in the page with the address
/widget/anim?cfg=<configuration id>&match=<match id or auto>&token=<token>.matchis the platform's match id;autopicks a popular match in play. - Transport: the widget receives live data over SSE by default; add
&transport=wsto 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
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)
<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
<!-- 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
<!-- 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
| Parameter | Description |
|---|---|
| app_key | Required. The App Key from the console (starts with pk_). In the script embed: data-app-key. |
| match | The 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. |
| view | Optional: 2.5d or 3d. Defaults to the view in the configuration; only views your plan includes can be chosen. |
| lang | Optional: zh or en. Defaults to the language in the configuration. |
| exp / sig | Only in signed mode: the expiry (Unix seconds) and the signature, see below. |
| data-height | Script 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:ready | The widget connected to the match: match_id, home, away |
| animwidget:goal | A goal: side (1 home / 2 away), score, minute |
| animwidget:end | Full time: score |
| animwidget:error | It could not play: reason is one of the codes under “Refusal reasons” below |
How an App Key stays safe
| Domain allowlist | Required, 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 token | Every 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 caps | Rate-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 / rotate | After 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)
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)
| unknown | No such App Key (mistyped, rotated or deleted) |
| domain | The embedding page’s domain isn’t on the allowlist, or the source can’t be verified; opening the embed address directly counts too |
| disabled | The App Key is disabled, the widget isn’t enabled, or the organization is suspended |
| quota | Today’s view cap has been reached |
| concurrent | The concurrent viewer cap has been reached |
| rate | Too many loads (per key and per IP; one address is also limited across all keys) |
| signature | Signed mode: the signature is missing, wrong or expired |
| scope | The match ID is invalid, or outside the range this key allows |
| config | The 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):
| Message | Description |
|---|---|
| anim-widget-size | Field 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-event | Fields 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-error | Fields 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/token | Exchange an embed token (server side only) |
GET /api/v1/widget/options | Views, templates, layouts and modules available to your account |
GET /api/v1/widget/configs | Configurations saved by your account |
GET /api/v1/widget/stats | Viewers online now and views today |
GET /api/v1/widget/app-keys | App Keys with today's views and refusals; create and update with POST on the same path |