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/v1

What 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, and mapping:status:read
  • Allowed source prefixes, such as cbs.basketball.nba, cbs.baseball.mlb, or wnba.basketball.wnba
  • Allowed entity types, currently player, team, and event
  • Optional new-entity minting permissions for approved primary sources only

Send the key on every request:

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

source must use this format:

provider.sport.league

Examples:

cbs.basketball.nba
cbs.baseball.mlb
wnba.basketball.wnba
fantasycalc.football.nfl

Basic Flow

  1. Send structured payloads to POST /self-service/enrich.
  2. SportsStack returns the original item plus enrichment results.
  3. If a provider mapping already exists, the response includes mapping_state: "mapped" and common_model_id.
  4. If the item has a stable provider ID but no known mapping, SportsStack creates a pending mapping and returns pending_mapping_id.
  5. If the item only has a name and no stable provider ID, SportsStack does not create a durable mapping. For player and team mentions, the response includes a resolution object describing whether the mention was resolved, ambiguous, not found, or invalid. mapping_state is "resolved" when resolution.state is "resolved", and "candidates_only" otherwise — see Context-Aware Player Resolution.
  6. Review pending mappings with GET /self-service/mappings/pending.
  7. Reconcile a pending mapping with POST /self-service/mappings/pending/{id}/reconcile.
  8. 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_id
  • player_source_id
  • team_source_id
  • event_source_id
  • player_id
  • provider_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_id

Example:

cbs.basketball.nba + player + jayson-tatum -> 0210eeb0-513d-47ab-a383-0631217b47ff

Modes

Use the mode field to describe the shape of the submitted payload.

ModeUse whenTypical payload
rowsYou scraped tabular rows from a pageinjury rows, roster rows, rankings rows
document_rowsYou parsed a report or PDF into rowsofficial injury reports, uploaded documents
jsonYou fetched an API response and want entity references enrichedtrade values, projections, rankings
textYou have text and want entity mentions matchedsearch 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

PurposeMethod and routeRequired scope
Enrich rows, document rows, JSON, or textPOST /self-service/enrichenrichment:write
Resolve known provider IDs in bulkPOST /self-service/mappings/resolvemapping:read
Create or resolve a mapping to an existing CMIDPOST /self-service/mappingsmapping:write
List pending mappingsGET /self-service/mappings/pendingmapping:read
Map or ignore a pending mappingPOST /self-service/mappings/pending/{id}/reconcilemapping:reconcile
Sync changed mapping statesGET /self-service/mappings/statusmapping:status:read
Mint a new SportsStack entity for an approved primary sourcePOST /self-service/entitiesentity:mint

Enrich Scraper Rows

Use POST /self-service/enrich for normal scraper output.

Recommended request fields:

  • source: allowlisted provider source
  • dataset: your dataset name, such as injuries
  • external_batch_id: your scrape or ingestion run ID
  • mode: rows, document_rows, json, or text
  • entity_types: entity types to enrich, such as ["player", "team"]
  • items: rows or objects to enrich
  • source_url: page, API, or document URL
  • report_timestamp: report or scrape timestamp
  • row_count: number of rows produced by the scrape
  • declared_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 does not create a durable mapping. For player and team mentions it runs contextual resolution and returns a resolution object alongside ranked candidates; mapping_state is "resolved" when resolution.state is "resolved" and "candidates_only" otherwise. 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 and team mentions submitted to POST /self-service/enrich with no stable provider source_idtext, document_rows, and player_name items. It is additive: the rows + source_id mapping flow described above is unchanged. A nonblank text or document_rows item requesting player automatically extracts every literal player name from the corpus described in Roster Scope unless the item already supplies explicit mentions or player_name. Requesting team on the same kind of item extracts every literal team mention — see Team extraction from text.

Request shape

