Play-by-play API
Plays for one game in chronological order: text, type, period, clock, score after the play, scoring flags, down-and-distance for football and the athletes involved. Paginated; limit up to 1000 returns a whole game in one call. Data is aggregated from public sources and is typically 20-30 seconds behind live play (about 1 second after our source). There is no SLA.
- Soccer plays are match events (passes, shots, fouls, cards, substitutions), so a full match can have more than 1,000 entries.
- The penalties endpoint scans every play and returns only penalty plays.
Paths and parameters
GET/api/v1/sports/{sport}/leagues/{league}/events/{eventId}/plays
Get play-by-play. Plays in chronological order, paginated. Use `limit` up to 1000 to fetch a whole game in one call.
sport(path, required): Sport slug: football, basketball, baseball, hockey, soccer.league(path, required): League slug, e.g. nfl, college-football, nba, mens-college-basketball, mlb, nhl, eng.1, usa.1.eventId(path, required): Event (game) id, as returned by the events endpoints.limit(query): Maximum plays to return (default 25, max 1000).page(query): Page number (1-based).enrich(query): Resolve athlete names, teams and positions for play participants (default true). Pass false for faster, id-only results.
GET/api/v1/sports/{sport}/leagues/{league}/events/{eventId}/penalties
Get penalty plays. Only the penalty plays of an event; all plays are scanned, not just one page.
sport(path, required): Sport slug: football, basketball, baseball, hockey, soccer.league(path, required): League slug, e.g. nfl, college-football, nba, mens-college-basketball, mlb, nhl, eng.1, usa.1.eventId(path, required): Event (game) id, as returned by the events endpoints.enrich(query): Resolve athlete names, teams and positions for play participants (default true). Pass false for faster, id-only results.
Live example
Request
curl -H "Authorization: Bearer $RSA_API_KEY" \
"https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/401872965/plays?limit=3"Response captured from live data at 2026-10-05 02:10 UTC (refreshed every few minutes). Long arrays are trimmed; meta.rateLimit values are illustrative for a Free key.
Response
{
"success": true,
"data": [
{
"id": "4018729651",
"sequenceNumber": "100",
"text": "GAME",
"shortText": "Game",
"alternativeText": "GAME",
"type": {
"id": "70",
"text": "Coin Toss"
},
"period": 1,
"clock": {
"value": 900,
"displayValue": "15:00"
},
"homeScore": 0,
"awayScore": 0,
"scoringPlay": false,
"scoreValue": 0,
"situation": {
"down": 0,
"distance": 0,
"yardLine": 0,
"yardsToEndzone": 0
},
"endSituation": {
"down": 0,
"distance": 0,
"yardLine": 0,
"yardsToEndzone": 65
},
"yardsGained": 0,
"athletes": [],
"priority": false,
"modified": "2026-10-04T16:46Z"
},
{
"id": "40187296539",
"sequenceNumber": "3900",
"text": "D.Stevens kicks 61 yards from WAS 35 to IND 4. S.McGowan to IND 32 for 28 yards (D.Brown; P.Butler).",
"shortText": "Drew Stevens 61 Yd Kickoff Seth McGowan 28 Yd Kickoff Return",
"alternativeText": "D.Stevens kicks 61 yards from WAS 35 to IND 4. S.McGowan to IND 32 for 28 yards (D.Brown; P.Butler).",
"type": {
"id": "53",
"text": "Kickoff",
"abbreviation": "K"
},
"period": 1,
"clock": {
"value": 900,
"displayValue": "15:00"
},
"wallClock": "2026-10-04T13:32:18Z",
"homeScore": 0,
"awayScore": 0,
"scoringPlay": false,
"scoreValue": 0,
"situation": {
"down": 0,
"distance": 0,
"yardLine": 35,
"yardsToEndzone": 65
},
"endSituation": {
"down": 1,
"distance": 10,
"yardLine": 68,
"yardsToEndzone": 68
},
"yardsGained": 28,
"athletes": [
{
"id": "5081335",
"name": "Drew Stevens",
"displayName": "Drew Stevens",
"shortName": "D. Stevens",
"position": "PK",
"team": {
"id": "28"
},
"role": "kicker",
"type": "kicker",
"order": 1
},
{
"id": "4686468",
"name": "Seth McGowan",
"displayName": "Seth McGowan",
"shortName": "S. McGowan",
"position": "RB",
"team": {
"id": "11"
},
"role": "returner",
"type": "returner",
"order": 2
},
{
"id": "4361577",
"name": "Dyami Brown",
"displayName": "Dyami Brown",
"shortName": "D. Brown",
"position": "WR",
"team": {
"id": "28"
},
"role": "assistedBy",
"type": "assistedBy",
"order": 3
},
"... 1 more"
],
"priority": false,
"modified": "2026-10-04T16:46Z"
},
{
"id": "40187296562",
"sequenceNumber": "6200",
"text": "(Shotgun) D.Jones pass incomplete short left to J.Taylor (J.Kinlaw).",
"shortText": "Daniel Jones Incomplete Pass, Intended For Jonathan Taylor",
"alternativeText": "(Shotgun) D.Jones pass incomplete short left to J.Taylor (J.Kinlaw).",
"type": {
"id": "3",
"text": "Pass Incompletion"
},
"period": 1,
"clock": {
"value": 894,
"displayValue": "14:54"
},
"wallClock": "2026-10-04T13:33:01Z",
"homeScore": 0,
"awayScore": 0,
"scoringPlay": false,
"scoreValue": 0,
"situation": {
"down": 1,
"distance": 10,
"yardLine": 68,
"yardsToEndzone": 68,
"downDistanceText": "1st & 10 at IND 32",
"possessionText": "IND 32"
},
"endSituation": {
"down": 2,
"distance": 10,
"yardLine": 68,
"yardsToEndzone": 68
},
"yardsGained": 0,
"athletes": [
{
"id": "3917792",
"name": "Daniel Jones",
"displayName": "Daniel Jones",
"shortName": "D. Jones",
"position": "QB",
"team": {
"id": "11"
},
"role": "passer",
"type": "passer",
"order": 1
},
{
"id": "4242335",
"name": "Jonathan Taylor",
"displayName": "Jonathan Taylor",
"shortName": "J. Taylor",
"position": "RB",
"team": {
"id": "11"
},
"role": "receiver",
"type": "receiver",
"order": 2
},
{
"id": "4242335",
"name": "Jonathan Taylor",
"displayName": "Jonathan Taylor",
"shortName": "J. Taylor",
"position": "RB",
"team": {
"id": "11"
},
"role": "receiver",
"type": "receiver",
"order": 3
},
"... 1 more"
],
"priority": false,
"modified": "2026-10-04T16:46Z"
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 3,
"total": 184,
"totalPages": 62,
"hasNextPage": true,
"hasPreviousPage": false
},
"rateLimit": {
"limit": 125,
"remaining": 124,
"reset": 1793491200000
}
}
}Fields
| Field | Type | Notes |
|---|---|---|
| id | string | |
| sequenceNumber | string | |
| text | string | |
| shortText | string | |
| alternativeText | string | |
| type | object | |
| period | integer | |
| clock | object | |
| wallClock | string | |
| homeScore | integer | |
| awayScore | integer | |
| scoringPlay | boolean | |
| scoreValue | integer | |
| scoringType | string | |
| situation | object | |
| endSituation | object | |
| yardsGained | integer | |
| teamId | string | |
| athletes | PlayAthlete[] | |
| priority | boolean | |
| modified | string |
League support
Checked against live data on 2026-10-04: 28 available, 0 limited, 1 seasonal or not verified, 0 not available.
Code samples
curl
curl -H "Authorization: Bearer $RSA_API_KEY" \
"https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/401872965/plays?limit=3"JavaScript (fetch)
const res = await fetch("https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/401872965/plays?limit=3", {
headers: { Authorization: `Bearer ${process.env.RSA_API_KEY}` }
});
const { success, data, meta } = await res.json();
console.log(data, meta.rateLimit);Python (requests)
import os, requests
res = requests.get(
"https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/401872965/plays?limit=3",
headers={"Authorization": f"Bearer {os.environ['RSA_API_KEY']}"},
timeout=10,
)
body = res.json()
print(body["data"])MCP prompt (Claude, Cursor, ChatGPT)
Using the Realtime Sports MCP server (https://www.realtimesportsapi.com/api/mcp), call get_plays with sport "football" and league "nfl" and pick a finished game from get_events and summarize its scoring plays for NFL.Connect the hosted MCP server first: setup guide. Each tool call counts as one API call.
Rate limits and pricing
| Plan | Price | Calls | Rate | WebSocket | Webhooks |
|---|---|---|---|---|---|
| Free | $0 | 125/mo (1,000 first 30 days) | 1/s | 500 msgs/mo | No |
| Starter | $29/mo ($290/yr) | 10,000/mo | 5/s | Shares monthly pool | Shares monthly pool |
| Growth | $49/mo ($490/yr) | 25,000/mo | 10/s | Shares monthly pool | Shares monthly pool |
| Pro | $99/mo ($990/yr) | 50,000/mo | 20/s | 100,000 msgs/mo | 25,000/mo |
| Scale | $299/mo ($2,990/yr) | 500,000/mo | 100/s | 500,000 msgs/mo | 100,000/mo |
Every successful REST call counts as one call. Over the limit you get HTTP 429 with a Retry-After header. No SLA. Full pricing.
FAQ
- What does the play-by-play endpoint return?
- Plays for one game in chronological order: text, type, period, clock, score after the play, scoring flags, down-and-distance for football and the athletes involved. Paginated; limit up to 1000 returns a whole game in one call.
- Which leagues support play-by-play?
- Verified on 2026-10-04: NFL, College Football, NBA, MLB, NHL, MLS, U.S. Open Cup, Premier League, Champions League, Europa League, FA Cup, LaLiga, Serie A, Liga MX, FIFA World Cup, Women's Super League, Women's Champions League, Conference League, English League Cup, Bundesliga, Ligue 1, NWSL, EFL Championship, World Cup Qualifying (UEFA), World Cup Qualifying (CONMEBOL), World Cup Qualifying (AFC), World Cup Qualifying (CAF), World Cup Qualifying (CONCACAF). Seasonal or not verified: Men's College Basketball.
- Is it free to try?
- Yes. The Free plan is $0 with 125 calls per month (1,000 in the first 30 days) at 1 request per second, no card required. Every successful call counts as one call.
- How fresh is the data?
- Data is aggregated from public sources and is typically 20-30 seconds behind live play (about 1 second after our source). There is no SLA.
- Can I get updates pushed instead of polling?
- Webhooks are included on every paid plan (Starter, Growth, Pro, Scale). WebSocket is included on paid plans, and the Free plan gets a 500-message monthly preview.