
Website Content Crawler
scrape.ly/store/querystone/website-content-crawlerAIDeveloper tools
1 monthly user

Collect public tennis matches, tournaments, point-by-point scores, and player statistics from SofaScore and Tennis Abstract.
Get live tennis matches, daily tournament lists, season and match tables, point-by-point scoring, and player profiles as clean structured data. It reads public SofaScore feeds and public Tennis Abstract profiles, with no account and no API key.
| Field | Type | What it does |
|---|---|---|
mode | string | liveMatches, tournaments, tournamentSeasons, tournamentDetails, pointByPoint, or playerDetails. |
maxResults | integer | Rows to save for lists. Default 10. 0 saves everything listed. |
date | string | YYYY-MM-DD for tournaments. Empty means today in UTC. |
matchType, gender, tournamentLevel | array | Filters for live matches. Empty includes everything. |
tournamentId | integer | For seasons or tournament matches. Wimbledon men's singles is 2361. |
seasonId | integer | Optional season for tournament matches. Empty uses the newest. |
matchId | integer | For point-by-point. Copy it from a live or tournament match row. |
playerId | integer | For player details. Jannik Sinner is 206570. |
proxyConfiguration | object | Residential proxy by default (the match feed blocks datacenter addresses). |
Tournaments on a date:
{ "mode": "tournaments", "maxResults": 10 }
Live matches:
{
"mode": "liveMatches",
"maxResults": 10,
"matchType": ["singles"],
"gender": ["male"],
"tournamentLevel": ["atp1000", "challenger"]
}
Other modes take ids from earlier rows:
{ "mode": "tournamentDetails", "tournamentId": 18508, "seasonId": 96709, "maxResults": 10 }
{ "mode": "pointByPoint", "matchId": 17304354 }
{ "mode": "playerDetails", "playerId": 206570 }
Each mode saves its own kind of row.
| Mode | Row |
|---|---|
tournaments | One row per tournament on that UTC date: name, tour, surface, ranking points, event count. |
liveMatches, tournamentDetails | One row per match: players, round, status, set score, current points, odds when available. |
tournamentSeasons | One row per season. |
pointByPoint | One row with pointByPoint and statistics. |
playerDetails | One row with profile, recent results, season tables, splits, and charting tables. |
Matches that have not started have firstToServe, winnerSide, and score set to null.
Real live match item (trimmed):
{
"id": 17304354,
"url": "https://www.sofascore.com/tennis/match/carlos-maria-zarate-gonzalo-villanueva/Cr",
"startTime": "2026-10-14T00:25:00Z",
"matchType": "singles",
"roundSlug": "round-of-32",
"surface": "Red clay",
"status": "2nd set",
"tournamentId": 18508,
"tournamentName": "Santa Cruz 2, Bolivia",
"firstToServe": "away",
"homePlayerId": 85877,
"homePlayerName": "Gonzalo Villanueva",
"awayPlayerId": 511702,
"awayPlayerName": "Carlos Maria Zarate",
"score": {
"home": 0,
"away": 1,
"sets": [
{ "set": 1, "home": 3, "away": 6, "durationSeconds": 2396 },
{ "set": 2, "home": 6, "away": 5 }
],
"homePoint": "30",
"awayPoint": "30"
},
"odds": {
"home": { "fractional": "8/13", "decimal": 1.62 },
"away": { "fractional": "6/5", "decimal": 2.2 }
}
}
Real tournament item:
{
"date": "2026-10-14",
"id": 2519,
"name": "Shanghai",
"url": "https://www.sofascore.com/tennis/tournament/atp/shanghai/2519",
"categoryName": "ATP",
"surface": "Hardcourt outdoor",
"tennisPoints": 1000,
"eventCount": 4
}
$0.10 per 1,000 rows, which is $0.0001 per saved row.
Worked example: 1,000 rows cost $0.10 in row fees. Platform usage is billed separately at standard rates, and residential proxy traffic ($2 per GB) is used by default. A 10-match live run at the default 256 MB costs about $0.002 in total, most of it row fees, compute and log writes.
Is it legal? The actor reads public feeds and pages only. You are responsible for using the data in line with the sources' terms and local law.
Do I need an account or my own proxy? No account. Residential proxy is selected by default because the match feed blocks datacenter addresses.
Why does a live run sometimes save nothing? The live feed can be empty between sessions. The run still succeeds. Use tournaments for a date when you want a list that does not depend on matches being in progress.
What happens on a bad id or date?
The run fails with a clear status message and saves no rows, so nothing is charged. The reason is also stored in the OUTPUT key-value record (failedInputs). A date like February 31, a missing tournament id, or an unknown player id is rejected.
Where do player statistics come from? The player is matched to the public Tennis Abstract profile by name. Recent results, seasons, splits, and charting tables are parsed from that page.
Can I schedule it or use the API? Yes. Use schedules in the console, or start runs and read the dataset through the API.
others.Every field below can be set in the run form, passed through the API, or saved as a task. Unset fields fall back to the defaults shown.
modeenumOptionaldefault "tournaments"Mode
Live matches, tournaments on a date, a tournament's seasons or matches, point-by-point for one match, or one player's statistics.
maxResultsintegerOptionaldefault 10Maximum results
How many rows to save for live matches, tournaments, and seasons. Use 0 to save everything the source lists. Tournament details stays one row and limits the matches inside it. A player or point-by-point run saves one row.
Live matches can be narrowed by type, gender, and level. Tournament discovery uses the date.
datestringOptionalDate
Used for tournaments on a date. A real calendar date in YYYY-MM-DD. Leave it empty to use today in UTC.
matchTypearrayOptionalMatch types
Used for live matches. Leave empty to include every format.
genderarrayOptionalGenders
Used for live matches. Leave empty to include every gender.
tournamentLevelarrayOptionalTournament levels
Used for live matches. Leave empty to include every level. Levels come from the live feed's tournament metadata.
Required for point-by-point, tournament seasons, tournament matches, and player statistics.
matchIdintegerOptionalMatch ID
SofaScore match id for point-by-point. Copy it from a live match or a tournament match.
tournamentIdintegerOptionalTournament ID
SofaScore tournament id for seasons or tournament matches. Wimbledon men's singles is 2361.
seasonIdintegerOptionalSeason ID
Optional season id for tournament matches. Leave it empty to use the newest season. Copy it from tournament seasons.
playerIdintegerOptionalPlayer ID
SofaScore player id for player statistics. Copy it from a match. Jannik Sinner is 206570.
proxyConfigurationobjectOptionaldefault {"useScrapelyProxy":true,"scrapelyProxyGroups":["RESIDENTIA…Proxy configuration
Residential proxies are required. The public match feed blocks datacenter addresses.
Every run writes its results to a dataset. Preview it in the dashboard, then export it or read it through the API.
| Field | Type | Description |
|---|---|---|
| date | string | |
| id | integer | |
| name | string | |
| slug | string | |
| url | string | |
| categoryName | string | |
| categorySlug | string | |
| surface | string | |
| tennisPoints | integer | |
| eventCount | integer | |
| matchType | string | |
| tournamentName | string | |
| homePlayerName | string | |
| awayPlayerName | string | |
| homeTeamName | string | |
| awayTeamName | string | |
| status | string | |
| startTime | string | |
| fullName | string | |
| country | string | |
| currentRank | integer | |
| eloRating | integer | |
| year | string | |
| tournamentId | integer | |
| matchId | integer |
You pay for the events the actor reports, such as each result or page it produces. Platform usage is included in the event prices.
| Event | Charged when | Price |
|---|---|---|
| Result | One saved tennis row. | $0.0001 |
Run this actor from your own code. Swap in your API token and the examples work as-is.
import { ScrapelyClient } from 'scrapely-client'
const client = new ScrapelyClient({ token: process.env.SCRAPELY_TOKEN })
// Start the actor and wait for the run to finish.
const run = await client.actor('querystone/tennis-scraper').call({
"mode": "tournaments",
"maxResults": 10,
"proxyConfiguration": {
"useScrapelyProxy": true,
"scrapelyProxyGroups": [
"RESIDENTIAL"
]
}
})
// Read the dataset the run produced.
const { items } = await client.dataset(run.defaultDatasetId).listItems()
console.log(items)Tokens live in your account settings. Keep them server-side: a token carries the full permissions of your account.
Connect the Scrape.ly MCP server once and your agent can run this actor as a tool. Pick your tool:
Fetch scrape.ly/agents.md and run querystone/tennis-scraperPaste into Claude Code, Codex, Cursor or any AI agent. It signs you in once in the browser.
{
"mcpServers": {
"scrapely": {
"url": "https://api.scrape.ly/mcp?tools=actors,runs,storage,docs&actors=querystone/tennis-scraper"
}
}
}No key in the file: your client opens the browser to sign you in the first time it connects.
Inputs the developer prepared for common jobs. Open one to see its input, then copy it to your account to run it as-is or adjust it.
Tournaments and matches played on one date.
Past seasons of one tournament.
Details of a tournament in a given season.
Profile and ranking details for one player.
Scrape.ly gives you the infrastructure, the tooling, and the team to extract, process, and operationalize web data at any scale. Setup takes minutes. The advantage lasts forever.