An item may include:

  • player_name (optional): the player's name as known to your source.
  • text (optional): free text containing player mentions. Exact full names are matched wherever they appear; surname-only and fuzzy prose are not matched. Repeated references to the same decisive CMID collapse into one enrichment with multiple ordered mentions spans.
  • title (optional): a headline for the article. It is scored separately for relevance.in_title and never shifts the byte offsets reported in mentions — those stay anchored to text exactly as submitted. See The relevance object.
  • context.league (optional): a league abbreviation such as MLB. It must match the request source suffix case-insensitively or the request is rejected with 422.
  • context.team (optional): a team hint, as an object, a plain string, or an item-level shorthand field. Team is optional and not guaranteed — name-only requests still work and can resolve decisively on a unique strong name match. See Team hints below for every accepted form.
  • context.season_year (optional): the season the mention applies to. See "Current-Season Limitation" below.

The top-level request field roster_scope (not per item) controls how broad the player corpus is for text/document_rows extraction — see Roster Scope below.

Text-mode limits are 25 articles per request, 50,000 characters per article, 500,000 aggregate characters, 100 unique player groups per article, and 500 per request. Results are never silently truncated. A valid article with no detected player or team returns 200 with enrichments: [].

Request-level limits — article count, character budgets, and the 500-group request total — return 422 for the whole request. Limits and malformed fields that describe a single article do not: an article exceeding the 100-group cap, or carrying an invalid context.season_year or an unresolvable team hint, is reported on that item as resolution.state: "invalid_context" (with reason too_many_player_groups, invalid_season, or team_not_found) inside a normal 200. Every other item in the batch still returns its results.

Team hints

context.team accepts either an object or a plain string. Item-level shorthand fields on the item itself (outside of context) are also accepted, so you do not have to build a nested object.

FormWhereMatched against
Objectcontext.teamcommon_model_id, source_id, name, abbreviation
Plain stringcontext.teamabbreviation, name, nickname, and market + nickname
Plain stringitem-level teamsame as the context.team plain-string form
Stringitem-level team_namename or nickname
Stringitem-level team_abbr (or team_abbreviation)abbreviation
Stringitem-level team_common_model_idcommon_model_id
Stringitem-level team_source_id (or provider_team_id)source_id

When you send the object form, qualify it with one of common_model_id, source_id, name, or abbreviation. An unqualified team_id is not accepted.

Object form:

"context": {
  "team": { "name": "Los Angeles Dodgers", "abbreviation": "LAD" }
}

Plain string form. Because a bare string is matched against abbreviation, name, nickname, and market + nickname, any of these resolve the same team:

"context": { "team": "LAL" }
"context": { "team": "Lakers" }
"context": { "team": "Los Angeles Lakers" }

Item-level shorthand — equivalent to the plain string form, without nesting under context:

{
  "row_id": "row-1",
  "text": "Smith scored 20 for the Lakers.",
  "team": "LAL"
}

Item-level team_abbr / team_name fields work the same way:

{
  "row_id": "row-1",
  "text": "Smith scored 20 for the Lakers.",
  "team_abbr": "LAL"
}

A team hint that does not resolve to exactly one team in the request's source league does not fail the request. It is reported on that one item's resolution as state: "invalid_context" with reason: "team_not_found" (or reason: "team_ambiguous" if the hint matches more than one team) inside a normal 200 response — see Example C. Other items in the same batch resolve independently.

Multi-player extraction and byte offsets

{
  "source": "espn.baseball.mlb",
  "dataset": "news",
  "external_batch_id": "article-batch-42",
  "mode": "text",
  "entity_types": ["player"],
  "items": [
    {
      "row_id": "article-42",
      "text": "José Ramírez singled before Josh Naylor homered.",
      "context": {"league": "MLB", "season_year": 2026}
    }
  ]
}

The item returns two enrichments in first-occurrence order. Each includes all exact occurrences using zero-based UTF-8 byte offsets with an exclusive end:

