SofaScore Tennis Data Stream avatar

SofaScore Tennis Data Stream

Pricing

from $0.01 / 1,000 snapshots

Go to Apify Store
SofaScore Tennis Data Stream

SofaScore Tennis Data Stream

Stream live tennis data from SofaScore over WebSocket, including current scores, accumulated point-by-point records, and match statistics.

Pricing

from $0.01 / 1,000 snapshots

Rating

0.0

(0)

Developer

Crawl Stone

Crawl Stone

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

4 hours ago

Last modified

Share

This is a Standby-only Actor designed to stream live tennis scores, accumulated point-by-point history, and real-time statistics to your application over a persistent WebSocket connection.

We built this stream to address the limitations of standard bulk-scraping or polling models when handling live sports data. Unlike traditional Actors that write scraping outputs to an Apify Dataset, this product has no standard input-to-run-to-Dataset workflow. It runs continuously as a Standby service, maintaining connection state in memory and serving real-time events straight to your connected clients.

Unofficial Actor: SofaScore Tennis Data Stream is an independent tool and is not affiliated with or endorsed by SofaScore.

Looking for historical data or standard exports? If you need scheduled tournaments, match details, player profiles, or standard flat files (JSON, CSV, Excel) rather than live streaming updates, use our companion product: Tennis Scraper.


Onboarding and Mental Model

The delivery model utilizes a simple connection and subscription flow:

  1. Connect: Establish a persistent WebSocket connection to the Standby endpoint with your Apify API token.
  2. Discover: Send a JSON command to retrieve the list of matches currently in progress (liveMatches).
  3. Subscribe: Send a subscription command (subscribe) with the unique ID of the match you want to follow.
  4. Consume: Receive combined snapshots containing full point progression and statistics. Snapshots are sent when fresh point-by-point or statistics data becomes available at the source, including a cached snapshot sent immediately after a successful subscription when available. Each snapshot replaces any previously received snapshot for that match.
  5. Clean Up: Remove subscriptions when matches finish, or close the connection when you're done.

Code Tutorials

Use these copy/paste-friendly, happy-path client examples to start streaming live data.

TypeScript Client

Install the pinned dependencies in your Node.js project:

npm install ws@8.21.1
npm install --save-dev tsx@4.23.1 typescript@7.0.2 @types/node@26.1.1 @types/ws@8.18.1

Save the code below as client.ts, replace YOUR_APIFY_TOKEN, and run it:

