Lottery results look like the easiest data in the world: five or six numbers, twice a week. Then you try to use them in an app and the trouble starts. Every operator publishes them differently. Some have a PDF, some have a page that changes layout every year, some publish late, and a few correct a draw after the fact. Scraping one lottery is a weekend. Keeping fifty of them right is a job.

In this tutorial we'll skip the scraping and read results from a REST API instead. With requests and a free key we'll:

  1. list the latest EuroMillions draws,
  2. check a set of numbers against past draws, and see what they would have won,
  3. find out when the next draw is.

Each step shows its output, and the whole script is at the end, ready to copy and run.

1. Get a key

I'm using Lottevo, a lottery results API I built. Create an account, open API keys and create a sandbox key. It's free and needs no card. It starts with lv_test_ and reads one game, EuroMillions (EU-002), with real results but a delay: the key sees the latest 50 draws that are at least seven days old. So "latest" below means the most recent draws your key can read, not tonight's.

That's enough for everything in this tutorial. A plan key unlocks the games included in your plan and results as soon as they land; statistics, webhooks and the event stream also need a plan key.

Put the key in an environment variable so it never lands in your code:

export LOTTEVO_API_KEY="lv_test_..." pip install requests 

A quick check from the terminal:

curl "https://api.lottevo.com/v1/lotteries/EU-002/draws?per_page=1" \ -H "Authorization: Bearer $LOTTEVO_API_KEY" 

2. A small helper

Every call needs the same header, and requests waits forever unless you give it a timeout, so we wrap both in one function:

import os import requests API = "https://api.lottevo.com/v1" TIMEOUT = 15 session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['LOTTEVO_API_KEY']}" session.headers["Accept"] = "application/json" def get(url, **params): response = session.get(url, params=params, timeout=TIMEOUT) response.raise_for_status() return response.json() 

3. List the latest draws