{
  "entity_type": "player",
  "source_id": null,
  "mapping_state": "resolved",
  "common_model_id": "PLAYER_CMID",
  "mentions": [
    {"text": "José Ramírez", "start_byte": 0, "end_byte": 14}
  ],
  "entity": {
    "common_model_id": "PLAYER_CMID",
    "entity_type": "player",
    "display_name": "José Ramírez",
    "team": {
      "common_model_id": "TEAM_CMID",
      "display_name": "Cleveland Guardians",
      "abbreviation": "CLE"
    }
  },
  "resolution": {
    "state": "resolved",
    "decisive": true,
    "confidence": 1.0,
    "runner_up_gap": 1.0,
    "strategy": "document_active_roster",
    "reason": null
  },
  "relevance": {
    "role": "primary",
    "score": 0.55,
    "mention_count": 1,
    "in_title": false,
    "in_lede": true,
    "first_mention_byte": 0,
    "snippet": "José Ramírez singled before Josh Naylor homered."
  },
  "candidates": [
    {
      "common_model_id": "PLAYER_CMID",
      "entity_type": "player",
      "display_name": "José Ramírez",
      "confidence": 1.0,
      "team": {
        "common_model_id": "TEAM_CMID",
        "display_name": "Cleveland Guardians",
        "abbreviation": "CLE"
      }
    }
  ]
}

The second enrichment, for Josh Naylor, has the same shape with its own mentions span (start_byte: 30, end_byte: 41). Its relevance.score is lower (0.43, role "secondary") because it is mentioned later in the sentence and accounts for half the document's mention share — see The relevance object for how that score is derived.

Team extraction from text

entity_types: ["team"] on a text or document_rows item extracts every literal team mention — full name, nickname, market + nickname (for example "Boston Celtics"), and uppercase abbreviation (for example "BOS", not "bos" or "Bos") — against the tenant-visible team dictionary for the request's source league. It returns entity_type: "team" enrichments shaped like the player enrichments above.

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": "news",
    "external_batch_id": "article-batch-7",
    "mode": "text",
    "entity_types": ["team"],
    "items": [
      {
        "row_id": "article-7",
        "text": "The Lakers waived a two-way player on Friday."
      }
    ]
  }'

Response enrichment:

{
  "entity_type": "team",
  "source_id": null,
  "mapping_state": "resolved",
  "common_model_id": "TEAM_CMID",
  "entity": {
    "common_model_id": "TEAM_CMID",
    "entity_type": "team",
    "display_name": "Los Angeles Lakers",
    "abbreviation": "LAL"
  },
  "mentions": [
    {"text": "Lakers", "start_byte": 4, "end_byte": 10}
  ],
  "resolution": {
    "state": "resolved",
    "decisive": true,
    "confidence": 1.0,
    "runner_up_gap": null,
    "strategy": "document_team_gazetteer",
    "reason": null
  },
  "relevance": {
    "role": "primary",
    "score": 0.732,
    "mention_count": 1,
    "in_title": false,
    "in_lede": true,
    "first_mention_byte": 4,
    "snippet": "The Lakers waived a two-way player on Friday."
  },
  "candidates": []
}

Team gazetteer matches are always decisive: resolution.state is always "resolved", confidence is always 1.0, and candidates is always []. There is no ambiguous or not_found outcome for team extraction — a team that is not literally named in the text produces no enrichment at all, rather than a low-confidence one.

The resolution object

Every player or team 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 of resolved, ambiguous, not_found, invalid_context.
  • decisive: boolean, true only when state is "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 when state is "resolved". Team gazetteer matches have no runner-up, so this is null.
  • strategy: the resolution strategy that produced the result, one of document_active_roster, team_scoped_name, league_scoped_name, prose_span (player mentions), or document_team_gazetteer (team extraction).
  • reason: set when state is not "resolved", for example duplicate_name, runner_up_within_gap, low_confidence, historical_roster_unavailable, team_not_found, team_ambiguous, or team_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.

The relevance object