$npx tsx client.ts
import WebSocket from "ws";
const standbyUrl = "https://crawlstone--sofascore-tennis-data-stream.apify.actor";
const token = "YOUR_APIFY_TOKEN";
// Construct the WebSocket URL from the Standby HTTP URL
const websocketUrl = new URL(standbyUrl);
websocketUrl.protocol = websocketUrl.protocol === "http:" ? "ws:" : "wss:";
websocketUrl.pathname = `${websocketUrl.pathname.replace(/\/$/, "")}/v1/ws`;
console.log(`Connecting to ${websocketUrl.toString()}...`);
// Connect to the Standby WebSocket endpoint with Bearer header authentication
const socket = new WebSocket(websocketUrl.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
// Once connected, request the list of currently live matches
socket.on("open", () => {
console.log("Connected! Requesting live matches...");
socket.send(JSON.stringify({ type: "liveMatches" }));
});
// Handle incoming messages from the stream
socket.on("message", (rawData) => {
const message = JSON.parse(rawData.toString());
switch (message.type) {
case "liveMatches": {
const matches = message.matches || [];
console.log(`Found ${matches.length} live matches.`);
if (matches.length === 0) {
console.log("No live matches available right now. Closing connection.");
socket.close(1000, "No live matches");
return;
}
// Select a simple target: the first currently live match
const targetMatch = matches[0];
const matchId = targetMatch.id;
const homeName = targetMatch.homePlayerName || targetMatch.homeTeamName || "Home Team";
const awayName = targetMatch.awayPlayerName || targetMatch.awayTeamName || "Away Team";
console.log(`Subscribing to: ${homeName} vs ${awayName} (ID: ${matchId})`);
// Subscribe to receive real-time data for this match
socket.send(JSON.stringify({ type: "subscribe", matchId }));
break;
}
case "subscribed":
console.log(`Successfully subscribed to match ${message.matchId}`);
break;
case "snapshot":
// A snapshot contains the full point-by-point and statistics data currently available.
// Important: Each snapshot replaces the previous snapshot; it is not a delta.
console.log(`\n--- Received Snapshot for Match ${message.matchId} ---`);
console.log(JSON.stringify(message, null, 2));
break;
case "subscriptionEnded":
case "unsubscribed": {
const reason = message.reason || "requested";
console.log(`Subscription finished for match ${message.matchId} (Reason: ${reason})`);
// If our subscription ended (e.g., match finished), we can close the connection
console.log("Closing connection...");
socket.close(1000, "Subscription ended");
break;
}
case "error":
console.error(`Received stream error: ${message.code} - ${message.message}`);
console.log("Closing connection due to error...");
socket.close(1000, "Error received");
break;
default:
console.log(`Unhandled message type: ${message.type}`);
}
});
// Handle connection closure
socket.on("close", (code, reason) => {
console.log(`Connection closed (Code: ${code}, Reason: ${reason.toString() || "None"})`);
});
// Handle connection errors
socket.on("error", (error) => {
console.error(`WebSocket error: ${error.message}`);
});

Python Client

Install the pinned dependency in your Python environment:

$python -m pip install websockets==16.1.1

Save the code below as client.py, replace YOUR_APIFY_TOKEN, and run it:

$python client.py
import asyncio
import json
from urllib.parse import urlsplit, urlunsplit
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
STANDBY_URL = "https://crawlstone--sofascore-tennis-data-stream.apify.actor"
TOKEN = "YOUR_APIFY_TOKEN"
# Construct the WebSocket URL from the Standby HTTP URL
parts = urlsplit(STANDBY_URL)
scheme = "ws" if parts.scheme == "http" else "wss"
WEBSOCKET_URL = urlunsplit(
(scheme, parts.netloc, f"{parts.path.rstrip('/')}/v1/ws", parts.query, "")
)
async def main():
print(f"Connecting to {WEBSOCKET_URL}...")
# Connect to the Standby WebSocket endpoint with Bearer header authentication
try:
async with connect(
WEBSOCKET_URL,
additional_headers={"Authorization": f"Bearer {TOKEN}"},
max_size=None, # Do not limit incoming message size for large snapshots
) as websocket:
print("Connected! Requesting live matches...")
# Once connected, request the list of currently live matches
await websocket.send(json.dumps({"type": "liveMatches"}))
# Handle incoming messages from the stream
async for raw_message in websocket:
message = json.loads(raw_message)
message_type = message.get("type")
if message_type == "liveMatches":
matches = message.get("matches", [])
print(f"Found {len(matches)} live matches.")
if not matches:
print("No live matches available right now. Closing connection.")
break
# Select a simple target: the first currently live match
target_match = matches[0]
match_id = target_match["id"]
home_name = (
target_match.get("homePlayerName")
or target_match.get("homeTeamName")
or "Home Team"
)
away_name = (
target_match.get("awayPlayerName")
or target_match.get("awayTeamName")
or "Away Team"
)
print(f"Subscribing to: {home_name} vs {away_name} (ID: {match_id})")
# Subscribe to receive real-time data for this match
await websocket.send(
json.dumps({"type": "subscribe", "matchId": match_id})
)
elif message_type == "subscribed":
print(f"Successfully subscribed to match {message.get('matchId')}")
elif message_type == "snapshot":
# A snapshot contains the full point-by-point and statistics data currently available.
# Important: Each snapshot replaces the previous snapshot; it is not a delta.
print(
f"\n--- Received Snapshot for Match {message.get('matchId')} ---"
)
print(json.dumps(message, indent=2))
elif message_type in {"subscriptionEnded", "unsubscribed"}:
reason = message.get("reason", "requested")
print(
f"Subscription finished for match {message.get('matchId')} "
f"(Reason: {reason})"
)
# If our subscription ended (e.g., match finished), we can close the connection
print("Closing connection...")
break
elif message_type == "error":
print(
f"Received stream error: {message.get('code')} - {message.get('message')}"
)
print("Closing connection due to error...")
break
else:
print(f"Unhandled message type: {message_type}")
except ConnectionClosed as error:
print(f"Connection closed ({error.code}: {error.reason})")
except Exception as error:
print(f"WebSocket connection failed: {error}")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass

Connection and Service Endpoints

Deployed Standby routing requires a valid Apify API token for HTTP service requests and WebSocket handshakes. Bearer headers are recommended for server clients; restricted query tokens are the browser fallback.

  • Readiness Check (Optional): https://crawlstone--sofascore-tennis-data-stream.apify.actor/ An optional service-health check. Requesting with GET and the X-Apify-Container-Server-Readiness-Probe: 1 header returns HTTP 200 with the text ready to confirm the Standby server is active. This confirms service availability, not match discovery.
  • WebSocket Endpoint: wss://crawlstone--sofascore-tennis-data-stream.apify.actor/v1/ws Exposes the persistent connection for live commands and streaming snapshots.

Scannable Protocol Reference

1. Commands (JSON text frames)

  • List Live Matches: {"type":"liveMatches"} Returns all matches currently active in the feed with their live score states.
  • Subscribe: {"type":"subscribe","matchId":12345678} Starts streaming snapshots for the target match ID.
  • Unsubscribe: {"type":"unsubscribe","matchId":12345678} Stops the streaming snapshot feed for that match ID.

2. Message Types

  • liveMatches: Contains a list of active matches and scores.
  • subscribed / unsubscribed: Explicit command acknowledgements.
  • snapshot: Combined point progression (pointByPoint) and period statistics (statistics).
  • subscriptionEnded with reason: "matchEnded": Sent when a match leaves the live feed.
  • unsubscribed with reason: "sourceErrors": Sent if the match polling worker registers three consecutive source failures.
  • error: Sent when an operation fails (e.g., subscribing to an invalid or concluded match ID). Common codes: INVALID_MESSAGE, INVALID_COMMAND, UNKNOWN_COMMAND, INVALID_MATCH_ID, MATCH_NOT_LIVE, SUBSCRIPTION_LIMIT, SOURCE_UNAVAILABLE.

Production Integration Checklist

When moving from our single-target happy-path tutorials to production-grade integrations, ensure your client design accounts for the following server limits and lifecycle behaviors:

  • Connection Recovery Strategy: Subscriptions are maintained in server memory and belong strictly to your active WebSocket connection. If the connection closes, implement exponential backoff, request liveMatches again, and resubscribe to desired match IDs that remain active.
  • Zero-Subscription Grace Period: The server closes any connection that maintains zero active subscriptions for 60 seconds (either upon initial connection readiness or after removing/ending the final subscription). Expiry closes the socket immediately with status code 1008 (Policy Violation) and reason "subscription required".
  • Snapshot Replacement Semantics: Each incoming snapshot is a full replacement of the match state, not a delta. Overwrite your local storage or memory state for the matching ID completely.
  • Command & Connection Limits: The connection supports at most 200 active subscriptions. Inbound messages must be text frames and are capped at 16 KiB; larger commands or binary frames result in immediate connection closure.
  • Slow-Consumer Backpressure: Clients must consume messages promptly. If your client cannot keep up with updates, the server terminates the connection with status 1013 (Try Again Later) and reason "client cannot keep up". Offload expensive database or AI tasks to workers or queues.
  • Browser Token Restrictions: Browser clients cannot set Bearer handshake headers and must authenticate using query parameters (?token=...). Since this exposes the token, use a narrowly scoped restricted token or run a server-side relay to mask your primary secrets.
  • Billing Fan-Out: You pay for snapshot writes. Subscribing to the same match from five separate clients results in five distinct fanned-out writes. Use a single central client to ingest the stream, then distribute updates within your internal network to avoid unnecessary event charges.

Pricing

The service runs on a dual billing structure split between event charges and platform resources:

  1. Delivered Snapshots (Pay-Per-Event): You are billed for successful snapshot writes. One snapshot-delivered event is charged for each merged snapshot successfully written to a WebSocket client. Connection handshakes, liveMatches queries, command acknowledgements, protocol errors, close frames, and failed/unwritten frames are not charged.
  2. Separate caller-paid platform usage: Callers separately pay for all underlying compute, memory, data transfer, and automatic residential proxy routing consumed while serving your Standby connection. The Actor routes requests to SofaScore through proxies automatically; no user-side proxy setup is needed.

Review current pricing rates and account usage ceilings in the Actor's Pricing tab and your Apify console billing panel.


Frequently Asked Questions

Why did I receive an empty live matches list?

No matches are currently available in the live feed (common between tournaments or during schedule gaps). Do not poll the readiness endpoint to search for matches; readiness remains only a service-health signal. Since Standby terminates idle connections after 60 seconds, close the connection and request the list again in a later connection.

Why is point-by-point or statistics data empty for some matches?

Coverage varies based on tournament tier. The Actor preserves empty lists when the source does not publish tracking data rather than injecting guessed values.

Can I retrieve past tournaments or completed match data?

No. This is a live stream. For completed tournaments, historical statistics, player profile splits, and flat files, use Tennis Scraper.


Support

If you need assistance, please open a ticket on this Actor's Discussion tab and include:

  • Approximate UTC timestamp of the issue.
  • Client language and WebSocket library version.
  • The WebSocket close code and close reason, if applicable.
  • The command or match ID involved.
  • A clear description of what you expected versus what occurred.

Never share your primary Apify Token, credentials, or authenticated URLs in public support requests.