DEV Community

Astefa Labs
Astefa Labs

Posted on AI-assisted

Get EuroMillions results and check your numbers with Python

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)}")
Enter fullscreen mode Exit fullscreen mode

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)")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Output:

Next draw: Friday 30 October, 21:45 Europe/Paris
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode
403 lottery_not_entitled
Enter fullscreen mode Exit fullscreen mode

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 in matches:
    if match["prize_status"] != "not_won":
        print(f"  {describe(match)}")

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)")

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")
Enter fullscreen mode Exit fullscreen mode

Going further

  • A client library. pip install lottevo gives you the same calls as methods (lottevo.lotteries.draws("EU-002", {"per_page": 5})), with refusals raised as LottevoError. There are also PHP and JavaScript clients.
  • The reference. Every endpoint and field is documented at lottevo.com/docs.

If you build something with it, or something in the API gets in your way, I'd like to hear about it in the comments.

Top comments (0)