relevance is returned only for text/document_rows extraction. It is omitted entirely for player_name-only items, which have no document to be "about". It answers a different question than resolution:

  • resolution.confidence answers "is this the right entity?" — identity confidence for the top candidate.
  • relevance answers "is the article about it?" — how central the entity is to the document, independent of how confidently it was matched. A decisive match on a name that appears once in a closing aside scores identically to the headline subject on resolution.confidence; relevance is what tells them apart.
"relevance": {
  "role": "primary",
  "score": 0.75,
  "mention_count": 1,
  "in_title": false,
  "in_lede": true,
  "first_mention_byte": 0,
  "snippet": "Will Smith homered for the Dodgers."
}

Fields:

  • role: "primary" (a subject of the article), "secondary" (substantively covered), or "mention" (named in passing). Derived from score alone. A title match is strong evidence but is not decisive on its own: a headline naming two entities divides that evidence between them, so an "A vs B" headline does not mark both "primary".
  • score: an aboutness score in [0, 1], read as the fraction of available aboutness evidence the entity holds. It weighs four independent signals: how much of the document's naming the entity accounts for (both its share of all mentions and how far it stands above the average named entity, so adding more names to an article does not demote its subject), how early it first appears, whether it is in the lede (the first 400 bytes of text), and whether it is in title.
  • mention_count: how many times the entity is mentioned in text.
  • in_title: whether the entity was independently matched in the item's title field.
  • in_lede: whether the first mention falls within the first 400 bytes of text.
  • first_mention_byte: the byte offset of the first mention, or null if there is no mention.
  • snippet: text surrounding the first mention, or null if there is no mention.

Supplying title never shifts the byte offsets reported in mentionstitle is matched separately, and mentions stays anchored to text exactly as submitted.

mapping_state reflects resolution

mapping_state 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. For source-ID mappings those values are "mapped", "pending", and "ignored", exactly as in Enrich Scraper Rows above.

Contextual resolution (player_name/text items with no source_id, and document team extraction) never persists a mapping, so here mapping_state can only take one of two values:

  • "resolved" when resolution.state is "resolved".
  • "candidates_only" for every other resolution.state (ambiguous, not_found, invalid_context).

The two fields agree with each other. Continue to gate automation on resolution.state == "resolved" (decisive == true) plus common_model_id, per the guidance above — mapping_state is a convenience mirror of that decision, not an independent signal.

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.

Roster Scope

The top-level request field roster_scope controls how broad the player corpus is for text/document_rows extraction:

ValueBehavior
active_roster (default)Team-sport roster semantics: only players on an active team roster are eligible.
leagueAdditionally admits inactive identities — prospects, call-ups, and retirees — that news copy legitimately names.

Individual-sport leagues (UFC, boxing, MMA, tennis, golf, and similar) have no roster concept: competitors carry no team. SportsStack detects these leagues and always uses the full league corpus for them regardless of roster_scope, rather than applying team-sport roster eligibility, which would otherwise return no fighters or players at all.

A player is eligible if any tenant-visible provider feed reports them active. Provider records are ranked by priority rather than freshness, so a single stale is_active: false on a high-priority feed no longer hides a player the other feeds agree is active.

Both scopes remain subject to a 30,000-identity corpus cap. If the resolvable corpus for a league exceeds the cap, extraction fails closed with 503 service_unavailable rather than silently truncating the corpus and risking a wrong match.

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:

