Skip to content

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.

Code example
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:

Code example
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.

EventFires when
draw.publishedA new result is recorded
draw.revisedA recorded result's numbers were corrected
draw.confirmedA result first published live (provisional: true) was confirmed by the operator with the same numbers
jackpot.updatedA jackpot figure moved without the numbers changing, on a draw from the last 48 hours or the game's newest
draw.missedAn expected draw did not arrive by its watchdog window
lottery.pausedA game's operator stopped publishing results; its draws are not reported as missed meanwhile
lottery.resumedA 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.

Code example
X-Lottevo-Signature: t=1758374400,v1=5d41402abc4b2a76b9719d911017c592…

v1 is HMAC-SHA256 over {t}.{raw body} with your endpoint secret.

Code example
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:

Code example
curl -X POST https://api.lottevo.com/v1/webhooks/{id}/test -H 'X-Api-Key: …'
Code example
{ "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.

Code example
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.

Code example
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.