← Back to API docs

SDKs and client libraries

Clients in five languages. Each wraps the REST calls, the realtime subscription (ticket exchange, automatic reconnect, catch-up and de-duplication after a reconnect) and typed messages; Flutter, iOS and Android also get a native component for the live animation. Message formats and the reconnect and de-duplication rules are the same in every SDK.

Available SDKs

SDKWhat it coversRuns onSource directory
JavaScript / TypeScriptREST client, realtime subscription (WebSocket), MQTT helpers, type definitions; for Node and browsers, no runtime dependenciesNode 22+ (fetch and WebSocket built in); browserssdk/js
PythonREST client (sync / asyncio), realtime subscription (asyncio / sync callbacks), MQTT helpers; no runtime dependenciesPython 3.9+sdk/python
DartREST client, realtime subscription, typed messages; pure Dart, for servers, command-line tools and Flutter (including web)Dart 3.5+sdk/dart/opendata
FlutterMatch animation widget: token mode or App Key mode, renews its token, disconnects in the background, goal and full-time callbacksFlutter 3.27+, iOS / Androidsdk/dart/opendata_anim
SwiftOpenData: REST client and realtime subscription; OpenDataAnim: animation component (SwiftUI / UIKit), token mode or App Key modeiOS 15+ / macOS 12+; the animation component is iOS onlysdk/swift
Kotlinopendata: REST client and realtime subscription; opendata-anim: Android animation view, token mode or App Key modeJVM 11+ / Android (minSdk 24)sdk/kotlin

