DEV Community

Cover image for Live horse-racing odds in one HTTP call
0xGollum
0xGollum

Posted on

Live horse-racing odds in one HTTP call

The 14:15 at Vincennes, ten minutes before the off. A 50/1 outsider is suddenly 20/1. In a pari-mutuel pool that means one thing: money is coming in on that horse, fast. If your bot finds out two minutes later, after a scraper run has spun up, fetched and finished, the price has already moved again.

So I put the odds behind an endpoint that is already awake.

What you get

Horse Racing Pulse API is a Server Actor on Apify: an always-on HTTP API over the live PMU programme (French tracks plus the foreign tracks PMU takes bets on). It refreshes every race still open for betting once a minute and keeps 90 minutes of snapshots in memory, so a call is answered in milliseconds.

GET /movers?countries=FRA&window_minutes=10 returns the horses the money is moving on:

{
  "updated_at": "2026-11-11T07:45:24+00:00",
  "window_minutes_used": 10.0,
  "mover_count": 1,
  "movers": [{
    "signal": "steamer", "trigger": "relative",
    "hippodrome": "LYON LA SOIE", "race_code": "R4C2", "race_name": "OURASI",
    "horse_num": 6, "horse_name": "JUAN LES PINS",
    "current_cote": 16.0, "previous_prob_pct": 2.5, "current_prob_pct": 6.25,
    "relative_change_pct": 150.0
  }]
}
Enter fullscreen mode Exit fullscreen mode

A move counts when the implied probability shifts by 5 points or by 30 % of its previous value. The second rule is the one that matters for outsiders: 50/1 to 20/1 is only three points, but it is a +150 % move, and it is exactly the kind of steam people want to see.

GET /races returns every open race (venue, R4C2 code, start time, discipline, distance, prize, number of runners) with the odds of each runner, favourite first.

Calling it

curl -H "Authorization: Bearer $APIFY_TOKEN" \
  "https://0xgollum--horse-racing-pulse-api.apify.actor/races?disciplines=PLAT&max_races=5"
Enter fullscreen mode Exit fullscreen mode
import httpx

r = httpx.get(
    "https://0xgollum--horse-racing-pulse-api.apify.actor/movers",
    params={"countries": "FRA", "window_minutes": 15, "start_before": "16:00"},
    headers={"Authorization": f"Bearer {token}"},
)
for m in r.json()["movers"]:
    print(m["race_code"], m["horse_name"], m["signal"], m["relative_change_pct"])
Enter fullscreen mode Exit fullscreen mode

Nineteen filters work as query parameters: country, meeting, discipline, category (HANDICAP, GROUPE_I…), surface, going, distance, prize money, number of runners, start-time window, and min/max odds per runner.

Why an always-on server changes things

With a classic batch scraper, "how did the odds move in the last 10 minutes?" means scheduling runs and storing the previous one yourself. Here the server already remembers the last 90 minutes, so the question is a single GET. One poller refreshes the cache for everyone, and your answer never waits on the source.

The first call after a quiet period wakes the server in a few seconds; after that, answers are immediate. A request that fails, or arrives while the cache is warming up, is never billed.

Try it here: Horse Racing Pulse API on Apify

Odds are informational, not betting advice. Not affiliated with PMU.

Top comments (0)

⌂ 换网址