BitRaffleDocs

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

  1. In your admin, open Settings → Webhooks.
  2. Enter your HTTPS endpoint URL and choose the events you want. Leave every event unticked to receive all of them.
  3. Copy the signing secret. It starts with whsec_ and is shown once.
  4. Press Send test ping to send a ping event and check your endpoint answers.

Events

EventWhendata
entry.confirmedTickets were issued.lotId, userId, quantity, ticketNumbers, source (fiat, crypto or free), orderId, txHash
payment.settledA card or bank payment settled.orderId, lotId, userId, rail, amount, currency, quantity
lot.closedA competition reached its end time and stopped selling.lotId, closedAt, endDate, ticketsSold
lot.cancelledA competition was cancelled.lotId, cancelledAt, ticketsSold
draw.completedA winner was drawn.lotId, winnerUserId, winnerTicketId
pingA 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
  }
}
HeaderValue
X-BitRaffle-EventThe event type.
X-BitRaffle-DeliveryThe event id. Stays the same on every retry.
X-BitRaffle-Signaturet=<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 afterwards

Delivery and retries

  • Answer with any 2xx within 10 seconds. Do slow work after you respond.
  • A 5xx, a timeout, a 408 or a 429 is retried with backoff (5 seconds, 10, 20 … up to 15 minutes apart) for about six hours.
  • Any other 4xx is final: we take it as your endpoint refusing that event.
  • Delivery is at least once. Use the event id to ignore repeats.