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 returns mapping_state: "candidates_only" and does not create a durable mapping. For player mentions, the response also includes a resolution object describing whether the mention was resolved, ambiguous, not found, or invalid — 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 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_idtext 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 in text — 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 send context.team, qualify it with one of:

    • common_model_id
    • source_id
    • name and/or abbreviation

    An unqualified team_id is not accepted.

  • context.season_year (optional): the season the mention applies to. See "Current-Season Limitation" below.

The resolution object

Every 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 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".
  • strategy: the resolution strategy that produced the result, one of team_scoped_name, league_scoped_name, or prose_span.
  • reason: set when state is not "resolved", for example duplicate_name, runner_up_within_gap, low_confidence, historical_roster_unavailable, team_not_found, 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.

mapping_state is independent of resolution

mapping_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 no source_id to key one) while resolution.state is "resolved" with a confident common_model_id.
  • A request can be mapping_state: "candidates_only" with resolution.state: "ambiguous" and no common_model_id at 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:

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 mentions with no source_id, gate automation on resolution.state == "resolved" (decisive == true) plus common_model_id. Never act on candidates[0] alone, and never treat mapping_state and resolution.state as the same signal — 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.

Did this page help you?