{
  "entity_type": "player",
  "source_id": null,
  "mapping_state": "resolved",
  "common_model_id": "6b1f2e10-9c2a-4e77-8b0e-2b6a6a2c9a11",
  "entity": {
    "common_model_id": "6b1f2e10-9c2a-4e77-8b0e-2b6a6a2c9a11",
    "entity_type": "player",
    "display_name": "Will Smith",
    "team": {
      "common_model_id": "8e2f1a30-4b5c-4d6e-9f70-1a2b3c4d5e6f",
      "display_name": "Los Angeles Dodgers",
      "abbreviation": "LAD"
    }
  },
  "mentions": [
    {"text": "Will Smith", "start_byte": 0, "end_byte": 10}
  ],
  "resolution": {
    "state": "resolved",
    "decisive": true,
    "confidence": 1.0,
    "runner_up_gap": 1.0,
    "strategy": "document_active_roster",
    "reason": null
  },
  "relevance": {
    "role": "primary",
    "score": 0.75,
    "mention_count": 1,
    "in_title": false,
    "in_lede": true,
    "first_mention_byte": 0,
    "snippet": "Will Smith homered for the Dodgers."
  },
  "candidates": [
    {
      "common_model_id": "6b1f2e10-9c2a-4e77-8b0e-2b6a6a2c9a11",
      "entity_type": "player",
      "display_name": "Will Smith",
      "confidence": 1.0,
      "team": {
        "common_model_id": "8e2f1a30-4b5c-4d6e-9f70-1a2b3c4d5e6f",
        "display_name": "Los Angeles Dodgers",
        "abbreviation": "LAD"
      }
    }
  ]
}

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. This item has no player_name field, so it is routed through the same document-extraction path as Team extraction from text and the multi-player example above; that is why strategy is "document_active_roster" and the response carries mentions and relevance. Consumers act on common_model_id plus resolution.decisive == truemapping_state: "resolved" mirrors the same decision.

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:

{
  "entity_type": "player",
  "source_id": null,
  "mapping_state": "candidates_only",
  "common_model_id": null,
  "entity": null,
  "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,
      "team": {
        "common_model_id": "3f405162-7788-99aa-bbcc-ddeeff011223",
        "display_name": "Carolina Hurricanes",
        "abbreviation": "CAR"
      }
    },
    {
      "common_model_id": "2e3f4051-6677-8899-aabb-ccddeeff0112",
      "entity_type": "player",
      "display_name": "Sebastian Aho",
      "confidence": 1.0,
      "team": {
        "common_model_id": "40516273-8899-aabb-ccdd-eeff01122334",
        "display_name": "New York Islanders",
        "abbreviation": "NYI"
      }
    }
  ]
}

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. This item has no text field, so it does not go through document extraction and the response has no mentions or relevance — those are populated only for text/document_rows items.

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. This item has a text field and no player_name, so it is document-routed, but the unresolved team hint short-circuits extraction before any player name is scanned — mentions is [] and relevance reflects zero mentions:

{
  "entity_type": "player",
  "source_id": null,
  "mapping_state": "candidates_only",
  "common_model_id": null,
  "entity": null,
  "mentions": [],
  "resolution": {
    "state": "invalid_context",
    "decisive": false,
    "confidence": null,
    "runner_up_gap": null,
    "strategy": null,
    "reason": "team_not_found"
  },
  "relevance": {
    "role": "mention",
    "score": 0.0,
    "mention_count": 0,
    "in_title": false,
    "in_lede": false,
    "first_mention_byte": null,
    "snippet": null
  },
  "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. This item's resolution.state is invalid_context for the whole item, but the request itself still returns 200: a team hint that fails to resolve is reported per item, and other items in the same batch resolve independently — see Team hints. Treat invalid_context 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:

StatusMeaning
401Missing or invalid API key
403Missing scope, disallowed source, disallowed entity type, or minting not approved
409Source ID is already mapped to a different CMID
422Invalid 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 resolve for fast read-only mapping checks.
  • Use enrich when you want SportsStack to return candidates and create pending mappings for stable unknown IDs.
  • Use status if 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 and team mentions with no source_id, gate automation on resolution.state == "resolved" (decisive == true) plus common_model_id. Never act on candidates[0] alone — see Context-Aware Player Resolution.
  • Send a context.team hint when you have one; it is optional, but it is what turns an ambiguous duplicate-name result into a resolved one. An unresolvable hint is reported per item as invalid_context, not a batch-level failure — see Team hints.

Did this page help you?