Competition filter odds API: filter Pinnacle odds by league
PinnWire is the cleanest way to get current Pinnacle odds into a league-focused workflow. Request one sport, use the returned league_id and league_name fields, and apply an auditable client-side allowlist instead of relying on an undocumented query parameter.
GET /kit/v1/markets?sport_id=N and GET /kit/v1/prematch/fixtures?sport_id=N return the current event fields your filter needs: sport_id, league_id, league_name, event identity, participants, start time, and markets. PinnWire does not claim a server-side league_id query parameter. Keep league selection in a small, tested policy layer you control.
Why competition filtering belongs in your odds client
A sports odds league filter usually serves one of three jobs: focus a dashboard on a competition, reduce a model's input universe, or limit an alert queue to leagues you have reviewed. Each job needs a slightly different rule, but all of them benefit from the same boundary: fetch a truthful current PinnWire snapshot first, then make your competition decision from fields in that response.
Only reviewed leagues
Match stable league_id values where possible. This is the safest default for a model, dashboard tab, or narrow alert queue.
Remove known noise
Normalize league_name and exclude a reviewed set. Send new names to an unknown bucket rather than silently accepting them.
Review what is live
Start broad within one sport_id, log the names and IDs you actually receive, then promote a league into an allowlist.
PinnWire keeps this separation explicit. It supplies the current Pinnacle reference board and freshness metadata; your application decides which competition is relevant to its own users or model.
Try the PinnWire competition data now
The public demo key works without signup on REST. It is a small shared taster, so use a free personal trial key for repeated development or a production integration.
curl "https://pinnwire.com/kit/v1/markets?sport_id=1&key=demo&fresh=competition-filter-1"
curl "https://pinnwire.com/kit/v1/prematch/fixtures?sport_id=1&key=demo&fresh=competition-filter-2"
Inspect the real response before writing an allowlist. A league can be absent because it has no currently published event, and coverage changes with the live board. The optional fresh value is an ignored cache-buster; it does not create a league filter.
{
"sport_id": 1,
"generated_at": "2026-08-26T10:05:12.412Z",
"events": [{
"event_id": 1628594960,
"sport_id": 1,
"league_id": 2196,
"league_name": "Spain - La Liga",
"home": "Alaves",
"away": "Villarreal",
"starts": "2026-08-28T19:30:00Z",
"event_type": "prematch",
"periods": {"num_0": {"money_line": {"home": 2.45, "draw": 3.30, "away": 2.90}}
}
}]
}
The sample shape is intentionally enough to show the filter boundary. Iterate events, read league_id as the stable identifier, use league_name as the human label, and treat markets and periods as optional published data.
Supported query parameters—and the one that is not
Use the server-side parameters that PinnWire documents to reduce transport volume. There is no documented league or competition query parameter on these surfaces, so do not invent one and expect the server to enforce it.
| Surface | Supported parameters | League filter boundary |
|---|---|---|
/kit/v1/markets | sport_id required; optional event_type=live|prematch, since, include_specials=1|nested | Filter returned league_id/league_name in your client. |
/kit/v1/prematch/fixtures | sport_id required; optional since, include_specials=1|nested | Filter returned league_id/league_name in your client. |
/kit/v1/details | event_id required | Use the returned event's league fields to confirm a selected fixture. |
/api/drops or /v1/drops | mode, sport_id, min_drop_pct, max_drop_pct, max_age_sec, markets, periods, live=1, limit | Filter each REST drop's league string in your client; no league query parameter. |
/odds-drop / /odds-drop-prematch | min_drop; prematch also accepts recheck=N | Apply the league policy in your SSE consumer after each frame. |
&league_id=2196 to a request does not turn it into a server-side competition filter. Unknown query values are not a substitute for a documented contract. Request the sport, filter the response, and test that the result set matches your policy.Build the league policy around IDs and names
Use IDs for identity and names for review. A name is useful in a UI and in logs, but punctuation, spacing, translation, and provider naming can change. A normalized name is a practical fallback when a drop record carries league but not league_id.
Choose one sport before choosing leagues
Pass the correct sport_id first. The current map is in the PinnWire docs: Soccer is 1, Tennis 2, Basketball 3, Hockey 4, Football 5, Baseball 6, Rugby 7, MMA 8, Boxing 9, Volleyball/Handball 10, Esports 11, Golf 12, and Cricket 13.
Record the live IDs you actually receive
Log league_id, league_name, sport_id, and a sample event_id. A league list is not a permanent catalog: it represents currently published events and can change.
Prefer an allowlist for high-consequence work
For a model or alert pipeline, accept a row only when its ID is known. Keep the name alongside the ID so a changed label becomes visible during review rather than silently changing the meaning of your filter.
Handle unknowns deliberately
Route missing IDs, blank names, and new names to unknown or reject them. Never turn a missing league into “all leagues” by accident, and never substitute team-name guesses for competition identity.
const SPORT_ID = 1;
const allowedLeagueIds = new Set(["2196", "2201"]);
const allowedLeagueNames = new Set(["spain - la liga", "england - premier league"]);
function normalizeName(value) {
return String(value ?? "")
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[’']/g, "")
.replace(/[^a-z0-9]+/g, " ")
.trim()
.replace(/\s+/g, " ");
}
function eventLeague(event) {
return {
id: event.league_id == null ? null : String(event.league_id),
name: String(event.league_name ?? ""),
};
}
function keepLeague(event) {
if (Number(event.sport_id) !== SPORT_ID) return false;
const league = eventLeague(event);
if (league.id !== null) return allowedLeagueIds.has(league.id);
return allowedLeagueNames.has(normalizeName(league.name));
}
const filtered = (body.events ?? []).filter(keepLeague);
This example uses exact matches. That is intentional: a short substring such as liga can match more competitions than you meant. If you need a broad country or title grouping, create and review the resulting ID set first, then store the IDs rather than leaving a fuzzy search in the production decision path.
Names, IDs, and unknown competitions
league rather than league_id.unknown in a broad discovery screen.Keep the raw value beside your normalized value. That lets an operator see whether a new competition is genuinely new or only differs in punctuation. It also keeps your filter auditable when the current PinnWire board changes.
This tiny mockup demonstrates exact normalized-name matching. It does not call the API or imply a server-side league parameter.
Deduplicate parent and child fixtures safely
One real fixture can have multiple matchup records in the underlying stream. PinnWire's /kit/v1/markets and /kit/v1/prematch/fixtures surfaces already collapse parent/child fixture records and return one canonical event with the most useful market payload. That is the best starting point for a league-filtered REST board.
You still need an explicit policy when combining REST with raw WebSocket frames, special rows, or drops. Preserve each raw ID for traceability. Use parentId/parent_id only where the data actually supplies it, and never dedupe by “home vs away” text alone.
function fixtureKey(row) {
// parentId/parent_id are optional raw/special metadata.
// A normal /kit event is already canonical, so event_id is enough there.
return String(row.parent_id ?? row.parentId ?? row.event_id ?? row.id);
}
function dedupeFixtures(rows) {
const byFixture = new Map();
for (const row of rows) {
const key = fixtureKey(row);
if (key === "undefined" || key === "null") continue;
const previous = byFixture.get(key);
const previousDepth = JSON.stringify(previous?.periods ?? previous?.markets ?? {}).length;
const currentDepth = JSON.stringify(row.periods ?? row.markets ?? {}).length;
if (!previous || currentDepth > previousDepth) byFixture.set(key, row);
}
return [...byFixture.values()];
}
const canonicalLeagues = dedupeFixtures((body.events ?? []).filter(keepLeague));
Filter competition-specific drops
PinnWire's REST drop endpoints are useful for a recent, queryable alert buffer. Add the supported sport and movement filters on the request, then apply the league policy locally. A REST drop row exposes event_id, sport_name, league, market, side, decimal from/to, drop_pct, optional nvp, and freshness fields.
curl "https://pinnwire.com/api/drops?mode=prematch&sport_id=1&min_drop_pct=3&max_age_sec=900&markets=spread,total&periods=0&limit=50&key=demo&fresh=league-drops-1"
function dropLeague(drop) {
// REST drops publish the league name; they do not publish league_id.
return normalizeName(drop.league);
}
function keepDrop(drop, policy) {
if (policy.sportId != null && Number(drop.sport_id) !== policy.sportId) return false;
const name = dropLeague(drop);
if (!name) return policy.unknown === "keep";
if (policy.excludeNames?.has(name)) return false;
if (policy.includeNames?.size) return policy.includeNames.has(name);
return true;
}
const policy = {
sportId: 1,
includeNames: new Set(["spain la liga", "england premier league"]),
excludeNames: new Set(),
unknown: "reject",
};
const leagueDrops = (body.drops ?? []).filter((drop) => keepDrop(drop, policy));
Notice the two different league fields: normalized fixture responses use league_name and league_id; REST drops use league and event_id. Normalize them into one internal shape before you share policy code.
function normalizeLeagueRow(row) {
if (row.league_name != null || row.league_id != null) {
return {
id: row.league_id == null ? null : String(row.league_id),
name: String(row.league_name ?? ""),
eventId: row.event_id == null ? null : String(row.event_id),
};
}
return {
id: null,
name: String(row.league ?? ""),
eventId: row.event_id == null ? null : String(row.event_id),
};
}
The drop buffer is recent and in memory, not a permanent history. Persist accepted rows yourself if your competition report needs more than the roughly three-hour recent buffer.
Map an alert back to the canonical event
A drop's event_id is the event reference for the detection. Use it to join the alert to the latest snapshot you fetched. If you cannot find an exact ID in your local snapshot, fetch /kit/v1/details?event_id=... and re-check the returned league before displaying or routing the alert. Do not infer a match from participant strings.
async function enrichDrop(drop, key, eventsById) {
const eventId = String(drop.event_id);
const local = eventsById.get(eventId);
if (local) return { drop, event: local, league: normalizeLeagueRow(local) };
const url = new URL("https://pinnwire.com/kit/v1/details");
url.searchParams.set("event_id", eventId);
url.searchParams.set("key", key);
const response = await fetch(url);
if (!response.ok) return { drop, event: null, league: normalizeLeagueRow(drop) };
const body = await response.json();
const event = body.events?.[0] ?? null;
return { drop, event, league: event ? normalizeLeagueRow(event) : normalizeLeagueRow(drop) };
}
function routeDrop(enriched, policy) {
const league = enriched.league;
if (league.id && policy.includeIds?.size) {
return policy.includeIds.has(league.id);
}
return keepDrop({
sport_id: enriched.drop.sport_id,
league: league.name,
}, policy);
}
If the details lookup returns no event or the league is missing, keep the item in an explicit review queue. A missing join is a data-quality state, not permission to route the alert to every league.
Move from REST tuning to SSE delivery
REST is the right place to discover competition names, tune thresholds, backfill after reconnects, and verify a league policy. When the policy is stable and qualifying drops should arrive immediately, use PinnWire's SSE surface. The stream supplies a min_drop threshold; filter league in your consumer because SSE has no league selector.
REST snapshot
Request one sport, inspect IDs and names, and build the reviewed set from the actual current response.
REST drops
After reconnecting, query an overlapping max_age_sec window and apply the same normalized policy.
SSE alerts
Connect to /odds-drop or /odds-drop-prematch, then reject non-matching leagues at the edge.
const stream = new EventSource(
`https://pinnwire.com/odds-drop-prematch?key=${encodeURIComponent(KEY)}&min_drop=3&recheck=30`
);
stream.onmessage = (message) => {
const rows = JSON.parse(message.data);
for (const drop of rows) {
if (keepDrop(drop, policy)) notify(drop);
}
};
Production checklist for a sports odds league filter
- Use
sport_idon every board or drop query instead of fetching all sports for a one-sport screen. - Use exact
league_idallowlists for stable routing; retainleague_namefor display and review. - Normalize names with Unicode folding, punctuation removal, and whitespace collapse before comparing fallback names.
- Keep an
unknownbucket for missing or newly observed competitions. - Use PinnWire's already canonical
/kitfixtures; if merging raw records, dedupe only with actual parent/child IDs. - Map drops by
event_id, then confirm the league from the current event before routing high-value alerts. - Check
generated_atand dropage_s; do not present a cached response as live. - Persist accepted rows or snapshots when you need history beyond PinnWire's recent drop buffer.
- Keep API keys server-side and honor plan limits and HTTP 429 responses.
Why PinnWire is a strong competition-filter foundation
PinnWire gives a focused Pinnacle-only odds surface with current live and prematch snapshots, decimal JSON, explicit league identifiers, complete published market periods, freshness fields, recent drop alerts, and an optional raw WebSocket for applications that need every subscribed update. That makes the boundary easy to reason about: PinnWire supplies the live market context; your league policy remains readable, testable application code.
Choose PinnWire when you want one sharp reference board for league-specific dashboards, models, line-movement monitors, fair-price work, and alert routing. Start with the public demo, prove the response shape, and move to a free personal trial key when your filter needs a private allowance.
Read the PinnWire API docs, see the dropping odds guide, or learn about the PinnWire Pinnacle WebSocket API when a client-side snapshot loop is no longer enough.
Competition filter odds API questions
Does PinnWire have a server-side league filter parameter?
No. PinnWire's documented odds endpoints accept sport_id and other transport or response-shape parameters, but not league_id or league_name. Request the sport you need, then filter the returned league_id and league_name fields in your own client.
Which fields should I use to filter Pinnacle odds by league?
For /kit/v1/markets and /kit/v1/prematch/fixtures, use league_id as the stable key and league_name as display text. For REST or SSE drops, the published drop shape includes league as a name and event_id as the event reference; apply the league-name policy there and map event context separately when needed.
Should a sports odds league filter use names or IDs?
Use league_id for an allowlist when it is present, and keep a normalized league_name for display and review. Names can change spelling or punctuation, so name matching should be normalized and unknown or missing leagues should be handled explicitly.
How do I filter PinnWire odds drops by competition?
Add the supported sport_id, mode, market, period, age, and drop-threshold filters to /api/drops, then filter each returned row's league field in your consumer. SSE provides push delivery and a minimum drop threshold, but league filtering remains client-side.
Does PinnWire deduplicate parent and child fixtures?
Yes. The /kit fixture surfaces collapse parent and child matchup records to one canonical event before returning them. If you merge raw updates, drops, or special rows yourself, retain the raw IDs and use parent_id or parentId only when that field is actually present; never deduplicate by team names alone.
key=demo to inspect current Pinnacle league fields, pass sport_id to keep the response focused, then move to a free emailed trial key. Use IDs for identity, normalized names for review, explicit unknown handling, and client-side filtering for both REST snapshots and SSE drops.