/v1/resultsResults
Finishing positions, margins and official starting prices for completed races. Takes the same parameters as Race Form, so any race you fetched form for can be looked up here with the identical query string — fetch a single race, or a whole meeting's card in one call.
Pro only
Result Freshness
On Free and Pro, results are ingested by a pipeline run every 2–3 hours, not streamed in real time. Expect a lag between a race finishing and its result appearing here.
On Premium and Enterprise, results arrive as races settle rather than waiting for that run, and each early position says how settled it is via resultStage — interim or correct_weight. Scratchings, track condition and weather are live on these tiers too, so a race card reflects a scratching before the batch pipeline has seen it.
Use the race-level resultStatus field to tell a race that hasn't run yet from one that has run but isn't ingested yet — don't poll in a tight loop waiting for a result to change. A race that exists but has no result yet returns 200 with resultStatus: "pending", not a 404.
Market Coverage
Results cover every meeting Race Form covers, across gallops, harness and greyhounds. How a result is sourced varies by market, and so does what it carries:
- Where the batch pipeline holds the race, positions arrive with
marginand, where published, the officialstartingPrice. - Elsewhere the result comes from bookmaker results feeds, which report placings but not margins, so those results carry no
margin. Premium sees them as races settle; Pro receives the confirmed (correct_weight) result once a race is two hours past its start with no batch result.
Coverage deepens as sources are added. Treat margin and startingPrice as optional on every runner.
Parameters
datestringYYYY-MM-DD format. Range: 30 days in the past to 7 days in the future.trackstringraceinteger/meeting variant to return every race at the meeting.race_codestringgallopsgallops, harness, or greyhounds.countrystringautimezonestringAustralia/SydneyPacific/Auckland, Asia/Hong_Kong). The date you provide is interpreted in this timezone, so overseas meetings match your local calendar date. Defaults to Australian Eastern time. This scopes which races you get — it does not shift the times in the response. startTime is always UTC regardless of what you pass here; use the timezone response field to render a local post time.Two Endpoints
/v1/resultsA single race. Requires date, track and race. Returns a RaceResult object.
/v1/results/meetingEvery race at a meeting, in race-number order — one call instead of looping over race numbers. Omit race. Races still awaiting results appear with resultStatus: "pending" rather than being omitted, so the card is always complete.
Code Examples
// Single race
const response = await fetch(
'https://api.formfav.com/v1/results?' +
new URLSearchParams({
date: '2026-07-25',
track: 'caulfield',
race: '1'
}),
{
headers: { 'X-API-Key': 'your_api_key_here' }
}
);
const race = await response.json();
if (race.resultStatus === 'final') {
race.placings
.filter(p => p.position != null)
.forEach(p => console.log(`${p.position}. ${p.name} (${p.margin})`));
}Result Status
The race-level resultStatus field tells you how complete a race's result is:
| Status | Meaning |
|---|---|
final | Every non-scratched runner has a result |
interim | Results are partially in — some runners resulted, some not yet |
pending | No results yet — the race may not have been run, or hasn't been ingested |
abandoned | The race was abandoned — no result will ever exist |
Result Stages
On Premium and Enterprise, a finishing position can reach you from the real-time feed before the batch pipeline has the race at all. When it does, that runner carries resultStage saying how settled the position is. It is a per-runner field — placings[].resultStage, not a race-level one.
| Stage | Meaning |
|---|---|
interim | Provisional. The finish has been called but stewards have not confirmed it, so the placings can still change. |
correct_weight | Stewards have confirmed the result. Payouts are settled and the placings will not change. |
An absent resultStage does not mean unconfirmed
resultStage is dropped — so a runner without one has the settled position, not an unconfirmed one. Reading absence as “not yet confirmed” inverts the meaning, and it will read that way for every historical race you fetch.Two consequences of it being a fill-in rather than an override:
- •A runner the batch pipeline has already resulted never carries a stage, and neither does a scratching. Where the two sources both have a position the batch one wins — it comes from the controlling body rather than a bookmaker feed.
- •A position filled in this way carries no
margin. The feed reports the finishing order, not margins; the margin arrives later with the batch result.
interim means two different things
•
resultStatus: "interim" is about the field — some runners are resulted, some are not.•
resultStage: "interim" is about one runner's position — provisional, pending stewards.A race can read
resultStatus: "final" while every runner is still resultStage: "interim": the whole field has been placed, but nothing is stewards-confirmed yet. Do not use either as a proxy for the other. Note also that correct_weight is never a resultStatus value — the two vocabularies do not overlap beyond interim.Abridged, showing a race the feed has called but the batch pipeline has not yet ingested. The placed runners carry a stage and no margin; the scratching carries neither:
{
"raceNumber": 4,
"resultStatus": "final",
"placings": [
{
"number": 6,
"name": "Torrent Bay",
"position": 1,
"scratched": false,
"resultStage": "correct_weight"
},
{
"number": 2,
"name": "Second Reading",
"position": 2,
"scratched": false,
"resultStage": "correct_weight"
},
{
"number": 9,
"name": "Withdrawn Early",
"scratched": true
}
]
}Although the field sits on each runner, the stage is decided once per race — a feed calls a whole race interim or correct-weight, not individual placings. So every live-filled runner in a race carries the same value, and it is safe to read one runner's stage as the race's. Do not build logic that expects the two to differ within a race.
Where several feeds report the same race, a stewards-confirmed result is preferred over a provisional one, then the most recent. A feed that disagrees with the others is excluded outright rather than ranked below them, on the grounds that a known-wrong result is worse than none.
Pro keys never see interim. Their positions come from the batch pipeline or, where it has no result two hours after the start, from the confirmed live result — which carries resultStage correct_weight until a batch position replaces it.
Corrected and withdrawn results
A result can change after it first appears: stewards amend the order, a margin arrives after the placings, or an abandonment clears results that were already recorded. Three race-level fields let you tell which copy of a race is the latest and whether its placings were ever altered.
| Field | Meaning |
|---|---|
resultUpdatedAt | When the result last changed, in UTC. Compare it with the value you stored: a later time means refetch. This is when we recorded the change — the stewards' own publication time is not available to us. |
resultVersion | How many separate writes have touched the result, starting at 1. It orders two copies of the same race. A result normally lands in more than one write (placings, then margins), so a value above 1 does not mean it was corrected. |
resultRevised | true once a placing was changed to a different one, or withdrawn, after it had been recorded. false means we hold a change record and nothing was ever altered. |
Absent means unknown, not unchanged
resultUpdatedAt only — that feed keeps no history, so it has no version or revision flag.A race whose result was withdrawn (for example by an abandonment) still reports the time of the withdrawal, with resultRevised: true and no positions. Corrections show on the next request; a settled result is cached for a few minutes only once it has been unchanged for ten.
Reading a placing
Two fields tell you everything about a runner: position and scratched. There is no outcome field — see the note below if you were using one.
| What you see | What it means |
|---|---|
position present | Ran and finished in that position. margin is present too. |
scratched: true | Withdrawn before the race. Never has a position. |
| Neither | No result: either it did not finish, or the result has not landed yet. Use resultStatus to tell them apart. |
Distinguishing a non-finisher from a pending result
position and scratched: false. When the race reads resultStatus: "final", every unscratched runner has been resulted — so one still without a position did not finish. Non-finishers are rare, roughly ten runners a week across all markets.Dead heats
position in a dead heat, so a finishing position is not unique within a race. Order placings by position and expect ties.outcome has been removed
outcome field with values placed · did_not_finish · scratched · pending. It was removed because placed meant only “completed the race”, which clashes with the racing sense of finishing in the paying places — a runner sixth of six reported outcome: "placed", so filtering on it returned the whole field rather than the placegetters. If you were filtering outcome === 'placed', use position != null; for actual placegetters, use position <= 3.Response Fields
Empty fields are omitted, not null
omitted below is left out of the JSON entirely when it has no value — it is never returned as null. Check for presence ('margin' in runner), not for null, and type these as optional properties rather than nullable ones.datestringtrackstringraceNumberintegerraceNamestring | omitteddistancestring | omittedconditionstring | omittedweatherstring | omittedraceClassstring | omittedabandonedbooleanstartTimestring | omittedtimezone to convert to local venue time.timezonestring | omittedprizeMoneystring | omittednumberOfRunnersintegerplacings, including scratchings — not the number that contested the race. Count runners whose scratched is false for that.resultStatusstringresultStage.resultUpdatedAtstring | omittedresultVersioninteger | omittedresultUpdatedAt, and always on a result served from the real-time feed.resultRevisedboolean | omittedtrue when a placing was changed or withdrawn after being recorded.placingsarrayplacings[].numberintegerplacings[].namestringplacings[].jockeystring | omittedplacings[].trainerstring | omittedplacings[].weightnumber | omittedplacings[].claimnumber | omittedplacings[].barrierinteger | omittedstartRow) or at its handicap mark (handicapMetres).placings[].startRowstring | omittedfront or second. Together with barrier it is the start — second with barrier 1 is Sr1. Omitted for back-markers in a standing start and outside harness.placings[].unrulyboolean | omittedtrue when the runner is drawn in an unruly place in its row or at its mark (FrU3, 20m U1).placings[].handicapMetresinteger | omittedbarrier is then the place at that mark. Omitted on mobile starts and for front-mark runners.placings[].positioninteger | omittedplacings[].marginstring | omittedplacings[].scratchedbooleanplacings[].startingPricenumber | omittedplacings[].resultStagestring | omittedinterim or correct_weight. Premium and Enterprise only, and present only while the real-time feed is ahead of the batch pipeline — see Result Stages. Absence means the position is settled, not unconfirmed.placings[].pricesarray | omittedrunners[].prices. Premium and Enterprise only — absent on other tiers, and absent for a runner with no quote worth serving. Not the same thing as startingPrice above.dividendsarray | omitteddividends[].poolstringdividends[].combinationinteger[]dividends[].dividendnumberdividends[].operatorstringTABdividends[].jurisdictionstringVIC. Pools pay differently for the same result, so a dividend is only meaningful with its pool.The /results/meeting variant wraps a list of these race objects: date, track, slug, country, raceType, abandoned, and a races array.
Tote dividends
On Premium and Enterprise, each race carries dividends: every declared single-race tote pool, including the exotics — Quinella, Exacta, Duet, Trifecta and FirstFour — alongside Win and Place. They arrive once the operator declares them, typically within minutes of correct weight, and never change after that.
A dividend belongs to a pool
Tote pools are separate and pay differently for the same result — one exacta on the same race paid $7.20, $17.90 and $17.20 across three state pools. Every dividend therefore carries operator and jurisdiction. Today these are TAB’s VIC pool; read the label rather than assuming it.
Multi-race pools (Quaddie, Running Double, Treble and so on) are not included: their combinations name runners in other races.
Coverage follows the tote: dividends appear where it runs a pool on the race, and races it does not sell carry no dividends key. Like startingPrice, a dividend is decimal and includes the stake.
Starting price
startingPrice is the official starting price as decimal odds, taken from the racing body's own published result — not from a wagering operator. It is settled after the race and never moves.
It is not a live or pre-race price. There is no price movement, no per-bookmaker breakdown, and no market data before a race is run. If you need those, this endpoint is not the right source.
Coverage is partial — plan for absence
The key is omitted entirely for any runner we do not hold a price for, and that is common today. Coverage is still filling and varies by market, so treat the key as optional everywhere.
Two runners in the same race can legitimately differ in whether they carry one. Do not treat an absent price as a scratching, a zero, or an error — and do not compute a field-wide average from a race unless every runner in it has a price.
Prices are decimal odds including stake, so a $1 win bet on a runner at 4.50 returns $4.50 in total, not $5.50. Winners are typically far shorter than the field average, which is a useful sanity check if you are ingesting these at scale.
Example Response
{
"date": "2026-07-25",
"track": "Caulfield",
"raceNumber": 1,
"raceName": "Le Pine Funerals Handicap",
"distance": "1200m",
"condition": "Good 4",
"weather": "Fine",
"raceClass": "BM70",
"abandoned": false,
"startTime": "2026-07-25T02:30:00Z",
"timezone": "Australia/Melbourne",
"prizeMoney": "130000",
"numberOfRunners": 10,
"resultStatus": "final",
"placings": [
{
"number": 3,
"name": "Custom",
"jockey": "J. McDonald",
"trainer": "C. Waller",
"weight": 58.5,
"barrier": 4,
"position": 1,
"margin": "0.0",
"scratched": false,
"startingPrice": 3.4
},
{
"number": 7,
"name": "Sun Setting",
"jockey": "M. Dee",
"trainer": "G. Waterhouse",
"weight": 56.0,
"barrier": 2,
"position": 2,
"margin": "3.0",
"scratched": false
},
{
"number": 9,
"name": "Late Withdrawal",
"trainer": "P. Moody",
"barrier": 8,
"scratched": true
}
]
}Related
- •Race Form endpoint — runners, form and ratings going into a race (same parameters)
- •Meetings endpoint — discover which tracks ran on a date
- •Errors — how 400 / 403 / 404 responses are shaped