Bet on YesPlay Bet on Hollywoodbets
Bet on YesPlay Bet on Hollywoodbets
ResultsZA Webhook Push: real-time lottery results delivered to your server, application, system or platform

Webhook Payload Reference

Every delivery is a signed HTTP POST with a JSON body. Verify authenticity by computing HMAC-SHA256(raw_request_body, webhook_secret) and comparing to the X-ResultsZA-Signature header. Click any game pill to see its payload.

Official SDKs

Verify webhooks without the crypto

Our SDKs include HMAC-SHA256 signature verification and event parsing, so you can consume ResultsZA webhooks securely without writing the crypto yourself.

Python pip install resultsza-sdk
Node npm install resultsza-sdk

Quick facts

  • Method: HTTPS POST to your endpoint (plain HTTP is rejected).
  • Content-Type: application/json — the body is a single JSON object.
  • Authentication: HMAC-SHA256 signature in X-ResultsZA-Signature (verify against the raw body). Authenticate with the signature, not the source IP. If you need a fixed source IP for a firewall allowlist, contact us.
  • Acknowledge: return any 2xx (200, 201, 202 or 204 are all treated as success) within 10 seconds. The response body can be empty and needs no particular Content-Type.
  • On failure: a non-2xx response, a timeout, or an unreachable endpoint is retried up to 3 times, 10 minutes apart, then marked failed (you get an email and a Delivery Log entry).
  • Timing: South African lottery pushes wait for the official prize breakdown. Every other feed, horse racing included, is sent as soon as we capture the result (see When your endpoint is called).
  • Delivery: at-least-once and not strictly ordered — de-duplicate per draw (see Building your listener for the per-game key).
  • Payload size: deliveries are a few KB. Horse racing sends one race per delivery, not the whole card, so a busy race day means many small POSTs rather than one large one. There is no fixed cap, so don't set your request-body limit too low.
  • Stability: the payload is unversioned and changes are additive only. New fields may be added over time, so ignore unknown fields rather than failing on them.

When your endpoint is called

Which behaviour applies depends on the feed, not on whether the payload contains prize data.

South African lottery games (lotto, lotto plus1, lotto 5 max, powerball, powerball xtra and dailylotto) are held until the official prize breakdown is available, so you receive one payload carrying both the winning numbers and the full division table rather than two partial ones. The breakdown is published by the National Lottery operator after the draw, and it is normally later than the numbers themselves, sometimes by half an hour or more.

Every other feed is POSTed as soon as we capture the result: Ghana, Nigeria, Kenya, UK 49s, EuroMillions, Irish Lotto, US Powerball, Mega Millions and horse racing. Horse racing is worth calling out, because its payload includes tote dividends and is still sent immediately, one delivery per race. Those dividends are published with the result rather than after it.

If you need South African winning numbers as early as possible, poll the API instead. Our results endpoints return the numbers as soon as we capture them, without waiting for the breakdown. Many customers use both: the API for speed, the push for the complete record.

Request Headers

Content-Type: application/json
X-ResultsZA-Signature: sha256=<hmac_hex>
X-ResultsZA-Game: lotto
X-ResultsZA-Event: result.published

Test payloads sent from the settings page use "event": "result.test".

Envelope (all games)

{
  "event": "result.published",
  "game": "<game_id>",
  "country": "<ISO country code>",
  "dispatched_at": "2026-06-25T21:05:00Z",
  "data": { ... }
}

The data object varies by game, see each section below.

FieldTypeNotes
eventstringresult.published for real results. Test sends from Push Settings use result.test — handle or explicitly ignore it so test traffic does not enter your live pipeline.
gamestringLowercase game id, also sent in the X-ResultsZA-Game header (e.g. lotto, us powerball, horse_racing).
countrystringISO-style code: ZA, NG, GH, KE, GB, EU, US.
dispatched_atstringUTC timestamp in ISO 8601, e.g. 2026-06-25T21:05:00Z (the trailing Z denotes UTC).
dataobjectGame-specific result fields, see each section below.

Building your listener

Your endpoint needs to do three things: accept an HTTPS POST, verify the signature against the raw request body, and return a 2xx response within 10 seconds.