Key rules (read this first)

  1. The data API key exists only on your server. Never put it in a web page, a mini program or an app package, and never send it to the front end: anything in front-end code can be extracted.
  2. Your server exchanges the API key for two short-lived credentials: a one-time realtime ticket (POST /api/v1/realtime/ticket, valid for 60 seconds, single use) and a widget token (POST /api/v1/widget/token, bound to a configuration and the page's domain).
  3. SDK code in a browser or an app receives only the ticket or the token: you give the SDK a callback (ticket provider / token provider), the SDK calls it for every connection and every renewal, and the callback asks your server.
  4. The only credential that may ship in front-end code is the public App Key (starts with pk_): it only loads the animation widget, cannot call any data endpoint, and is protected by the domain allow-list, usage limits and optional signing.
Where the code runsCredentialHow to get it
Your serverAPI keyThe X-API-Key header; pass it when you construct the SDK's REST client. A client that holds the key belongs on servers, command-line tools and internal tools only
Realtime subscription in a browser or an appRealtime ticket (valid for 60 seconds, single use)Your server calls POST /api/v1/realtime/ticket and hands the result to the front end; every (re)connect needs a new one
Live animation in a web page or an appWidget token, or an App Key (starts with pk_)Token: your server calls POST /api/v1/widget/token. App Key: create it in the console; it only loads the animation and may appear in a page or an app

Download and install

The SDKs are delivered as source zips. Sign in to the console and download them under "SDK downloads" (one zip per SDK, with a SHA-256 checksum); they are not published to npm, PyPI, pub.dev, Maven Central or the Swift Package Index. Every zip unpacks to sdk/<directory>/ plus sdk/contract/. After unzipping, add the SDK to your project from the local directory as shown below. Download in the console →

unzip opendata-sdk-js-<version>.zip          # → sdk/js, sdk/contract

# build the SDK once (the zip is source only, without dist/)
cd sdk/js && npm install && npm run build

# then, in your project, install it from that directory
npm install /path/to/sdk/js

# import name (the name in its package.json):
#   import { RestClient, RealtimeClient } from '@openplatform/football-sdk'

On your server: list the matches in play, then subscribe to them

These two run on your server, with the API key read from the environment: fetch the matches in play over REST, then subscribe to their scores and events. The key needs realtime push enabled.

import { RestClient, RealtimeClient } from '@openplatform/football-sdk'

const rest = new RestClient({ baseUrl: 'https://api.sports.csap.ai', apiKey: process.env.OPB_API_KEY })

const live = await rest.live({ limit: 50 }) // matches in play
console.log(live.length, 'live matches')

const rt = new RealtimeClient({
  rest, // exchanges the key for a one-time ticket on every (re)connect
  subscriptions: [{ channels: ['match', 'incident'], matches: live.slice(0, 5).map((r) => r.id) }],
})
rt.on('match', (m, meta) => console.log(m.match_id, m.home_scores[0], m.away_scores[0], meta.snapshot ? '(snapshot)' : ''))
rt.on('incident', (e, meta) => console.log(e.match_id, e.incident_type, e.minute, meta.revised ? '(revised)' : ''))
rt.on('gap', (g) => console.log('offline', g.since, '→', g.until)) // after each reconnect
rt.on('fatal', (err) => console.error('will not reconnect:', err.message))
await rt.connect()

Browsers and apps: a ticket only, never the key

There is no API key in front-end code. Add a ticket endpoint to your server first (first block); the SDK in the front end calls it through a callback. A ticket works once, so the callback runs on every (re)connect. The JS SDK refuses to construct a client with an API key inside a browser.

Your server: the ticket endpoint (Node example)

Node.js
// the API key stays here, in an environment variable of your server
import express from 'express'
import { RestClient } from '@openplatform/football-sdk'

const rest = new RestClient({ baseUrl: 'https://api.sports.csap.ai', apiKey: process.env.OPB_API_KEY })
const app = express()
// protect this route with your own login: whoever can call it can watch the realtime feed
app.post('/opendata/ticket', async (_req, res) => res.json(await rest.realtimeTicket()))   // { ticket, expires_in, ws_url }
app.listen(3000)

Front end: get the ticket through a callback

import { RealtimeClient } from '@openplatform/football-sdk'

// no API key in the page: the callback asks your server for a ticket
const rt = new RealtimeClient({
  baseUrl: 'https://api.sports.csap.ai',
  getTicket: () => fetch('/opendata/ticket', { method: 'POST' }).then((r) => r.json()),
  subscriptions: [{ channels: ['match'], matches: ['900001'] }],
})
rt.on('match', (m) => console.log(m.match_id, m.home_scores[0], m.away_scores[0]))
await rt.connect()

Disconnects, reconnects and catching up

  • The server keeps no message sequence and replays nothing that was sent while you were offline. The SDK reconnects by itself (1 second, doubling, 30 seconds at most, with random jitter), gets a new ticket for every reconnect and re-sends all subscriptions once connected.
  • After a reconnect the server pushes the latest state again for your subscriptions (snapshot: true): matches (with the whole event list and the statistics), animation and odds. Whatever changed in the score and the events while you were offline is in that snapshot.
  • The SDK drops only repeats whose content is identical: for matches, an older version is dropped and an equal version is dropped only when the content is the same; for events, an exact repeat is dropped and a message with the same dkey but different content is delivered flagged revised (it may complete the earlier event, or be another event for the same side in the same minute; you decide by the content); a ball position is dropped only when it equals the previous frame. A match that did not change while you were offline does not fire a callback again.
  • After every reconnect the SDK emits a gap notice (when the connection was lost and when it came back). Single events from that period are not re-sent; if you need them one by one, reconcile against the whole event list in the snapshot.
  • The SDK stops reconnecting and reports a fatal error when the ticket exchange returns 401 / 403 (key invalid or disabled, no realtime push, IP not on the allow-list), when the daily quota is used up on the ticket exchange, when the WebSocket upgrade is refused with 403 (in the SDKs that can see the status: Python, Dart, Kotlin, Swift), when the server closes with code 1008 (realtime access withdrawn), or when you close the connection. Every other failure is retried with backoff.
  • The SDK takes care of the heartbeat. If you write your own client: with two or more subscription rules on a connection, do not re-send set as a heartbeat, because it replaces all rules with that one. Subscription messages have a few more pitfalls (what unsubscribe really removes, misspelt channel names, the 8192-byte limit); the SDKs stop the obvious mistakes locally.

Protocol details and the rules for your own client are in the realtime guide →

Error handling

HTTPCaseWhat the SDK does
401Missing, invalid or disabled keyThrows an authentication error, no retry
403Endpoint not in the plan, client IP not on the key's allow-list, realtime push not enabledThrows a permission error, no retry
404Unknown path or no such dataThrows, no retry
429Per-minute rate limit exceeded (message: rate limit exceeded)Waits for the start of the next clock minute and tries again (the server counts per clock minute and sends no Retry-After), twice by default; if it is still 429 the error is handed to you
429 Daily quota used up (message: daily quota exceeded)Throws, no retry (the quota comes back the next day)
502 / 503 / 504Gateway or service temporarily unavailableRetries with exponential backoff, twice at most
500Server errorThrows, no retry
—Network failure or timeoutRetries GET requests only

Native components for the live animation

Flutter, iOS and Android each have a component that puts the hosted animation page in a WebView and handles getting and renewing the token, disconnecting in the background, goal and full-time callbacks and automatic height.

import 'package:flutter/widgets.dart';
import 'package:opendata_anim/opendata_anim.dart';

// Token mode: your server exchanges its API key for a widget token (POST /api/v1/widget/token)
Widget tokenMode(Future<Map<String, Object?>> Function(Map<String, Object?> body) askMyServer) {
  return OpenDataAnim(
    baseUrl: Uri.parse('https://api.sports.csap.ai'),
    match: '900001',             // match id; 'auto' picks a match in play; 'replay:demo' plays a recording
    view: AnimView.flat25d,      // or AnimView.stadium3d
    tokenProvider: (req) async => AnimToken.fromJson(await askMyServer(req.toJson())),
    onEvent: (e) {
      if (e.type == AnimEventType.goal && !e.historical) debugPrint('goal ${e.side} ${e.score}');
      if (e.type == AnimEventType.fullTime) debugPrint('full time ${e.score}');
    },
  );
}

// App Key mode: no server of your own; the key is public and only loads the animation
Widget appKeyMode() {
  return OpenDataAnim.appKey(
    baseUrl: Uri.parse('https://api.sports.csap.ai'),
    appKey: 'pk_xxxxxxxx',                              // Console → Animation widget → App Key
    embedOrigin: Uri.parse('https://app.example.com'),  // its host must be on the key's domain allow-list
    match: '900001',
    onError: (e) {
      if (e is AnimRefused) debugPrint('refused: ${e.reason}');   // domain / disabled / quota …
    },
  );
}
  • Token mode (all three): the component asks a callback of yours for a widget token; your server gets it with its API key from POST /api/v1/widget/token.
  • App Key mode (all three): no server of your own; the component loads the animation with the public App Key and reports the reason when it is refused (domain, disabled, quota and so on).
  • An app has no web address, so App Key mode takes an embedOrigin. Its host must be on the App Key's domain allow-list, and its scheme must match the platform address (https in production). In an app the origin is declared by the app itself, so the allow-list cannot stop someone from copying the key into another app; if that matters, turn on signed mode or use token mode.
  • The view (2.5D / 3D), template and layout come from the configuration published in the console.

Views, layouts, App Keys and refusal reasons are in the animation widget guide →