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
Our SDKs include HMAC-SHA256 signature verification and event parsing, so you can consume ResultsZA webhooks securely without writing the crypto yourself.
pip install resultsza-sdk
npm install resultsza-sdk
POST to your endpoint (plain HTTP is rejected).application/json — the body is a single JSON object.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.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.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.
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".
{
"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.
| Field | Type | Notes |
|---|---|---|
| event | string | result.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. |
| game | string | Lowercase game id, also sent in the X-ResultsZA-Game header (e.g. lotto, us powerball, horse_racing). |
| country | string | ISO-style code: ZA, NG, GH, KE, GB, EU, US. |
| dispatched_at | string | UTC timestamp in ISO 8601, e.g. 2026-06-25T21:05:00Z (the trailing Z denotes UTC). |
| data | object | Game-specific result fields, see each section below. |
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.
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);
});
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:
game + data.draw_number (or data.draw_date).game + data.draw_date + data.draw_time — these run several draws per day, so the date alone is not unique.game + data.draw_date (each draws at most once per day).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.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.
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.
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 | game field |
Country | Numbers | Range | Bonus / Special | Format |
|---|---|---|---|---|---|---|
| Daily Lotto | dailylotto | ZA | 5 | 1-36 | None (bonus_ball: null) | string |
| Lotto | lotto | ZA | 6 | 1-52 | Bonus ball, range 1-52 | string |
| Lotto Plus 1 | lotto plus1 | ZA | 6 | 1-52 | Bonus ball, range 1-52 | string |
| Lotto 5 Max | lotto 5 max | ZA | 6 | 1-52 | Bonus ball, range 1-52 | string |
| Powerball | powerball | ZA | 5 | 1-50 | powerball, range 1-16 | string |
| Powerball Xtra | powerball xtra | ZA | 5 | 1-50 | powerball, range 1-16 | string |
| Nigeria | ||||||
| Baba Ijebu (25 draws) | premier <name> | NG | 5 | 1-90 | None | integer array |
| Ghana | ||||||
| Ghana Lottery (10 draws) | ghana <name> | GH | 5 | 1-90 | None | integer array |
| Kenya | ||||||
| Kenya Lotto (daily) | Kenya Lotto <Day> | KE | 6 | 0-39 | None | string |
| kenya mega jackpot | kenya mega jackpot | KE | 6 | 0-39 | None | string |
| UK | ||||||
| UK 49s (4 draws/day) | uk49s <draw> | GB | 6 | 1-49 | Booster ball (booster_ball), range 1-49 | string |
| Europe | ||||||
| euromillions | euromillions | EU | 5 | 1-50 | 2 Lucky Stars (lucky_star_1/2), range 1-12 | string |
| Irish Lotto | irish lotto | IE | 6 | 1-47 | Bonus ball (bonus_ball), range 1-47 | string |
| USA | ||||||
| US Powerball | us powerball | US | 5 | 1-69 | Powerball (special_ball), range 1-26 | string |
| US Mega Millions | us mega millions | US | 5 | 1-70 | Mega Ball (special_ball), range 1-24 | string |
| Horse Racing | ||||||
| Horse Racing | horse_racing | ZA | Structured 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]
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 }
]
}
}
| Field | Type | Notes |
|---|---|---|
| draw_number | string | Official draw number. Always a numeric string (e.g. "2657"), not a JSON integer, so parse it before any numeric comparison. |
| draw_date | string | YYYY-MM-DD |
| winning_numbers | string |
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_ball | integer 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
|
| powerball | integer | powerball / powerball xtra only. Range 1-16 |
| jackpot | number or null | Next jackpot estimate in ZAR (integer); null if not yet known |
| divisions | array or null | Prize 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) |
{
"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]
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| draw_time | string or null | HH:MM (WAT, Nigeria local time) |
| numbers | array of integers | 5 numbers, range 1-90 (order as drawn, not sorted) |
{
"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]
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| draw_time | string or null | HH:MM (GMT; Ghana is UTC+0) |
| numbers | array of integers | 5 numbers, range 1-90 (order as drawn, not sorted) |
{
"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"
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| draw_time | string or null | HH:MM (EAT, Kenya local time) |
| winning_numbers | string | 6 numbers, range 0-39, comma-separated (not zero-padded) |
{
"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
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| winning_numbers | string | 6 numbers, range 1-49, comma-separated |
| booster_ball | integer | Range 1-49 |
{
"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
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| winning_numbers | string | 5 numbers, range 1-50, comma-separated (not zero-padded) |
| lucky_star_1 | integer | First Lucky Star, range 1-12 |
| lucky_star_2 | integer | Second Lucky Star, range 1-12 |
| jackpot | number or null | Jackpot 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).
{
"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
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD |
| winning_numbers | string | 6 numbers, range 1-47, comma-separated (not zero-padded) |
| bonus_ball | integer or null | Bonus ball, range 1-47 (auto-drawn from the same pool) |
| jackpot | number or null | Jackpot in EUR; may be a decimal; null if not yet published |
Draws every Wednesday and Saturday at ~18:45 UTC (20:45 SAST).
{
"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
}
}
| Field | Type | Notes |
|---|---|---|
| draw_date | string | YYYY-MM-DD (ET draw date) |
| winning_numbers | string | 5 numbers (us powerball: range 1-69; us mega millions: range 1-70), comma-separated |
| special_ball | integer | us powerball: range 1-26. us mega millions (Mega Ball): range 1-24 |
| multiplier | integer or null | Power 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.
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_date | string | YYYY-MM-DD, the racing day this race belongs to |
| races_id | integer | Unique id for this race. Use it as your de-duplication key. |
| venue | string | Racecourse name, e.g. "Vaal", "Greyville" |
| track_code | string | Short venue code, e.g. "V" (Vaal), "D" (Greyville) |
| province | string | Province code, e.g. "G", "N" |
| race_no | integer | Race number on the card |
| race_name | string | Official race name / conditions |
| race_class | string | Race classification as published, e.g. "MJP", "MR66", "Cnd", "MR110" |
| distance | integer | Race distance in metres |
| going | string or null | Track condition code as published, e.g. "g" (often null) |
| off_time | string or null | Scheduled start time HH:MM:SS |
| win_time | number or null | Winning time in seconds, e.g. 69.26 (null if not published) |
| stake_total | integer or null | Total prize money in ZAR (null if not published) |
| races_scraped | integer | Always 1 on a push, since each delivery is one race. Present for consistency with the email payload; safe to ignore. |
| finishers | array | Top 3 finishers (1st, 2nd, 3rd) |
| finishers[].finish | integer | Finishing position (1, 2, or 3) |
| finishers[].horse_name | string | Horse name |
| finishers[].jockey | string or null | Jockey name |
| finishers[].trainer | string or null | Trainer name |
| finishers[].tote_win | float or null | Tote win dividend, only set for finish=1 |
| finishers[].tote_place | float or null | Tote place dividend |
| finishers[].sp | string or null | Starting price, e.g. "7/1" |
| dividends | array | Pool dividends for this race |
| dividends[].bet_type | string | Tote 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[].selections | string | Winning combination for the pool, e.g. "1-2-3" |
| dividends[].dividend | float | Payout for a R1 unit on that pool |
Questions? Contact info@resultsza.co.za