Endpoint requirements

  • Reachable over HTTPS (plain HTTP is rejected).
  • Returns a 2xx status within 10 seconds, otherwise the delivery is treated as failed and retried.
  • Do slow work (database writes, downstream calls) after you respond, not before.

Verify the signature

Verify against the raw request bytes, exactly as received. If your framework parses the JSON and you then re-serialise it, the bytes change and the signature will never match. The header value includes the sha256= prefix, so compare against the full string.

Python (Flask):

import hmac, hashlib
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = b"your_webhook_secret"          # from Push Settings

@app.post("/webhooks/resultsza")
def resultsza():
    body = request.get_data()            # raw bytes, before any parsing
    expected = "sha256=" + hmac.new(SECRET, body, digestmod=hashlib.sha256).hexdigest()
    received = request.headers.get("X-ResultsZA-Signature", "")
    if not hmac.compare_digest(expected, received):
        abort(403)
    event = request.get_json()
    # de-dupe on event["game"] + event["data"].get("draw_date") before processing
    return "", 200

Node (Express):

const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = "your_webhook_secret";    // from Push Settings

// express.raw keeps the body as bytes so the signature verifies byte-for-byte
app.post("/webhooks/resultsza",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected = "sha256=" +
      crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const received = req.get("X-ResultsZA-Signature") || "";
    if (expected.length !== received.length ||
        !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
      return res.sendStatus(403);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // de-dupe on event.game + event.data.draw_date before processing
    res.sendStatus(200);
  });

Delivery is at-least-once

Treat every delivery as at-least-once and not strictly ordered. The same result can arrive more than once (for example, if your endpoint is slow and a retry overlaps a late success), so make processing idempotent. De-duplicate per game using a key that is unique per draw:

  • SA Lottery: game + data.draw_number (or data.draw_date).
  • Nigeria (Baba Ijebu) and Ghana: game + data.draw_date + data.draw_time — these run several draws per day, so the date alone is not unique.
  • Kenya, UK 49s, EuroMillions, Irish Lotto, US: game + data.draw_date (each draws at most once per day).
  • Horse racing: data.races_id — one delivery per race, sent the moment that race's result lands, so a race day produces many deliveries. De-duplicating on data.race_date alone would keep the first race of the day and discard the rest of the card.

Retries

A non-2xx response or a timeout is retried up to 3 times, 10 minutes apart. After the final failure the delivery is marked failed, you receive an email, and it appears in the Delivery Log on your Push Settings page.

Testing your endpoint

From Push Settings in your portal, click Test Webhook to send a live, signed sample to your URL (it carries "event": "result.test") so you can confirm reachability and signature verification before any real results arrive. Returning a 2xx to that test is the quickest way to know your listener is wired up correctly.

To see exactly what a payload looks like before you write any code, use a free request-inspection tool like webhook.site (or RequestBin / Beeceptor). Grab the temporary URL it gives you, paste it as your webhook URL in Push Settings, then hit Test Webhook (or wait for a real draw). The tool captures the full request, so you can inspect the exact JSON body, the headers, and the X-ResultsZA-Signature value as they actually arrive. Remember to switch your webhook URL back to your real endpoint when you're done.

Monitoring deliveries

Every dispatch is recorded in the Delivery Log in your portal (Push Settings › Delivery Log). It lists your recent deliveries (paginated, retained for 15 days) with the game, draw date, status (delivered / failed / pending), attempt count, last attempted time and delivered at time, so you can confirm exactly what we sent and diagnose any failures on your side without contacting support.

Game Format Reference

