Sell entries
Webhooks
We POST a signed event to your endpoint when something happens: tickets issued, a payment settled, a competition closed or cancelled, a winner drawn.
Set up an endpoint
- In your admin, open Settings → Webhooks.
- Enter your HTTPS endpoint URL and choose the events you want. Leave every event unticked to receive all of them.
- Copy the signing secret. It starts with
whsec_and is shown once. - Press Send test ping to send a
pingevent and check your endpoint answers.
Events
| Event | When | data |
|---|---|---|
entry.confirmed | Tickets were issued. | lotId, userId, quantity, ticketNumbers, source (fiat, crypto or free), orderId, txHash |
payment.settled | A card or bank payment settled. | orderId, lotId, userId, rail, amount, currency, quantity |
lot.closed | A competition reached its end time and stopped selling. | lotId, closedAt, endDate, ticketsSold |
lot.cancelled | A competition was cancelled. | lotId, cancelledAt, ticketsSold |
draw.completed | A winner was drawn. | lotId, winnerUserId, winnerTicketId |
ping | A test delivery from your admin. | tenantId |
Request body
{
"id": "evt_1042",
"type": "entry.confirmed",
"createdAt": "2026-10-02T08:14:03.000Z",
"data": {
"lotId": 70,
"userId": 5531,
"quantity": 2,
"ticketNumbers": [
118,
119
],
"source": "fiat",
"orderId": "RAF1789046392849TpO2yXOd",
"txHash": null
}
}| Header | Value |
|---|---|
X-BitRaffle-Event | The event type. |
X-BitRaffle-Delivery | The event id. Stays the same on every retry. |
X-BitRaffle-Signature | t=<unix seconds>,v1=<hex>: an HMAC-SHA256 of <t>.<raw body> with your signing secret. |
Verify every delivery
Anyone can POST to a public URL. Check the signature before you trust an event, over the raw request body exactly as it arrived, and refuse deliveries more than five minutes old.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries((header || "").split(",").map((kv) => kv.trim().split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // stale: possible replay
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}
const app = express();
// Verify over the RAW body: parsing and re-serialising JSON changes the bytes.
app.post("/hooks/bitraffle", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifySignature(raw, req.get("X-BitRaffle-Signature"), process.env.BITRAFFLE_WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(raw);
// Deliveries are at-least-once: skip an event.id you have already handled.
queueForProcessing(event);
res.sendStatus(200); // answer within 10 seconds; do slow work afterwards
});import hashlib, hmac, json, os, time
from flask import Flask, request
def verify_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(kv.strip().split("=", 1) for kv in (header or "").split(",") if "=" in kv)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance: # stale: possible replay
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
app = Flask(__name__)
@app.post("/hooks/bitraffle")
def bitraffle_hook():
raw = request.get_data() # the raw bytes, before any JSON parsing
if not verify_signature(raw, request.headers.get("X-BitRaffle-Signature"), os.environ["BITRAFFLE_WEBHOOK_SECRET"]):
return "bad signature", 400
event = json.loads(raw)
# Deliveries are at-least-once: skip an event["id"] you have already handled.
queue_for_processing(event)
return "", 200 # answer within 10 seconds; do slow work afterwardsDelivery and retries
- Answer with any
2xxwithin 10 seconds. Do slow work after you respond. - A
5xx, a timeout, a408or a429is retried with backoff (5 seconds, 10, 20 … up to 15 minutes apart) for about six hours. - Any other
4xxis final: we take it as your endpoint refusing that event. - Delivery is at least once. Use the event
idto ignore repeats.