Every game has a stable code (EU-002 is EuroMillions, RO-001 is Romania's Loto 6/49, and so on). Its results live at /v1/lotteries/{code}/draws, newest first:

def latest_draws(code, count=5): return get(f"{API}/lotteries/{code}/draws", per_page=count)["data"] print("Latest EuroMillions draws") draws = latest_draws("EU-002") for draw in draws: numbers = " ".join(f"{n:2}" for n in draw["sorted_numbers"]) stars = " ".join(f"{n:2}" for n in draw["bonus_numbers"]) print(f" {draw['drawn_at'][:10]} {numbers} ★ {stars}") 

Output (your dates will be newer):

Latest EuroMillions draws 2026-10-13 21 23 39 44 49 ★ 4 11 2026-10-09 5 20 24 31 45 ★ 5 10 2026-10-06 18 22 32 41 48 ★ 8 10 2026-10-02 7 8 10 22 35 ★ 2 10 2026-09-29 4 7 12 31 44 ★ 8 11 

Each draw has the numbers in the order they came out of the machine (numbers) and sorted (sorted_numbers). The extra balls go in bonus_numbers; for EuroMillions those are the two Lucky Stars. drawn_at is an ISO 8601 timestamp in UTC. You'll also find the jackpot (jackpot_amount, plus EUR and USD conversions) and, where the operator publishes them, the prize tiers.

4. Check a set of numbers

This is the part people usually build by hand: compare the numbers with every past draw, count the matches, then look up what that match paid on that draw. The check endpoint does the comparison and the prize lookup for you:

def check_ticket(code, numbers, bonus=(), min_matched=1): page = get( f"{API}/lotteries/{code}/check", numbers=",".join(map(str, numbers)), bonus=",".join(map(str, bonus)), min_matched=min_matched, ) meta, matches = page["meta"], page["data"] while page["links"]["next"]: page = get(page["links"]["next"]) matches += page["data"] return meta, matches 

Two details matter here.

min_matched=1. It sets how many main numbers a draw must share with yours before it's listed. EuroMillions pays for one number plus both stars, so anything higher would hide a winning draw.

Pagination. Matches come back 25 per page, newest first. links.next is the URL of the next page, or null on the last one, so the loop keeps going until every match is collected.

Each match says how many numbers (matched_count) and stars (bonus_matched) it shared, and a prize_status:

  • won: the match is a prize category on that draw. prize gives the category and the amount paid per winner, as a string, so no floating point surprises on money. The amount can be null when nobody won that category on the day.
  • not_won: the draw's prizes are known and none fits.
  • unknown: the API can't tell, for example because the operator hasn't published that draw's prizes yet, or publishes them in a way it can't match.
from decimal import Decimal def money(amount, currency): return f"{Decimal(amount):.2f} {currency}" def describe(match): line = f"{match['drawn_at'][:10]} {match['matched_count']} + {match['bonus_matched'] or 0}" if match["prize_status"] == "won": prize = match["prize"] amount = money(prize["amount"], prize["currency"]) if prize["amount"] else "amount unavailable" return f"{line} → {amount} ({prize['category']})" if match["prize_status"] == "not_won": return f"{line} no prize" return f"{line} prize unknown" 

Numbers picked at random usually win nothing over 50 draws, which makes a dull demo. So for this example we build numbers that are sure to hit: three numbers and one star from the most recent draw, plus two numbers that weren't drawn. Swap in your own numbers once you've seen it work.

latest = draws[0] ticket = latest["sorted_numbers"][:3] + [n for n in range(1, 51) if n not in latest["sorted_numbers"]][:2] stars = latest["bonus_numbers"][:1] print(f"\nChecking the demo ticket {ticket} ★ {stars}") meta, matches = check_ticket("EU-002", ticket, stars) print(f" {meta['draws_checked']} draws checked, {len(matches)} shared at least one number") for match in matches: if match["prize_status"] != "not_won": print(f" {describe(match)}") 

Besides the matches, meta.winnings adds up what those numbers would have won over the whole window, by currency. complete is false when some draws couldn't be priced:

winnings = meta["winnings"] if winnings: totals = ", ".join(money(t["amount"], t["currency"]) for t in winnings["totals"]) or "nothing" print(f" In total: {totals} over {winnings['draws_won']} winning draws") if not winnings["complete"]: print(f" ({winnings['draws_unknown']} draws could not be priced)") 

Output:

Checking the demo ticket [21, 23, 39, 1, 2] ★ [4] 50 draws checked, 16 shared at least one number 2026-10-13 3 + 1 → 12.90 EUR (3+1) 2026-09-11 2 + 0 → 3.90 EUR (2+0) 2026-09-01 2 + 0 → 4.10 EUR (2+0) 2026-07-21 2 + 0 → 3.80 EUR (2+0) 2026-07-17 2 + 0 → 3.70 EUR (2+0) 2026-07-03 2 + 0 → 3.70 EUR (2+0) 2026-06-09 2 + 0 → 3.90 EUR (2+0) In total: 36.00 EUR over 7 winning draws 

Keep in mind what this is: a set of numbers compared with past draws, priced at what each draw paid. It tells you what those numbers would have won, not what a ticket you bought did.

5. When is the next draw?

Draw times are trickier than they look: different time zones, daylight saving, holiday changes. The schedule endpoint gives the next draw as an exact instant in UTC, plus the lottery's own time zone. A game can also have no draw scheduled (between seasons, or before a new game starts), so next_draw_at may be null:

from datetime import datetime from zoneinfo import ZoneInfo def next_draw(code): schedule = get(f"{API}/lotteries/{code}/schedule")["data"] if schedule["next_draw_at"] is None: return None instant = datetime.fromisoformat(schedule["next_draw_at"].replace("Z", "+00:00")) return instant.astimezone(ZoneInfo(schedule["timezone"])) when = next_draw("EU-002") if when: print(f"\nNext draw: {when:%A %d %B, %H:%M} {when.tzinfo}") else: print("\nNo upcoming draw scheduled") 

Output:

Next draw: Friday 30 October, 21:45 Europe/Paris 

6. Handling errors

When the API refuses a call, it answers with a stable code you can branch on. For example, the sandbox key asking for another game:

try: get(f"{API}/lotteries/RO-001/draws") except requests.HTTPError as error: print(error.response.status_code, error.response.json()["code"]) 
403 lottery_not_entitled 

Branch on code, not on message: the message is translated and may change.

The whole script

Everything above in one file. Save it as euromillions.py and run python euromillions.py.

euromillions.py
import os from datetime import datetime from decimal import Decimal from zoneinfo import ZoneInfo import requests API = "https://api.lottevo.com/v1" TIMEOUT = 15 session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['LOTTEVO_API_KEY']}" session.headers["Accept"] = "application/json" def get(url, **params): response = session.get(url, params=params, timeout=TIMEOUT) response.raise_for_status() return response.json() def latest_draws(code, count=5): return get(f"{API}/lotteries/{code}/draws", per_page=count)["data"] def check_ticket(code, numbers, bonus=(), min_matched=1): page = get( f"{API}/lotteries/{code}/check", numbers=",".join(map(str, numbers)), bonus=",".join(map(str, bonus)), min_matched=min_matched, ) meta, matches = page["meta"], page["data"] while page["links"]["next"]: page = get(page["links"]["next"]) matches += page["data"] return meta, matches def money(amount, currency): return f"{Decimal(amount):.2f} {currency}" def describe(match): line = f"{match['drawn_at'][:10]} {match['matched_count']} + {match['bonus_matched'] or 0}" if match["prize_status"] == "won": prize = match["prize"] amount = money(prize["amount"], prize["currency"]) if prize["amount"] else "amount unavailable" return f"{line} → {amount} ({prize['category']})" if match["prize_status"] == "not_won": return f"{line} no prize" return f"{line} prize unknown" def next_draw(code): schedule = get(f"{API}/lotteries/{code}/schedule")["data"] if schedule["next_draw_at"] is None: return None instant = datetime.fromisoformat(schedule["next_draw_at"].replace("Z", "+00:00")) return instant.astimezone(ZoneInfo(schedule["timezone"])) print("Latest EuroMillions draws") draws = latest_draws("EU-002") for draw in draws: numbers = " ".join(f"{n:2}" for n in draw["sorted_numbers"]) stars = " ".join(f"{n:2}" for n in draw["bonus_numbers"]) print(f" {draw['drawn_at'][:10]} {numbers} ★ {stars}") latest = draws[0] ticket = latest["sorted_numbers"][:3] + [n for n in range(1, 51) if n not in latest["sorted_numbers"]][:2] stars = latest["bonus_numbers"][:1] print(f"\nChecking the demo ticket {ticket} ★ {stars}") meta, matches = check_ticket("EU-002", ticket, stars) print(f" {meta['draws_checked']} draws checked, {len(matches)} shared at least one number") for match ...
Source:https://dev.to/astefalabs/get-euromillions-results-and-check-your-numbers-with-python-3635