Self-Service ID Mapping and Enrichment
Self-Service ID Mapping and Enrichment
Use the self-service API when you have scraped rows, provider JSON, document rows, or text and need SportsStack common_model_id values back.
This API is identity-only. It enriches your payload with SportsStack IDs, creates provider ID mappings when allowed, and creates pending mappings when SportsStack cannot safely resolve an entity. It does not save injuries, articles, odds, stats, dynasty values, or search results into SportsStack data tables.
Base URL:
https://api.sportsstack.io/api/v1What You Need
SportsStack must issue an API key with the sources and entity types you are allowed to submit.
Your API key controls:
- Scopes:
enrichment:write,mapping:read,mapping:write,mapping:reconcile, andmapping:status:read - Allowed source prefixes, such as
cbs.basketball.nba,cbs.baseball.mlb, orwnba.basketball.wnba - Allowed entity types, currently
player,team, andevent - Optional new-entity minting permissions for approved primary sources only
Send the key on every request:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonsource must use this format:
provider.sport.leagueExamples:
cbs.basketball.nba
cbs.baseball.mlb
wnba.basketball.wnba
fantasycalc.football.nflBasic Flow
- Send structured payloads to
POST /self-service/enrich. - SportsStack returns the original item plus enrichment results.
- If a provider mapping already exists, the response includes
mapping_state: "mapped"andcommon_model_id. - If the item has a stable provider ID but no known mapping, SportsStack creates a pending mapping and returns
pending_mapping_id. - If the item only has a name and no stable provider ID, SportsStack returns
mapping_state: "candidates_only"and does not create a durable mapping. For player mentions, the response also includes aresolutionobject describing whether the mention was resolved, ambiguous, not found, or invalid — see Context-Aware Player Resolution. - Review pending mappings with
GET /self-service/mappings/pending. - Reconcile a pending mapping with
POST /self-service/mappings/pending/{id}/reconcile. - Optionally sync mapping changes with
GET /self-service/mappings/status.
Keeping a local mapping table is optional. Some clients cache mappings for speed, then use these endpoints to seed, refresh, and reconcile the cache.
Why Stable IDs Matter
You can send names. Names are useful evidence and SportsStack will use them to return candidate matches.
Names alone are not enough to create a durable provider mapping because names are not stable:
- multiple players can share a name
- names can be formatted differently across sources
- scraped rows can merge or truncate text
- players can change teams
- the same name can exist in multiple leagues or sports
For mapping writes, include the best stable provider key you have:
source_idplayer_source_idteam_source_idevent_source_idplayer_idprovider_player_id- a stable URL slug
- a stable profile URL or provider record ID
The durable mapping is stored as:
source + entity_type + source_id -> common_model_idExample:
cbs.basketball.nba + player + jayson-tatum -> 0210eeb0-513d-47ab-a383-0631217b47ffModes
Use the mode field to describe the shape of the submitted payload.
| Mode | Use when | Typical payload |
|---|---|---|
rows | You scraped tabular rows from a page | injury rows, roster rows, rankings rows |
document_rows | You parsed a report or PDF into rows | official injury reports, uploaded documents |
json | You fetched an API response and want entity references enriched | trade values, projections, rankings |
text | You have text and want entity mentions matched | search results, article snippets, summaries |
For all modes, send structured fields whenever possible. Raw text is acceptable as evidence, but structured fields produce better candidates and safer pending mappings.
Endpoint Summary
| Purpose | Method and route | Required scope |
|---|---|---|
| Enrich rows, document rows, JSON, or text | POST /self-service/enrich | enrichment:write |
| Resolve known provider IDs in bulk | POST /self-service/mappings/resolve | mapping:read |
| Create or resolve a mapping to an existing CMID | POST /self-service/mappings | mapping:write |
| List pending mappings | GET /self-service/mappings/pending | mapping:read |
| Map or ignore a pending mapping | POST /self-service/mappings/pending/{id}/reconcile | mapping:reconcile |
| Sync changed mapping states | GET /self-service/mappings/status | mapping:status:read |
| Mint a new SportsStack entity for an approved primary source | POST /self-service/entities | entity:mint |
Enrich Scraper Rows
Use POST /self-service/enrich for normal scraper output.
Recommended request fields:
source: allowlisted provider sourcedataset: your dataset name, such asinjuriesexternal_batch_id: your scrape or ingestion run IDmode:rows,document_rows,json, ortextentity_types: entity types to enrich, such as["player", "team"]items: rows or objects to enrichsource_url: page, API, or document URLreport_timestamp: report or scrape timestamprow_count: number of rows produced by the scrapedeclared_min_rows: minimum expected row count for the scrape
row_count and declared_min_rows help prevent thin failed scrapes from being treated as authoritative data. For example, if a normally large injury scrape returns 0 rows, SportsStack can reject it rather than interpreting it as "no injuries."
Example request:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/enrich" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.basketball.nba",
"dataset": "injuries",
"external_batch_id": "cbs-nba-injuries-2026-06-30T15:00:00Z",
"mode": "rows",
"entity_types": ["player", "team"],
"source_url": "https://www.cbssports.com/nba/injuries/",
"report_timestamp": "2026-06-30T15:00:00Z",
"row_count": 126,
"declared_min_rows": 30,
"items": [
{
"row_id": "row-1",
"payload": {
"player_name": "Jayson Tatum",
"player_source_id": "jayson-tatum",
"team_name": "Boston Celtics",
"position": "SF",
"status": "Out",
"injury": "Achilles",
"raw_row": {
"player": "Jayson Tatum",
"team": "Boston Celtics",
"status": "Out"
}
}
}
]
}'Example mapped response:
{
"source": "cbs.basketball.nba",
"dataset": "injuries",
"external_batch_id": "cbs-nba-injuries-2026-06-30T15:00:00Z",
"mode": "rows",
"count": 1,
"data": [
{
"index": 0,
"item_ref": "row-1",
"payload": {
"player_name": "Jayson Tatum",
"player_source_id": "jayson-tatum",
"team_name": "Boston Celtics"
},
"enrichments": [
{
"mapping_id": "11111111-1111-1111-1111-111111111111",
"pending_mapping_id": null,
"source": "cbs.basketball.nba",
"source_id": "jayson-tatum",
"entity_type": "player",
"common_model_id": "0210eeb0-513d-47ab-a383-0631217b47ff",
"mapping_state": "mapped",
"match_status": "manual_matched",
"ignored_pending": false,
"action": "resolved",
"entity": {
"common_model_id": "0210eeb0-513d-47ab-a383-0631217b47ff",
"entity_type": "player",
"display_name": "Jayson Tatum"
}
}
]
}
]
}Example pending response:
{
"mapping_id": "22222222-2222-2222-2222-222222222222",
"pending_mapping_id": "22222222-2222-2222-2222-222222222222",
"source": "cbs.basketball.nba",
"source_id": "new-player-123",
"entity_type": "player",
"common_model_id": null,
"mapping_state": "pending",
"match_status": "pending_manual_review",
"ignored_pending": false,
"action": "pending_created",
"candidates": [
{
"common_model_id": "0210eeb0-513d-47ab-a383-0631217b47ff",
"entity_type": "player",
"display_name": "Jayson Tatum",
"confidence": 0.88
}
]
}When an item has no stable provider source_id, POST /self-service/enrich still returns a candidates_only mapping_state, but for player mentions it also runs contextual resolution and returns a resolution object alongside ranked candidates. See Context-Aware Player Resolution below for the full contract and worked examples.
Context-Aware Player Resolution (No Provider Source ID)
This section covers player mentions submitted to POST /self-service/enrich with no stable provider source_id — text and player_name items. It is additive: the rows + source_id mapping flow described above is unchanged.
Request shape
An item may include:
-
player_name(optional): the player's name as known to your source. -
text(optional): free text containing a player mention. The player name is matched wherever it appears intext— it does not have to be the first token. -
context.team(optional): a team hint. Team is optional and not guaranteed — name-only requests still work and can resolve decisively on a unique strong name match. When you do sendcontext.team, qualify it with one of:common_model_idsource_idnameand/orabbreviation
An unqualified
team_idis not accepted. -
context.season_year(optional): the season the mention applies to. See "Current-Season Limitation" below.
The resolution object
resolution objectEvery player-mention enrichment without a source_id includes a resolution object next to candidates:
"resolution": {
"state": "resolved",
"decisive": true,
"confidence": 0.94,
"runner_up_gap": 0.41,
"strategy": "team_scoped_name",
"reason": null
}Fields:
state: one ofresolved,ambiguous,not_found,invalid_context.decisive: boolean,trueonly whenstateis"resolved".confidence: a deterministic match score for the top candidate. It is not a calibrated probability.runner_up_gap: the score gap between the top candidate and the runner-up. A small gap is a close call even whenstateis"resolved".strategy: the resolution strategy that produced the result, one ofteam_scoped_name,league_scoped_name, orprose_span.reason: set whenstateis not"resolved", for exampleduplicate_name,runner_up_within_gap,low_confidence,historical_roster_unavailable,team_not_found, orteam_league_mismatch.
Act only on resolution.state == "resolved" (equivalently decisive == true) together with the top-level common_model_id. The candidates array is returned in every state, including ambiguous, not_found, and invalid_context. Candidates are advisory suggestions for a human reviewer or a downstream reconciliation step — a suggestion is never an implied tag. Do not select candidates[0] as if it were a confirmed match; a non-resolved state means SportsStack did not find enough evidence to pick a winner.
mapping_state is independent of resolution
mapping_state is independent of resolutionmapping_state (for example candidates_only) describes persistent identity-mapping state — whether a durable source + entity_type + source_id -> common_model_id mapping exists, is pending, or cannot be created at all. resolution describes the outcome of matching a name or text mention against SportsStack player identities for this one request. Neither implies the other:
- A request can be
mapping_state: "candidates_only"(no durable mapping exists, because there is nosource_idto key one) whileresolution.stateis"resolved"with a confidentcommon_model_id. - A request can be
mapping_state: "candidates_only"withresolution.state: "ambiguous"and nocommon_model_idat all.
Current-Season Limitation
If context.season_year is not the current season and team membership is material to disambiguating the player (for example, the player changed teams between seasons), SportsStack returns state: "ambiguous" with reason: "historical_roster_unavailable" rather than applying today's roster to a prior season's mention.
Example A: Decisive resolution with team context
Request:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/enrich" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.baseball.mlb",
"mode": "text",
"entity_types": ["player"],
"items": [
{
"row_id": "row-1",
"payload": {
"text": "Will Smith homered for the Dodgers.",
"context": {
"team": { "name": "Los Angeles Dodgers", "abbreviation": "LAD" },
"season_year": 2026
}
}
}
]
}'Response enrichment:
{
"mapping_id": null,
"pending_mapping_id": null,
"source": "cbs.baseball.mlb",
"source_id": null,
"entity_type": "player",
"common_model_id": "6b1f2e10-9c2a-4e77-8b0e-2b6a6a2c9a11",
"mapping_state": "candidates_only",
"match_status": "auto_matched",
"action": "resolved",
"resolution": {
"state": "resolved",
"decisive": true,
"confidence": 1.0,
"runner_up_gap": 1.0,
"strategy": "team_scoped_name",
"reason": null
},
"candidates": [
{
"common_model_id": "6b1f2e10-9c2a-4e77-8b0e-2b6a6a2c9a11",
"entity_type": "player",
"display_name": "Will Smith",
"confidence": 1.0
}
]
}There are two active MLB players named "Will Smith" — a Dodgers catcher and a Braves relief pitcher. Because the Los Angeles Dodgers / LAD team hint scopes candidates to the Dodgers roster, only the catcher is a candidate, so the mention resolves decisively — the Braves pitcher is never in the candidate set. Note that text mentions "Will Smith" as the second and third tokens, not the first, and the match still succeeds. Consumers act on common_model_id plus resolution.decisive == true.
Example B: Ambiguous — duplicate name, no uniquely-resolving context
Request:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/enrich" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.hockey.nhl",
"mode": "text",
"entity_types": ["player"],
"items": [
{
"row_id": "row-2",
"payload": {
"player_name": "Sebastian Aho"
}
}
]
}'Response enrichment:
{
"mapping_id": null,
"pending_mapping_id": null,
"source": "cbs.hockey.nhl",
"source_id": null,
"entity_type": "player",
"common_model_id": null,
"mapping_state": "candidates_only",
"match_status": "unresolved",
"action": "candidates_returned",
"resolution": {
"state": "ambiguous",
"decisive": false,
"confidence": 1.0,
"runner_up_gap": 0.0,
"strategy": "league_scoped_name",
"reason": "duplicate_name"
},
"candidates": [
{
"common_model_id": "1d2e3f40-5566-7788-99aa-bbccddeeff01",
"entity_type": "player",
"display_name": "Sebastian Aho",
"confidence": 1.0
},
{
"common_model_id": "2e3f4051-6677-8899-aabb-ccddeeff0112",
"entity_type": "player",
"display_name": "Sebastian Aho",
"confidence": 1.0
}
]
}There are two active NHL players named "Sebastian Aho" — a Carolina forward and an Islanders defenseman — and no context.team hint was sent to break the tie. common_model_id is null and resolution.decisive is false even though a candidates[0] exists. Route both candidates to a reviewer, or resend the request with a context.team hint if you can determine the correct team.
Example C: Invalid context — team hint does not resolve in this league
Request:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/enrich" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.basketball.nba",
"mode": "text",
"entity_types": ["player"],
"items": [
{
"row_id": "row-3",
"payload": {
"text": "Smith scored 20 for the Rockets.",
"context": {
"team": { "name": "Houston Texans", "abbreviation": "HOU" }
}
}
}
]
}'The context.team hint (Houston Texans) is an NFL team, but source is cbs.basketball.nba. SportsStack cannot apply a team hint that does not belong to the requested source's league, and it does not fall back to a league-wide name guess:
{
"mapping_id": null,
"pending_mapping_id": null,
"source": "cbs.basketball.nba",
"source_id": null,
"entity_type": "player",
"common_model_id": null,
"mapping_state": "candidates_only",
"match_status": "unresolved",
"action": "candidates_returned",
"resolution": {
"state": "invalid_context",
"decisive": false,
"confidence": null,
"runner_up_gap": null,
"strategy": null,
"reason": "team_not_found"
},
"candidates": []
}state: "invalid_context" tells you the context you sent could not be applied — the team hint did not resolve to a single team in the source's league. SportsStack deliberately does not fall back to a league-wide guess, so candidates is empty. Treat it like not_found for automated purposes, and route it to review or resend with a corrected hint.
Resolve Known Provider IDs
Use POST /self-service/mappings/resolve when you already have provider IDs and want the current mapping state.
This endpoint is read-only. It does not create pending mappings.
curl -X POST "https://api.sportsstack.io/api/v1/self-service/mappings/resolve" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.basketball.nba",
"entity_types": ["player"],
"items": [
{
"entity_type": "player",
"source_id": "jayson-tatum",
"name": "Jayson Tatum"
}
]
}'If a mapping exists, the item returns mapping_state: "mapped" and common_model_id.
If no mapping exists, the item returns mapping_state: "not_found" plus visible candidates when SportsStack can find possible matches.
Create a Mapping
Use POST /self-service/mappings when you already know the correct SportsStack common_model_id.
curl -X POST "https://api.sportsstack.io/api/v1/self-service/mappings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "cbs.basketball.nba",
"entity_type": "player",
"source_id": "jayson-tatum",
"common_model_id": "0210eeb0-513d-47ab-a383-0631217b47ff",
"evidence": {
"player_name": "Jayson Tatum",
"team_name": "Boston Celtics",
"source_url": "https://www.cbssports.com/nba/injuries/"
}
}'Rules:
- If the same mapping already exists, the response is idempotent.
- If a pending mapping exists for that source ID, the request resolves it.
- If the source ID is already mapped to a different CMID, the API returns
409 conflict. - If the target CMID is not visible to your tenant, the API returns an error.
Review Pending Mappings
List pending mappings:
curl "https://api.sportsstack.io/api/v1/self-service/mappings/pending?source=cbs.basketball.nba&entity_type=player&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"Resolve a pending mapping to an existing CMID:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/mappings/pending/22222222-2222-2222-2222-222222222222/reconcile" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "map",
"common_model_id": "0210eeb0-513d-47ab-a383-0631217b47ff",
"evidence": {
"reviewed_by": "external-reviewer",
"reason": "Matched by name, team, league, and source row"
}
}'Ignore a pending mapping:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/mappings/pending/22222222-2222-2222-2222-222222222222/reconcile" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "ignore",
"reason": "Not a real player row"
}'Use ignore for rows that should not become a SportsStack identity mapping, such as headers, notes, malformed parser output, test rows, or non-entity text.
Sync Mapping Changes
Use GET /self-service/mappings/status if you cache mappings locally and want to sync changes.
curl "https://api.sportsstack.io/api/v1/self-service/mappings/status?source=cbs.basketball.nba&entity_type=player&since=2026-06-30T00:00:00Z&limit=500" \
-H "Authorization: Bearer YOUR_API_KEY"The response includes mapped, pending, and ignored states that changed after since.
Optional: Approved New-League Seeding
Most clients cannot mint SportsStack IDs. New entity creation requires a separate approval because it can create canonical SportsStack entities.
If SportsStack approves your API key as a primary source for a new league, your key may receive:
entity:mint- approved mint source prefixes
Then you can call POST /self-service/entities to create a player, team, or event CMID and write the provider mapping in one step.
Example:
curl -X POST "https://api.sportsstack.io/api/v1/self-service/entities" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "partner.pickleball.mlp",
"entity_type": "player",
"source_id": "mlp-player-123",
"entity": {
"display_name": "Jane Example",
"league_common_model_id": "550e8400-e29b-41d4-a716-446655440000",
"position": "Right"
},
"evidence": {
"source_url": "https://example.com/roster",
"observed_at": "2026-06-30T15:00:00Z"
}
}'If your key does not have entity:mint, this route returns 403.
Error Behavior
Common responses:
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
403 | Missing scope, disallowed source, disallowed entity type, or minting not approved |
409 | Source ID is already mapped to a different CMID |
422 | Invalid request or failed quality-floor validation |
Quality-floor example:
{
"error_code": "validation_error",
"detail": "row_count is below declared_min_rows; batch rejected as non-authoritative"
}Practical Recommendations
- Send structured rows by default.
- Include stable provider IDs whenever possible.
- Include names, teams, league, position, source URL, report timestamp, and raw row as evidence.
- Use
resolvefor fast read-only mapping checks. - Use
enrichwhen you want SportsStack to return candidates and create pending mappings for stable unknown IDs. - Use
statusif you maintain a local cache. A local cache is optional. - Do not treat candidates as canonical mappings until a mapping is created or a pending mapping is reconciled.
- For player mentions with no
source_id, gate automation onresolution.state == "resolved"(decisive == true) pluscommon_model_id. Never act oncandidates[0]alone, and never treatmapping_stateandresolution.stateas the same signal — see Context-Aware Player Resolution. - Send a
context.teamhint when you have one; it is optional, but it is what turns anambiguousduplicate-name result into aresolvedone.
Updated 1 day ago