Game game field Country Numbers Range Bonus / Special Format
Daily LottodailylottoZA51-36None (bonus_ball: null)string
LottolottoZA61-52Bonus ball, range 1-52string
Lotto Plus 1lotto plus1ZA61-52Bonus ball, range 1-52string
Lotto 5 Maxlotto 5 maxZA61-52Bonus ball, range 1-52string
PowerballpowerballZA51-50powerball, range 1-16string
Powerball Xtrapowerball xtraZA51-50powerball, range 1-16string
Nigeria
Baba Ijebu (25 draws)premier <name>NG51-90Noneinteger array
Ghana
Ghana Lottery (10 draws)ghana <name>GH51-90Noneinteger array
Kenya
Kenya Lotto (daily)Kenya Lotto <Day>KE60-39Nonestring
kenya mega jackpotkenya mega jackpotKE60-39Nonestring
UK
UK 49s (4 draws/day)uk49s <draw>GB61-49Booster ball (booster_ball), range 1-49string
Europe
euromillionseuromillionsEU51-502 Lucky Stars (lucky_star_1/2), range 1-12string
Irish Lottoirish lottoIE61-47Bonus ball (bonus_ball), range 1-47string
USA
US Powerballus powerballUS51-69Powerball (special_ball), range 1-26string
US Mega Millionsus mega millionsUS51-70Mega Ball (special_ball), range 1-24string
Horse Racing
Horse Racinghorse_racingZAStructured meeting/race/finisher/dividend data for the full racing day-

Format: string = comma-separated e.g. "4, 11, 23"integer array = JSON array e.g. [3, 17, 28]

Game Payloads

SA Lottery

dailylotto lotto lotto plus1 lotto 5 max powerball powerball xtra

Note on timing: these six games are the only ones held until the official prize breakdown is available, so divisions is always populated and the push arrives later than the winning numbers do. See When your endpoint is called, and poll the API if you need the numbers sooner.

{
  "event": "result.published",
  "game": "dailylotto",
  "country": "ZA",
  "dispatched_at": "2026-06-25T18:48:26Z",
  "data": {
    "draw_number": "2657",
    "draw_date": "2026-06-24",
    "winning_numbers": "05, 07, 19, 24, 35",
    "bonus_ball": null,
    "jackpot": 380000,
    "divisions": [
      { "division": "DIV 1", "match": "MATCH 5", "winners": 2,     "prize": 177684.8 },
      { "division": "DIV 2", "match": "MATCH 4", "winners": 327,   "prize": 310.5 },
      { "division": "DIV 3", "match": "MATCH 3", "winners": 9046,  "prize": 16.8 },
      { "division": "DIV 4", "match": "MATCH 2", "winners": 85994, "prize": 4.7 }
    ]
  }
}
FieldTypeNotes
draw_numberstringOfficial draw number. Always a numeric string (e.g. "2657"), not a JSON integer, so parse it before any numeric comparison.
draw_datestringYYYY-MM-DD
winning_numbersstring Comma-separated, zero-padded to two digits, e.g. "05, 07, 19, 24, 35".
dailylotto: 5 balls, range 1-36
lotto / lotto plus1 / lotto 5 max: 6 balls, range 1-52
powerball / powerball xtra: 5 balls, range 1-50
bonus_ballinteger or null dailylotto: always null (no bonus ball)
lotto / lotto plus1 / lotto 5 max: bonus ball, range 1-52
Not present for powerball / powerball xtra — use powerball instead
powerballintegerpowerball / powerball xtra only. Range 1-16
jackpotnumber or nullNext jackpot estimate in ZAR (integer); null if not yet known
divisionsarray or nullPrize breakdown; null if not yet published. Each item: division (string, e.g. "DIV 1"; prefixed per game, e.g. "Plus1 DIV 1", "5 MAX DIV 1", "XTRA DIV 1"), match (string, e.g. "MATCH 5", "MATCH 5 + BONUS", "MATCH PowerBall"), winners (integer), prize (number, ZAR)

Nigeria (Baba Ijebu)

