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.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.race_date — a full day across all venues is sent as a single horse_racing payload, so there is only ever one delivery per date.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]
{
"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.
{
"event": "result.published",
"game": "horse_racing",
"country": "ZA",
"dispatched_at": "2026-06-25T19:14:32Z",
"data": {
"race_date": "2026-06-25",
"meetings": [
{
"venue": "Vaal",
"track_code": "V",
"province": "G",
"races": [
{
"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,
"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,
"sp": "87/100"
},
{
"finish": 3,
"horse_name": "Vision Of Gold",
"jockey": "C Murray",
"trainer": "Mike / Mathew de Kock",
"tote_win": null,
"tote_place": 1.9,
"sp": "11/2"
}
],
"dividends": [
{ "bet_type": "Exacta", "selections": "10/8", "dividend": 13.9 },
{ "bet_type": "Swinger", "selections": "10/11", "dividend": 4.2 },
{ "bet_type": "Trifecta", "selections": "10/8/11", "dividend": 65.6 },
{ "bet_type": "Quartet", "selections": "10/8/11/4", "dividend": 801 }
]
}
]
}
],
"races_scraped": 9,
"dividends_found": 74
}
}
| Field | Type | Notes |
|---|---|---|
| race_date | string | YYYY-MM-DD — the racing day |
| races_scraped | integer | Total races stored for this date across all meetings |
| dividends_found | integer | Tote dividend rows stored across the day (Swinger, Exacta, Trifecta, Quartet, Double, Pick 3, Pick 6, Jackpot, Bipot, Place Accumulator) |
| meetings | array | One object per race venue on the day |
| meetings[].venue | string | Racecourse name, e.g. "Vaal", "Greyville" |
| meetings[].track_code | string | Short venue code, e.g. "V" (Vaal), "D" (Greyville) |
| meetings[].province | string | Province code, e.g. "G", "N" |
| meetings[].races | array | Races at this venue, ordered by race number |
| races[].race_no | integer | Race number on the card |
| races[].race_name | string | Official race name / conditions |
| races[].race_class | string | Race classification as published, e.g. "MJP", "MR66", "Cnd", "MR110" |
| races[].distance | integer | Race distance in metres |
| races[].going | string or null | Track condition code as published, e.g. "g" (often null) |
| races[].off_time | string | Scheduled start time HH:MM:SS |
| races[].win_time | number or null | Winning time in seconds, e.g. 69.26 (null if not published) |
| races[].stake_total | integer | Total prize money in ZAR (null if not published) |
| races[].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 | Jockey name (null if not available) |
| finishers[].trainer | string | Trainer name (null if not available) |
| finishers[].tote_win | float | Tote win dividend — only set for finish=1 (null otherwise) |
| finishers[].tote_place | float | Tote place dividend (null if not available) |
| finishers[].sp | string | Starting price, e.g. "7/1" (null if not available) |
| races[].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 horse numbers, slash-separated per position, e.g. "10/8" (Exacta) or "10/8/11/4" (Quartet); multi-leg pools use commas within a leg, e.g. "2,4/2/3" |
| dividends[].dividend | number | Payout per R1 unit staked |
Questions? Contact info@resultsza.co.za