Integration guides
Updates, on your terms.
Choose the delivery method that fits your application.
Poll the feed
Simple and cheap with ETags — a poll that changed nothing is a 304. The right choice for a
daily import or a page that refreshes.
curl 'https://api.lottevo.com/v1/draws?filter[lottery]=EU-001' \
-H 'X-Api-Key: …' \
-H 'If-None-Match: "…"'
Subscribe to webhooks
Requires webhooks:manage, included in every plan and during the trial. Register an https endpoint for the events
you want and, optionally, only the games you care about:
curl -X POST https://api.lottevo.com/v1/webhooks \
-H 'X-Api-Key: …' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://yourapp.example/hooks/lottevo",
"events": ["draw.published", "jackpot.updated"],
"lottery_codes": ["EU-001", "RO-001"]
}'
The signing secret comes back on that response and never again. Rotate it with
POST /v1/webhooks/{id}/rotate-secret if it leaks.
| Event | Fires when |
|---|---|
draw.published | A new result is recorded |
draw.revised | A recorded result's numbers were corrected |
draw.confirmed | A result first published live (provisional: true) was confirmed by the operator with the same numbers |
jackpot.updated | A jackpot figure moved without the numbers changing, on a draw from the last 48 hours or the game's newest |
draw.missed | An expected draw did not arrive by its watchdog window |
lottery.paused | A game's operator stopped publishing results; its draws are not reported as missed meanwhile |
lottery.resumed | A paused game's operator is publishing results again |
After a draw.missed
We keep retrying, and a daily pass recovers what is still missing from the operator's archive.
Every draw recovered from inside the outage you were told about reaches you as draw.published,
like any other draw, with "recovered": true and recovered_at next to draw. A draw that
arrives as it happens carries "recovered": false. History we import from an archive is not
sent; GET /v1/draws/changes lists that too.
When a game is paused
When an operator stops publishing (its site is down for days, or the game is not being drawn), we
pause the game instead of sending a draw.missed for every slot. lottery.paused carries
lottery_code, reason (operator_not_publishing) and since; the game's paused field
on GET /v1/lotteries/{code} says the same while it lasts. Once the operator publishes again,
lottery.resumed adds resumed_at, and we look in its archive for any draw published meanwhile;
the slots it never drew stay out of the completeness figures.
Verifying a delivery
Each delivery is a POST carrying id, event, created_at and data, with headers
X-Lottevo-Event, X-Lottevo-Delivery and X-Lottevo-Signature.
X-Lottevo-Signature: t=1758374400,v1=5d41402abc4b2a76b9719d911017c592…
v1 is HMAC-SHA256 over {t}.{raw body} with your endpoint secret.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(header, body, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')))
const timestamp = Number(parts.t)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')
const a = Buffer.from(parts.v1, 'utf8')
const b = Buffer.from(expected, 'utf8')
return a.length === b.length && timingSafeEqual(a, b)
}
Check the timestamp too. A receiver that verifies only the HMAC accepts a replay of any request it ever received. And verify against the raw body — re-serialising the JSON can reorder keys and break the signature.
X-Lottevo-Delivery is stable across retries, so it is your idempotency key.
Test event
When you subscribe, and whenever you point a subscription at a new URL, we send it one
webhook.ping, signed like every other delivery. If your server does not answer with a 2xx, we
email you what it answered, before any real event is lost. Send another one at any time:
curl -X POST https://api.lottevo.com/v1/webhooks/{id}/test -H 'X-Api-Key: …'
{ "data": { "delivered": false, "status_code": 404, "error": "The receiver answered 404.", "duration_ms": 182 } }
A test is not a delivery: it does not appear in the delivery log and does not count towards the failures that switch a subscription off. Answer it with any 2xx, as you would an event you do not handle.
Retries
Anything but a 2xx is a failure, a redirect included. Six attempts on a widening backoff —
10s, 1m, 5m, 15m, 1h — then the delivery goes dead and appears in
GET /v1/webhooks/{id}/deliveries, from where you can replay it.
After 20 consecutive failed deliveries the subscription is switched off and you are notified;
PATCH /v1/webhooks/{id} with "active": true turns it back on.
Open the live stream
Requires stream:read, from the Pro plan up and during the trial. GET /v1/stream is Server-Sent Events.
id: 10482
event: draw.published
data: {"draw":{"id":10482,"lottery_code":"EU-001","numbers":[7,16,24,38,45],…}}
The frame id is your resume point: Last-Event-ID is honoured, so a reconnect resumes rather
than replaying or skipping. Narrow with filter[lottery] or the location filters; with no
filter you get exactly the games your key holds, not the firehose.
A connection lives up to 100 seconds and then sends stream.reconnect — every EventSource
reconnects on its own. Keep-alive comments arrive every 20 seconds. Up to 2 concurrent
connections per account, across all its keys.
EventSource cannot set headers, so it cannot send X-Api-Key. Consume the stream from
your server and fan it out to browsers yourself.
Results added to the history later are never streamed: a stream carries what just happened, not history being written.
Download a bulk export
Requires exports:read, on the Enterprise plan. An export is the whole history your plan
includes for one lottery your key holds, in one gzip-compressed file, oldest draw first.
POST /v1/exports
{"lottery": "RO-001", "format": "csv"}
Use ndjson instead of csv for one JSON object per line. An NDJSON line is the draw object of
GET /v1/draws/{id}, prize categories included; CSV carries the same fields without the
categories, with lists separated by spaces.
The answer is 202 with the export's id and status: "building". Poll
GET /v1/exports/{id} until status is ready, then follow download_url. A game with
millions of draws takes a few minutes. Draws held after as_of are not in the file, so it is
one consistent snapshot: follow it with GET /v1/draws/changes to stay current.
The download link works until expires_at, so treat it as a secret. Check the file against
sha256. Every download is recorded.
Each export counts against the account's monthly allowance, across all its keys, whether you
download it or not; an export that fails does not count. GET /v1/exports lists your key's
exports and says under meta how many are left this month and when the allowance renews.
You can also take exports from the console, under Exports.