premier 06 premier aseda premier bingo premier bonanza premier club master premier diamond premier enugu premier fairchance premier fortune premier gold premier international premier jackpot premier king premier lucky premier lucky g premier mark ii premier metro premier midweek premier msp premier national premier peoples premier royal premier super premier tota premier vag
{
  "event": "result.published",
  "game": "premier 06",
  "country": "NG",
  "dispatched_at": "2026-06-25T18:50:04Z",
  "data": {
    "draw_date": "2026-06-23",
    "draw_time": "12:45",
    "numbers": [35, 85, 88, 8, 57]
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
draw_timestring or nullHH:MM (WAT, Nigeria local time)
numbersarray of integers5 numbers, range 1-90 (order as drawn, not sorted)

Ghana Lottery

ghana monday special ghana lucky tuesday ghana mid-week ghana fortune thursday ghana friday bonanza ghana national weekly ghana sunday aseda ghana vag lotto ghana noon rush ghana daywa 5/39
{
  "event": "result.published",
  "game": "ghana monday special",
  "country": "GH",
  "dispatched_at": "2026-06-25T18:53:14Z",
  "data": {
    "draw_date": "2026-06-23",
    "draw_time": "20:00",
    "numbers": [43, 32, 45, 77, 11]
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
draw_timestring or nullHH:MM (GMT; Ghana is UTC+0)
numbersarray of integers5 numbers, range 1-90 (order as drawn, not sorted)

Kenya Lotto

kenya lotto monday kenya lotto tuesday kenya lotto thursday kenya lotto friday kenya lotto sunday kenya mega jackpot
{
  "event": "result.published",
  "game": "kenya lotto monday",
  "country": "KE",
  "dispatched_at": "2026-06-25T18:53:14Z",
  "data": {
    "draw_date": "2026-06-22",
    "draw_time": "23:00",
    "winning_numbers": "12, 21, 22, 29, 36, 37"
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
draw_timestring or nullHH:MM (EAT, Kenya local time)
winning_numbersstring6 numbers, range 0-39, comma-separated (not zero-padded)

UK 49s

uk49s brunchtime uk49s lunchtime uk49s drivetime uk49s teatime
{
  "event": "result.published",
  "game": "uk49s brunchtime",
  "country": "GB",
  "dispatched_at": "2026-06-25T15:05:00Z",
  "data": {
    "draw_date": "2026-06-25",
    "winning_numbers": "7, 10, 11, 17, 26, 35",
    "booster_ball": 49
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
winning_numbersstring6 numbers, range 1-49, comma-separated
booster_ballintegerRange 1-49

euromillions

euromillions
{
  "event": "result.published",
  "game": "euromillions",
  "country": "EU",
  "dispatched_at": "2026-06-25T18:53:16Z",
  "data": {
    "draw_date": "2026-06-23",
    "winning_numbers": "3, 33, 36, 45, 46",
    "lucky_star_1": 5,
    "lucky_star_2": 6,
    "jackpot": 47884519.6
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
winning_numbersstring5 numbers, range 1-50, comma-separated (not zero-padded)
lucky_star_1integerFirst Lucky Star, range 1-12
lucky_star_2integerSecond Lucky Star, range 1-12
jackpotnumber or nullJackpot in EUR; may be a decimal (e.g. 47884519.6); null if not yet published

Draws every Tuesday and Friday at ~21:00 CET (23:00 SAST in summer).

irish lotto

irish lotto
{
  "event": "result.published",
  "game": "irish lotto",
  "country": "IE",
  "dispatched_at": "2026-06-27T19:05:11Z",
  "data": {
    "draw_date": "2026-06-27",
    "winning_numbers": "12, 27, 29, 38, 43, 46",
    "bonus_ball": 25,
    "jackpot": 5181046.0
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD
winning_numbersstring6 numbers, range 1-47, comma-separated (not zero-padded)
bonus_ballinteger or nullBonus ball, range 1-47 (auto-drawn from the same pool)
jackpotnumber or nullJackpot in EUR; may be a decimal; null if not yet published

Draws every Wednesday and Saturday at ~18:45 UTC (20:45 SAST).

US Lottery

us powerball us mega millions
{
  "event": "result.published",
  "game": "us powerball",
  "country": "US",
  "dispatched_at": "2026-06-25T18:58:32Z",
  "data": {
    "draw_date": "2026-06-24",
    "winning_numbers": "13, 14, 16, 21, 38",
    "special_ball": 14,
    "multiplier": 2
  }
}
FieldTypeNotes
draw_datestringYYYY-MM-DD (ET draw date)
winning_numbersstring5 numbers (us powerball: range 1-69; us mega millions: range 1-70), comma-separated
special_ballintegerus powerball: range 1-26. us mega millions (Mega Ball): range 1-24
multiplierinteger or nullPower Play / Megaplier; null if not available

us powerball: Mon / Wed / Sat. us mega millions: Tue / Fri. Results arrive ~05:00 SAST the morning after the draw.

Horse Racing

horse_racing

One delivery per race, sent the moment that race's result lands, so a race day produces many deliveries. De-duplicate on data.races_id. The day's full card as a single message is the email alert, not the push.

{
  "event": "result.published",
  "game": "horse_racing",
  "country": "ZA",
  "dispatched_at": "2026-06-25T19:14:32Z",
  "data": {
    "race_date": "2026-06-25",
    "races_id": 184392,
    "venue": "Vaal",
    "track_code": "V",
    "province": "G",
    "race_no": 1,
    "race_name": "Maiden Juvenile Plate",
    "race_class": "MJP",
    "distance": 1200,
    "going": "g",
    "off_time": "12:00:00",
    "win_time": 69.26,
    "stake_total": 125000,
    "races_scraped": 1,
    "finishers": [
      {
        "finish": 1,
        "horse_name": "Summerfest",
        "jockey": "C Zackey",
        "trainer": "SG Tarry",
        "tote_win": 6.9,
        "tote_place": 1.8,
        "sp": "8/1"
      },
      {
        "finish": 2,
        "horse_name": "Molten Rock",
        "jockey": "G Lerena",
        "trainer": "JAJ v Vuuren",
        "tote_win": null,
        "tote_place": 1.0,
        "sp": "87/100"
      },
      {
        "finish": 3,
        "horse_name": "Cape Fear",
        "jockey": "S Veale",
        "trainer": "PA Peter",
        "tote_win": null,
        "tote_place": 2.4,
        "sp": "11/2"
      }
    ],
    "dividends": [
      { "bet_type": "Swinger",  "selections": "1-2",   "dividend": 12.5 },
      { "bet_type": "Exacta",   "selections": "1-2",   "dividend": 41.3 },
      { "bet_type": "Trifecta", "selections": "1-2-3", "dividend": 268.7 }
    ]
  }
}
race_datestringYYYY-MM-DD, the racing day this race belongs to
races_idintegerUnique id for this race. Use it as your de-duplication key.
venuestringRacecourse name, e.g. "Vaal", "Greyville"
track_codestringShort venue code, e.g. "V" (Vaal), "D" (Greyville)
provincestringProvince code, e.g. "G", "N"
race_nointegerRace number on the card
race_namestringOfficial race name / conditions
race_classstringRace classification as published, e.g. "MJP", "MR66", "Cnd", "MR110"
distanceintegerRace distance in metres
goingstring or nullTrack condition code as published, e.g. "g" (often null)
off_timestring or nullScheduled start time HH:MM:SS
win_timenumber or nullWinning time in seconds, e.g. 69.26 (null if not published)
stake_totalinteger or nullTotal prize money in ZAR (null if not published)
races_scrapedintegerAlways 1 on a push, since each delivery is one race. Present for consistency with the email payload; safe to ignore.
finishersarrayTop 3 finishers (1st, 2nd, 3rd)
finishers[].finishintegerFinishing position (1, 2, or 3)
finishers[].horse_namestringHorse name
finishers[].jockeystring or nullJockey name
finishers[].trainerstring or nullTrainer name
finishers[].tote_winfloat or nullTote win dividend, only set for finish=1
finishers[].tote_placefloat or nullTote place dividend
finishers[].spstring or nullStarting price, e.g. "7/1"
dividendsarrayPool dividends for this race
dividends[].bet_typestringTote pool name, e.g. "Swinger", "Exacta", "Trifecta", "Quartet", "Double", "Pick 3", "Pick 6", "Jackpot", "Bipot", "Place Accumulator". Win/place payouts are on each finisher's tote_win/tote_place, not here.
dividends[].selectionsstringWinning combination for the pool, e.g. "1-2-3"
dividends[].dividendfloatPayout for a R1 unit on that pool

Questions? Contact info@resultsza.co.za