Time to match start odds filter: filter prematch Pinnacle odds by start time
PinnWire gives you the current Pinnacle odds and the normalized start fields needed to make this filter reliable. Read starts or start_ts as UTC, calculate the minutes until kickoff in your own code, and keep only the prematch events that fit your product's window.
max_time_to_match_start or start_before query parameter. Request /kit/v1/prematch/fixtures?sport_id=N, read each event's starts (or the identical start_ts alias), calculate (start_ms - Date.now()) / 60000, and keep the range you want. For drops, use mode=prematch; max_age_sec controls how recently a drop was detected, not how close its match is to starting.
- What time to match start means
- Exact start fields and surfaces
- What PinnWire filters server-side
- Parse UTC, epoch seconds and milliseconds
- Filter prematch odds by start time
- Join dropping odds to current fixtures
- Handle live events, negative times and schedule changes
- Freshness, refresh cadence and storage
- Why PinnWire fits start-time workflows
- Questions
What a time to match start filter actually measures
A time to match start odds filter measures the distance between now and the event's scheduled start. It is different from the age of an odds update and different from the age of a detected drop.
Time to start
(scheduled_start - now) tells you whether an event begins in 30 minutes, 12 hours or two days.
Detection age
age_s and max_age_sec describe when PinnWire detected a price fall. They do not describe kickoff distance.
Live or prematch
event_type on events and is_live on drops tell your client which queue should receive the record.
Once those clocks are separate, “filter prematch odds by start time” becomes a small, testable rule. A model can ask for events between 0 and 48 hours away; a dashboard can show the next 6 hours; a reconnect worker can use a broader window while it refreshes its local cache.
Exact PinnWire start fields and API surfaces
The normalized /kit/v1 shape makes a Pinnacle odds start_ts API integration straightforward, but the field name and presence vary slightly by endpoint. This is the field contract to code against:
| Surface | Start field(s) | Stream clue | Use |
|---|---|---|---|
/kit/v1/markets | starts, start_ts | event_type | Live or prematch events; add event_type=prematch for upcoming odds. |
/kit/v1/prematch/fixtures | starts, start_ts | event_type: "prematch" | Best starting point for a sport's upcoming fixture board. |
/kit/v1/details | starts, start_ts | event_type | Refresh one known event after a drop or event selection. |
/kit/v1/prematch/lines | starts | Prematch endpoint | Compact full-game lines for one event; it does not return start_ts. |
/api/drops or /v1/drops | starts | is_live | Detected movement with event_id, alerted and age_s. |
In the normalized event response, starts and start_ts are the same ISO-8601 UTC string. Despite the _ts suffix, start_ts is not an epoch number on this surface. The alias exists for compatibility with common Pinnacle-shaped clients.
{
"event_id": 1634696920,
"home": "Home FC",
"away": "Away FC",
"starts": "2026-08-28T19:30:00Z",
"start_ts": "2026-08-28T19:30:00Z",
"event_type": "prematch"
}
The drop response uses starts rather than start_ts and also carries is_live. Its alerted value is an epoch-second detection timestamp; age_s is the computed detection age. Do not confuse either of those with the scheduled match start.
What PinnWire can and cannot filter for you
PinnWire has explicit server-side controls for stream, sport, market, period, movement size and drop recency. It does not currently expose a server-side maximum time-to-start filter.
| Need | Server-side control | Meaning |
|---|---|---|
| Upcoming event board | /kit/v1/prematch/fixtures | Returns prematch fixtures for the requested sport_id. |
| Live or prematch drops | mode=live|prematch | Chooses the corresponding detected-drop buffer. |
| Drop detection age | max_age_sec=900 | Keeps drops detected within the last 15 minutes; not start-time distance. |
| Only events already started | live=1 on REST drops | Excludes drops whose scheduled starts value is still in the future. |
| Maximum time until kickoff | Client-side | Read starts/start_ts, calculate minutes, then filter in your application. |
For a start-time window, query a prematch board and apply your own bounds. A request such as max_age_sec=3600 would answer “which drops were detected in the last hour?” It would not answer “which events start in the next hour?”
max_time_to_start, start_before and minutes_to_start are not part of the current PinnWire REST contract. Sending one will not create a kickoff filter; filter the normalized response instead.Try a prematch board with the public demo
Use key=demo for a quick request. The public demo is shared and capped; use a free emailed trial key for an integration that refreshes regularly.
curl "https://pinnwire.com/kit/v1/prematch/fixtures?sport_id=1&key=demo&fresh=start-window-1"
The response includes generated_at at the top level and normalized event rows. fresh is an optional harmless cache-buster; it does not change the selection and is useful when diagnosing a cache.
{
"sport_id": 1,
"last": 1787945400000,
"generated_at": "2026-08-26T10:05:00.000Z",
"events": [{
"event_id": 1634696920,
"starts": "2026-08-28T19:30:00Z",
"start_ts": "2026-08-28T19:30:00Z",
"event_type": "prematch",
"home": "Home FC",
"away": "Away FC"
}]
}
Parse UTC, epoch seconds and milliseconds safely
A normalized PinnWire start_ts is an ISO string, but a client may also receive a numeric start from a raw record, a stored copy or another adapter. Normalize the unit at the boundary. Never parse an ISO time with the browser's local timezone or guess that every number is milliseconds.
function parseStartMs(value) {
if (value === null || value === undefined || value === "") return null;
if (typeof value === "number" || /^[-+]?\d+(?:\.\d+)?$/.test(String(value).trim())) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
// Epoch seconds are about 1e9; epoch milliseconds are about 1e12.
const ms = Math.abs(n) < 1e11 ? n * 1000 : n;
return Number.isFinite(ms) ? ms : null;
}
const ms = Date.parse(String(value));
return Number.isFinite(ms) ? ms : null;
}
function startValue(record) {
// Normalized event shape first; startTime supports a raw-record adapter.
return record.start_ts ?? record.starts ?? record.startTime ?? null;
}
function minutesToStart(record, nowMs = Date.now()) {
const startMs = parseStartMs(startValue(record));
return startMs === null ? null : (startMs - nowMs) / 60000;
}
This parser treats a numeric value below 1e11 as epoch seconds and a larger value as epoch milliseconds. PinnWire's normalized event strings already carry the Z UTC marker, so Date.parse produces the same instant on every server timezone.
null, not “starts now.” If your product cannot safely classify the event, leave it out of a time-bounded queue and log the missing field for inspection.Filter prematch odds by start time
The following client policy keeps events scheduled between minMinutes and maxMinutes from now. It recognizes live rows, rejects missing times, and makes the boundary inclusive. Re-run it after every fresh response.
function streamOf(record) {
if (record.event_type === "live" || record.is_live === true) return "live";
if (record.event_type === "prematch" || record.is_live === false) return "prematch";
return "unknown";
}
function filterByStart(records, {
minMinutes = 0,
maxMinutes = 48 * 60,
nowMs = Date.now(),
allowOverdueMinutes = 0
} = {}) {
// A grace window is meaningful for the default future-only policy. If a
// caller supplies a positive minMinutes, that explicit lower bound wins.
const lowerBound = minMinutes === 0
? -Math.max(0, allowOverdueMinutes)
: minMinutes;
return records.flatMap((record) => {
const stream = streamOf(record);
const minutes = minutesToStart(record, nowMs);
if (stream === "live") return [];
if (minutes === null) return [];
// Negative time means the scheduled start has passed. A small explicit
// grace is allowed for delayed schedules; the default is future only.
if (minutes < lowerBound || minutes > maxMinutes) return [];
return [{ record, minutesToStart: minutes }];
});
}
const response = await fetch(
"https://pinnwire.com/kit/v1/prematch/fixtures?" +
new URLSearchParams({ sport_id: "1", key: "YOUR_KEY", fresh: String(Date.now()) })
);
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || payload.error || response.status);
const upcoming48h = filterByStart(payload.events ?? [], {
minMinutes: 0,
maxMinutes: 48 * 60
});
console.log({ generated_at: payload.generated_at, count: upcoming48h.length });
To include a five-minute delayed-start grace, pass allowOverdueMinutes: 5. That is an explicit product choice, not evidence that the event is still prematch. Keep live rows in a separate queue whenever the API marks them live.
Choose a window that matches the job
0–6 hours
Useful for a near-term board or a workflow that refreshes often. Recheck close to the lower boundary because a schedule can move.
0–48 hours
A practical starting window for an upcoming-events view. Measure volume and adjust instead of assuming every sport has the same cadence.
0–7 days
Useful for broader research, but keep the response current and remember that an event's time-to-start changes continuously.
Join dropping odds to current prematch fixtures
A REST drop already includes event_id, starts and is_live, so a simple client can filter the drop directly. For a schedule-sensitive workflow, join the drop to a freshly fetched fixture: the fixture response is the better source when a start time has been corrected since the drop was detected.
curl "https://pinnwire.com/api/drops?mode=prematch&sport_id=1&min_drop_pct=2&max_age_sec=900&limit=100&key=demo&fresh=drop-start-1"
Here max_age_sec=900 means “detected in the last 15 minutes.” It is deliberately separate from the later client-side 48-hour kickoff window.
const API = "https://pinnwire.com";
const KEY = "YOUR_KEY";
async function getJSON(path, params) {
const url = new URL(path, API);
url.search = new URLSearchParams({ ...params, key: KEY, fresh: String(Date.now()) });
const res = await fetch(url);
const body = await res.json();
if (!res.ok) throw new Error(body.message || body.error || res.status);
return body;
}
async function prematchDropsWithin({ sportId, maxMinutes = 48 * 60 }) {
const [dropBody, fixtureBody] = await Promise.all([
getJSON("/api/drops", {
mode: "prematch", sport_id: String(sportId),
min_drop_pct: "2", max_age_sec: "900", limit: "100"
}),
getJSON("/kit/v1/prematch/fixtures", { sport_id: String(sportId) })
]);
const fixtures = new Map(
(fixtureBody.events ?? []).map((event) => [String(event.event_id), event])
);
const nowMs = Date.now();
return (dropBody.drops ?? []).flatMap((drop) => {
const fixture = fixtures.get(String(drop.event_id));
// If fixture deduplication or a schedule update means the id is not in
// this board, the drop's own starts value is a safe fallback for now.
const current = fixture ?? drop;
const matches = filterByStart([current], {
minMinutes: 0, maxMinutes, nowMs
});
return matches.length ? [{ drop, fixture: fixture ?? null, minutesToStart: matches[0].minutesToStart }] : [];
});
}
const rows = await prematchDropsWithin({ sportId: 1, maxMinutes: 48 * 60 });
for (const row of rows) {
console.log({
event_id: row.drop.event_id,
home: row.fixture?.home ?? row.drop.home,
away: row.fixture?.away ?? row.drop.away,
minutes_to_start: Math.round(row.minutesToStart),
market: row.drop.market,
side: row.drop.side,
from: row.drop.from,
to: row.drop.to
});
}
For an individual event, /kit/v1/details?event_id=N is a useful refresh after a drop. Keep the drop's movement identity—event, market, period, side and points—separate from the fixture's current schedule and names.
age_s for movement recency.Handle live events, negative times and schedule changes
A robust time-to-start filter needs explicit outcomes for records that do not fit a clean future-prematch assumption.
| Record state | Minutes to start | Recommended handling |
|---|---|---|
event_type: "prematch" with a valid future start | 0 or higher | Eligible when it falls inside your configured min/max window. |
event_type: "live" or drop is_live: true | May be negative or stale | Route to the live queue; do not use the prematch time window to relabel it. |
| Prematch row whose start is in the past | Negative | Exclude by default. Add only a documented small grace for delayed starts. |
Missing or invalid starts/start_ts | Unknown | Exclude from a time-bounded queue and log the record for inspection. |
| Start moved later or earlier | Changes on refresh | Recalculate from the newest response; do not pin the old category. |
Why negative time is not automatically live
A negative calculation only says that the scheduled timestamp has passed. The match may be live, delayed, finished, suspended or represented by a stale prematch row. Use the explicit stream marker first, then use the sign of the time difference as a scheduling check. Never infer a score or game state from a negative number.
Re-evaluate after every schedule update
Keep an event keyed by event_id, replace its stored start when a fresher response arrives, and recalculate its bucket. If a start moves from 52 hours away to 20 hours away, it should enter a 48-hour queue on the next refresh. If it moves later, remove it. Near the cutoff, a refresh every 15–60 seconds is usually more useful than a long-lived client-side label.
function updateScheduleBucket(cache, payload) {
const nowMs = Date.now();
for (const event of payload.events ?? []) {
cache.set(String(event.event_id), {
event,
minutesToStart: minutesToStart(event, nowMs),
bucketedAt: nowMs
});
}
return cache;
}
// Run again on the next response. Do not reuse a stale `minutesToStart`.
const cache = updateScheduleBucket(new Map(), fixturePayload);
const eligible = filterByStart([...cache.values()].map((x) => x.event), {
minMinutes: 0,
maxMinutes: 48 * 60
});
Freshness, refresh cadence and longer history
Check the response clock
REST responses include generated_at. It is the response generation time, not the start time and not the age of the source tick. Before showing “next 48 hours,” check that the response is recent enough for your UI.
Use the feed-health clock when needed
/v1/health includes last_odds_update_seconds_ago. Use it to detect a quiet or stale feed separately from the event's scheduled start.
Refresh at the cutoff
Schedule changes and clock boundaries are normal. Poll the fixture surface again before acting on an event that is near your lower or upper threshold.
Persist what you need
PinnWire's detected-drop buffer is recent and in memory, roughly three hours. Store accepted drops and snapshots yourself when you need long-range start-time studies, opening lines or closing-line analysis.
The optional raw PinnWire Pinnacle WebSocket API is useful when your application needs continuous live and prematch market updates. REST fixtures remain the simple choice for periodic start-window refreshes; SSE is the push choice for detected drop alerts.
generated_at and the event's current starts/start_ts value. If a cached response looks stale, re-fetch with a unique fresh value before presenting odds or schedule classifications as current.Why PinnWire fits time-to-start workflows
Choose PinnWire when your application needs current Pinnacle odds and a transparent schedule filter in one focused API. PinnWire normalizes starts and start_ts, separates live from prematch data, exposes detected movement with event identifiers, and gives you REST, SSE and optional raw WebSocket surfaces as your workflow grows.
Filter the full fixture set
Fetch /kit/v1/prematch/fixtures, keep your desired UTC window, then render the current decimal market depth for each eligible event.
Match movement to a schedule
Use /api/drops?mode=prematch, join by event_id, and keep detection age separate from time until kickoff.
Push when a price moves
Use prematch SSE for detected drop alerts, or the optional raw WebSocket when your own store needs every market update.
PinnWire is a read-only real-time Pinnacle odds API: it is not a bookmaker, settlement service or permanent historical archive. Its clear fields and response freshness make it easy to build the time policy in your own application and verify what the policy saw.
Time-to-match-start filter questions
Does PinnWire have a server-side max-time-to-match-start parameter?
No. PinnWire does not expose a max_time_to_start or start_before parameter. Use the prematch fixtures or markets endpoint, parse starts/start_ts, and filter the UTC difference in your client. max_age_sec only filters drop detection age.
What are the start time fields in the PinnWire Pinnacle odds API?
/kit/v1/markets, /kit/v1/prematch/fixtures and /kit/v1/details include both starts and start_ts as the same ISO-8601 UTC value. Compact prematch lines include starts. Drop responses include starts, is_live, alerted and age_s.
Is PinnWire start_ts an epoch timestamp?
Not in the normalized /kit/v1 event shape. It is a backwards-compatible alias for starts and is an ISO-8601 UTC string. A defensive parser can also accept numeric seconds or milliseconds when handling raw or mixed inputs.
How do I filter prematch odds by start time?
Fetch prematch fixtures, parse their start value as UTC, calculate (start_ms - Date.now()) / 60000, and keep events between your minimum and maximum minutes. Recalculate on every response so schedule changes and cutoff crossings are reflected.
How should a client handle a negative time to match start?
A negative value means the scheduled start is in the past. Exclude it from a future prematch queue by default, route an explicitly live event to the live queue, and add only a deliberate small grace period for delayed-start handling.
Can I filter PinnWire drops by time to match start?
Yes, client-side. REST drops include event_id, starts and is_live. Filter the drop directly or join it to a freshly fetched fixture for the latest schedule. Use mode=prematch and keep max_age_sec focused on movement recency.
Can I try the PinnWire start-time filter without a paid plan?
Yes. Use key=demo for a quick REST request, then get a free emailed trial key for private development. The demo is shared and capped; a trial key has its own allowance.
Build a precise Pinnacle start-time filter
Try the current prematch board with key=demo, then use a free trial key for your own schedule-aware integration.