BitRaffleDocs

API reference

The BitRaffle API

A REST API over JSON. Every request is authenticated with your secret key and only ever sees your own organisation's data.

Base URL

Use the host your key was issued for: your own subdomain, or the main domain. All paths below are relative to it.

Authentication

Send your secret key as a bearer token. The Draws endpoints also need a BitRaffle-Proof header signed with your registered key; see Signed requests.

Responses

Successful responses wrap their payload in data (the Draws endpoints return their object directly). Errors carry a machine-readable error code and a human message; see Errors. Money is always an integer: USD amounts are cents.

Prefer a machine-readable spec? openapi.json imports into Postman, Insomnia or any OpenAPI code generator.

Base URL
https://www.bitraffle.io/api/v1
Authenticated request
curl https://www.bitraffle.io/api/v1/competitions \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"

competitions:read

Competitions

Read your live competitions: price, tickets left, closing time and how each can be paid for.

List live competitions

GET/api/v1/competitions

Your organisation's currently active raffles, newest first. Requires the competitions:read scope.

scope · competitions:read

Responses

200OK
401Missing or invalid API key
403Key lacks the required scope
429Rate limited — retry after the seconds in Retry-After
curl https://www.bitraffle.io/api/v1/competitions \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/competitions";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/competitions"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": [
    {
      "id": 71,
      "title": "Win a Tudor Black Bay 58",
      "description": "Tudor Black Bay 58, 39 mm, black dial…",
      "ticketPriceUSDCents": 200,
      "paymentToken": "USDC",
      "maxTickets": 3000,
      "ticketsSold": 412,
      "status": "active",
      "state": "open",
      "endDate": "2026-10-24T01:49:13.000Z",
      "imageUrl": "/uploads/1790921518423-Scf1NxFD.avif",
      "payment": {
        "acceptedTokens": [
          "USDC",
          "USDT"
        ],
        "fiat": false
      }
    }
  ]
}

Get one competition

GET/api/v1/competitions/{id}

Requires the competitions:read scope. 404 if the id doesn't belong to your organisation.

scope · competitions:read

Path parameters

idintegerRequired

Responses

200OK
401Missing or invalid API key
403Key lacks the required scope
404Not found
curl https://www.bitraffle.io/api/v1/competitions/71 \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/competitions/71";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/competitions/71"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "id": 71,
    "title": "Win a Tudor Black Bay 58",
    "description": "Tudor Black Bay 58, 39 mm, black dial…",
    "ticketPriceUSDCents": 200,
    "paymentToken": "USDC",
    "maxTickets": 3000,
    "ticketsSold": 412,
    "status": "active",
    "state": "open",
    "endDate": "2026-10-24T01:49:13.000Z",
    "imageUrl": "/uploads/1790921518423-Scf1NxFD.avif",
    "payment": {
      "acceptedTokens": [
        "USDC",
        "USDT"
      ],
      "fiat": false
    }
  }
}

winners:read

Winners

Published winners, each with the proof of how they were drawn.

List published winners

GET/api/v1/winners

Winners your organisation has published, each with its commit-reveal draw proof. Requires the winners:read scope.

scope · winners:read

Responses

200OK
401Missing or invalid API key
403Key lacks the required scope
curl https://www.bitraffle.io/api/v1/winners \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/winners";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/winners"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": [
    {
      "competitionId": 61,
      "competitionTitle": "Win a Rolex Datejust 41",
      "winnerWalletAddress": "0x7Fb6…2e19",
      "wonAt": "2026-09-30T14:02:11.000Z",
      "draw": {
        "method": "vrf",
        "seed": "0x5c1e…9a04",
        "winnerIndex": 1287,
        "totalTickets": 2600,
        "vrfRequestId": "8841…2203",
        "fulfillmentTx": "0x2b7d…c1f0",
        "blockNumber": null
      }
    }
  ]
}

entries:create

Entries and checkout

Sell entries from your own product: ask the entry question, start a hosted checkout, and follow it until tickets are issued.

Get the entry question

GET/api/v1/entry-question

Whether a skill question is required at checkout, and one to ask. Requires entries:create.

scope · entries:create

Query parameters

competitionIdinteger

Optional. Picks a question that fits this competition's prize category.

Responses

200OK
401Missing or invalid API key
403Key lacks the required scope
curl https://www.bitraffle.io/api/v1/entry-question \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/entry-question";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/entry-question"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "required": true,
    "question": {
      "id": 17,
      "question": "On an analogue watch, how many degrees does the minute hand move in one minute?",
      "options": [
        "1",
        "6",
        "12",
        "30"
      ],
      "category": "watch"
    }
  }
}

Check a buyer's answer

POST/api/v1/entry-question/verify

A correct answer returns a single-use pass to send with the checkout session. Requires entries:create.

scope · entries:create

Body

questionIdintegerRequired
answerIndexintegerRequired

Responses

200OK
404Unknown question
curl -X POST https://www.bitraffle.io/api/v1/entry-question/verify \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "questionId": 17,
    "answerIndex": 1
  }'
const url = "https://www.bitraffle.io/api/v1/entry-question/verify";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "questionId": 17,
    "answerIndex": 1
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/entry-question/verify"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "questionId": 17,
        "answerIndex": 1,
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "correct": true,
    "pass": "eyJxIjoxNywidCI6MSwiZSI6MTc5…Yw.k3Xb…"
  }
}

Start a checkout session

POST/api/v1/checkout/sessions

Prices the entries, reserves the buyer's credit, checks the entry window and the entry condition, then hands the order to the competition's payment rail. Render payment for the buyer; poll the session until ticketsIssued. Requires entries:create.

scope · entries:create

Body

competitionIdintegerRequired
quantityinteger

Default 1. Between 1 and 100.

buyerobjectRequired

One of email or walletAddress. The buyer becomes (or maps to) an account so tickets and results share one identity.

Show child attributes
emailstring (email)
walletAddressstring
skillPassstring

From POST /entry-question/verify, when the entry question is required.

currencystring

Must equal the competition's charge currency when given.

applyCreditboolean

Apply the buyer's account credit to the charge.

Default true.

returnUrlstring (uri)

Where redirect rails send the buyer afterwards, with ?ref=<session id>&status=success|failed appended. HTTPS, and its host must be one of your organisation's partner return hosts (Settings → Webhooks). Honoured by Checkout.com (after 3DS) and OnePay; in-page rails (Stripe, Worldpay, CVPay QR) never leave your page.

Responses

201Session started
400Invalid request, entries closed, sold out, or the entry condition was not met
403Key lacks the required scope, or fiat checkout is off for this competition
404Competition not found
412Fiat payments are not configured for your organisation
curl -X POST https://www.bitraffle.io/api/v1/checkout/sessions \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "competitionId": 71,
    "quantity": 2,
    "buyer": {
      "email": "buyer@example.com"
    },
    "skillPass": "eyJxIjoxNywidCI6MSwiZSI6MTc5…Yw.k3Xb…",
    "returnUrl": "https://your-site.com/raffle/return"
  }'
const url = "https://www.bitraffle.io/api/v1/checkout/sessions";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "competitionId": 71,
    "quantity": 2,
    "buyer": {
      "email": "buyer@example.com"
    },
    "skillPass": "eyJxIjoxNywidCI6MSwiZSI6MTc5…Yw.k3Xb…",
    "returnUrl": "https://your-site.com/raffle/return"
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/checkout/sessions"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "competitionId": 71,
        "quantity": 2,
        "buyer": {
            "email": "buyer@example.com",
        },
        "skillPass": "eyJxIjoxNywidCI6MSwiZSI6MTc5…Yw.k3Xb…",
        "returnUrl": "https://your-site.com/raffle/return",
    },
)
data = res.json()["data"]
Response · 201
{
  "data": {
    "id": "RAF1789046392849TpO2yXOd",
    "status": "pending",
    "provider": "checkout",
    "amount": 400,
    "currency": "USD",
    "payment": {
      "type": "PAY_URL",
      "redirectUrl": "https://pay.checkout.com/…"
    }
  }
}

Get a checkout session

GET/api/v1/checkout/sessions/{id}
scope · entries:create

Path parameters

idstringRequired

Responses

200OK
404Not your session
curl https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "id": "RAF1789046392849TpO2yXOd",
    "status": "paid",
    "competitionId": 71,
    "quantity": 2,
    "amount": 400,
    "currency": "USD",
    "ticketsIssued": true,
    "error": null
  }
}

Confirm a Worldpay session

POST/api/v1/checkout/sessions/{id}/confirm

Worldpay only. The hosted card fields on your page (mounted by the embed SDK) produce session hrefs; this authorizes and settles synchronously and issues the tickets. Requires entries:create.

scope · entries:create

Path parameters

idstringRequired

Body

sessionHrefstring (uri)Required
cvcHrefstring (uri)

Responses

200Authorized and settled
400Declined, not a Worldpay session, already final, or an invalid session href
404Not your session
curl -X POST https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd/confirm \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionHref": "https://access.worldpay.com/sessions/…",
    "cvcHref": "https://access.worldpay.com/sessions/…"
  }'
const url = "https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd/confirm";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "sessionHref": "https://access.worldpay.com/sessions/…",
    "cvcHref": "https://access.worldpay.com/sessions/…"
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/checkout/sessions/RAF1789046392849TpO2yXOd/confirm"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "sessionHref": "https://access.worldpay.com/sessions/…",
        "cvcHref": "https://access.worldpay.com/sessions/…",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "id": "RAF1789046392849TpO2yXOd",
    "status": "paid",
    "ticketsIssued": true,
    "paymentId": "wp_8a3…"
  }
}

competitions:write

Prizes and competitions

Create prizes and competitions, open them, and cancel them.

List your prizes

GET/api/v1/prizes
scope · competitions:write

Responses

200OK
curl https://www.bitraffle.io/api/v1/prizes \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/prizes";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/prizes"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": [
    {
      "id": 66,
      "category": "electronics",
      "name": "MacBook Pro 14",
      "brand": "Apple",
      "model": "MacBook Pro 14-inch (M5)",
      "referenceNumber": null,
      "condition": "new",
      "retailValueUSDCents": 199900,
      "description": "M5, 16 GB, 512 GB SSD, Space Black.",
      "imageUrls": [
        "/uploads/1790921180124-zCYDenet.png"
      ]
    }
  ]
}

Create a prize

POST/api/v1/prizes
scope · competitions:write

Body

categorystring

Default "watch".

namestring
brandstring
modelstring
referenceNumberstring
conditionstring

Default "new".

newunwornexcellentgood
yearManufacturedinteger
retailValueUSDCentsintegerRequired
descriptionstring
imageUrlsarray of string (uri)s

At most 12 items.

Responses

201Created
400Invalid body (issues listed)
curl -X POST https://www.bitraffle.io/api/v1/prizes \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "electronics",
    "brand": "Apple",
    "model": "MacBook Pro 14-inch (M5)",
    "condition": "new",
    "retailValueUSDCents": 199900,
    "description": "M5, 16 GB, 512 GB SSD, Space Black.",
    "imageUrls": [
      "https://your-cdn.com/macbook-pro-14.png"
    ]
  }'
const url = "https://www.bitraffle.io/api/v1/prizes";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "category": "electronics",
    "brand": "Apple",
    "model": "MacBook Pro 14-inch (M5)",
    "condition": "new",
    "retailValueUSDCents": 199900,
    "description": "M5, 16 GB, 512 GB SSD, Space Black.",
    "imageUrls": [
      "https://your-cdn.com/macbook-pro-14.png"
    ]
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/prizes"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "category": "electronics",
        "brand": "Apple",
        "model": "MacBook Pro 14-inch (M5)",
        "condition": "new",
        "retailValueUSDCents": 199900,
        "description": "M5, 16 GB, 512 GB SSD, Space Black.",
        "imageUrls": ["https://your-cdn.com/macbook-pro-14.png"],
    },
)
data = res.json()["data"]
Response · 201
{
  "data": {
    "id": 66,
    "category": "electronics",
    "name": null,
    "brand": "Apple",
    "model": "MacBook Pro 14-inch (M5)",
    "referenceNumber": null,
    "condition": "new",
    "retailValueUSDCents": 199900,
    "description": "M5, 16 GB, 512 GB SSD, Space Black.",
    "imageUrls": [
      "https://your-cdn.com/macbook-pro-14.png"
    ]
  }
}

Create a competition

POST/api/v1/competitions
scope · competitions:write

Body

prizeIdintegerRequired
titlestringRequired
descriptionstring

Markdown.

ticketPriceUSDCentsintegerRequired
maxTicketsintegerRequired
startDatestring (date-time)Required
endDatestring (date-time)Required
paymentTokenstring

Default "USDC".

USDCUSDT
imageUrlstring (uri)
createOnChainboolean

Default false.

paymentobject

Rail overrides; null or omitted inherits the organisation's rails. Must leave at least one usable rail.

Show child attributes
cryptoboolean, nullable
fiatboolean, nullable
fiatCurrencystring, nullable

Responses

201Created
400Invalid body, or a payment choice that leaves no usable rail
502On-chain creation failed
curl -X POST https://www.bitraffle.io/api/v1/competitions \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prizeId": 66,
    "title": "Win a MacBook Pro 14",
    "description": "One winner. Draw by Chainlink VRF when entries close.",
    "ticketPriceUSDCents": 100,
    "maxTickets": 3000,
    "startDate": "2026-10-02T12:00:00Z",
    "endDate": "2026-11-06T12:00:00Z",
    "paymentToken": "USDC"
  }'
const url = "https://www.bitraffle.io/api/v1/competitions";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "prizeId": 66,
    "title": "Win a MacBook Pro 14",
    "description": "One winner. Draw by Chainlink VRF when entries close.",
    "ticketPriceUSDCents": 100,
    "maxTickets": 3000,
    "startDate": "2026-10-02T12:00:00Z",
    "endDate": "2026-11-06T12:00:00Z",
    "paymentToken": "USDC"
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/competitions"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "prizeId": 66,
        "title": "Win a MacBook Pro 14",
        "description": "One winner. Draw by Chainlink VRF when entries close.",
        "ticketPriceUSDCents": 100,
        "maxTickets": 3000,
        "startDate": "2026-10-02T12:00:00Z",
        "endDate": "2026-11-06T12:00:00Z",
        "paymentToken": "USDC",
    },
)
data = res.json()["data"]
Response · 201
{
  "data": {
    "id": 70,
    "title": "Win a MacBook Pro 14",
    "description": "Tudor Black Bay 58, 39 mm, black dial…",
    "ticketPriceUSDCents": 100,
    "paymentToken": "USDC",
    "maxTickets": 3000,
    "ticketsSold": 0,
    "status": "draft",
    "state": "draft",
    "endDate": "2026-11-06T12:00:00.000Z",
    "imageUrl": null,
    "payment": {
      "acceptedTokens": [
        "USDC",
        "USDT"
      ],
      "fiat": false
    }
  }
}

Update a competition

PATCH/api/v1/competitions/{id}
scope · competitions:write

Path parameters

idintegerRequired

Body

titlestring
descriptionstring
statusstring

Open a draft with active. Closing and cancelling have their own paths.

draftactive
paymentobject
Show child attributes
cryptoboolean, nullable
fiatboolean, nullable
fiatCurrencystring, nullable

Responses

200Updated
400Invalid body
404Not found
curl -X PATCH https://www.bitraffle.io/api/v1/competitions/71 \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "active"
  }'
const url = "https://www.bitraffle.io/api/v1/competitions/71";
const res = await fetch(url, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "status": "active"
  }),
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/competitions/71"
res = requests.patch(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "status": "active",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "id": 70,
    "title": "Win a MacBook Pro 14",
    "description": "Tudor Black Bay 58, 39 mm, black dial…",
    "ticketPriceUSDCents": 100,
    "paymentToken": "USDC",
    "maxTickets": 3000,
    "ticketsSold": 0,
    "status": "active",
    "state": "open",
    "endDate": "2026-11-06T12:00:00.000Z",
    "imageUrl": "/uploads/1790921518423-Scf1NxFD.avif",
    "payment": {
      "acceptedTokens": [
        "USDC",
        "USDT"
      ],
      "fiat": false
    }
  }
}

Cancel a competition

POST/api/v1/competitions/{id}/cancel

Final. Closes the lot, refunds affiliate commissions on every sale, and (on-chain) leaves buyers to claim refunds from the contract. A lot with a winner cannot be cancelled.

scope · competitions:write

Path parameters

idintegerRequired

Responses

200Cancelled
400Already has a winner
404Not found
curl -X POST https://www.bitraffle.io/api/v1/competitions/71/cancel \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/competitions/71/cancel";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const { data } = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/competitions/71/cancel"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
data = res.json()["data"]
Response · 200
{
  "data": {
    "id": 70,
    "title": "Win a MacBook Pro 14",
    "description": "Tudor Black Bay 58, 39 mm, black dial…",
    "ticketPriceUSDCents": 200,
    "paymentToken": "USDC",
    "maxTickets": 3000,
    "ticketsSold": 412,
    "status": "cancelled",
    "state": "cancelled",
    "endDate": "2026-10-24T01:49:13.000Z",
    "imageUrl": "/uploads/1790921518423-Scf1NxFD.avif",
    "payment": {
      "acceptedTokens": [
        "USDC",
        "USDT"
      ],
      "fiat": false
    }
  }
}

draws:create

Draws

Provably-fair draws over any list you supply. Commit first, reveal second, and hand anyone the proof.

Price, limits and where to deposit

GET/api/v1/draws/info

Read before your first draw. Bearer key only, no proof needed.

scope · draws:create

Responses

200OK
curl https://www.bitraffle.io/api/v1/draws/info \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/draws/info";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/info"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
body = res.json()
Response · 200
{
  "algorithm": "fair-draw-v1",
  "proofHeader": "BitRaffle-Proof",
  "unitCostCents": 1,
  "maxCandidates": 10000,
  "maxProofTtlSec": 300,
  "settlement": {
    "depositAddress": "0x26A8…1324",
    "tokenAddress": "0xaf88…5831",
    "tokenSymbol": "USDC",
    "tokenDecimals": 6
  },
  "flow": [
    "register-key",
    "deposit (fund balance)",
    "commit",
    "reveal",
    "verify at /verify"
  ]
}

Register your signing key

POST/api/v1/draws/register-key

One time, per API key. Bearer key only: you are registering the key your proofs will be signed with. A second registration is refused (409); rotate instead.

scope · draws:create

Body

publicKeystringRequired

Your Ed25519 public key: SPKI DER, base64url (no padding).

Responses

200Registered
400Not a base64url Ed25519 SPKI key
409A key is already registered
curl -X POST https://www.bitraffle.io/api/v1/draws/register-key \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "publicKey": "MCowBQYDK2VwAyEA3v1Jx…"
  }'
const url = "https://www.bitraffle.io/api/v1/draws/register-key";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "publicKey": "MCowBQYDK2VwAyEA3v1Jx…"
  }),
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/register-key"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
    json={
        "publicKey": "MCowBQYDK2VwAyEA3v1Jx…",
    },
)
body = res.json()
Response · 200
{
  "ok": true,
  "keyPrefix": "sk_4f3a9c1e"
}

Credit a USDC deposit

POST/api/v1/draws/deposit

Send USDC to settlement.depositAddress, then report the transaction. We read it on-chain and credit what the chain shows, once per transaction.

scope · draws:createsigned request

Body

txHashstringRequired

The USDC transfer to settlement.depositAddress.

Responses

200Credited (or already credited)
400Not a USDC transfer to the deposit address, reverted, or deposits are off
404Transaction not found on chain yet; retry shortly
curl -X POST https://www.bitraffle.io/api/v1/draws/deposit \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "BitRaffle-Proof: $PROOF" \
  -H "Content-Type: application/json" \
  -d '{
    "txHash": "0x9b1c4e…e4a2"
  }'
const url = "https://www.bitraffle.io/api/v1/draws/deposit";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "BitRaffle-Proof": proof({ method: "POST", url }), // see Signed requests
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "txHash": "0x9b1c4e…e4a2"
  }),
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/deposit"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
        "BitRaffle-Proof": proof("POST", url),  # see Signed requests
    },
    json={
        "txHash": "0x9b1c4e…e4a2",
    },
)
body = res.json()
Response · 200
{
  "balanceCents": 2500,
  "creditedCents": 2500,
  "duplicate": false
}

Commit a draw

POST/api/v1/draws/commit

We generate a secret server seed and publish its hash. Keep the commitment: it proves the seed existed before you chose yours. No body.

scope · draws:createsigned request

Responses

200Committed
401Missing or invalid proof
409Proof already used (replay): sign a new one
curl -X POST https://www.bitraffle.io/api/v1/draws/commit \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "BitRaffle-Proof: $PROOF"
const url = "https://www.bitraffle.io/api/v1/draws/commit";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "BitRaffle-Proof": proof({ method: "POST", url }), // see Signed requests
  },
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/commit"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
        "BitRaffle-Proof": proof("POST", url),  # see Signed requests
    },
)
body = res.json()
Response · 200
{
  "drawId": "4b6f0c1e-8d2a-4f7b-9c3e-1a5d7e9f2b4c",
  "serverSeedHash": "3f1c9a7be0d24c5f8a1e6b2d9c4f7a0e5b8d1c3f6a9e2b5d8c1f4a7e0b3d6c9f",
  "algorithm": "fair-draw-v1",
  "maxProofTtlSec": 300
}

Reveal a draw

POST/api/v1/draws/{drawId}/reveal

Send your seed and the candidate list; get the winners and a proof anyone can recompute. Charged once (unitCostCents) from your prepaid balance. Re-sending identical inputs returns the same result at no charge; different inputs on a revealed draw are refused.

scope · draws:createsigned request

Path parameters

drawIdstring (uuid)Required

Body

clientSeedstringRequired

Your seed. Choose it AFTER you receive the commitment.

At most 200 characters.

candidatesarray of stringsRequired

The set to draw from, in a fixed order.

At most 10000 items.

pickintegerRequired

How many distinct winners to draw (at most the candidate count).

At least 1.

noncestring

Default "0". At most 80 characters.

Responses

200Revealed
400Invalid candidates or pick
402Prepaid balance too low
404No such draw for your organisation
409Already revealed with different inputs, or proof replayed
curl -X POST https://www.bitraffle.io/api/v1/draws/4b6f0c1e-8d2a-4f7b-9c3e-1a5d7e9f2b4c/reveal \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "BitRaffle-Proof: $PROOF" \
  -H "Content-Type: application/json" \
  -d '{
    "clientSeed": "acme-weekly-2026-10-02",
    "candidates": [
      "alice@example.com",
      "bob@example.com",
      "carol@example.com"
    ],
    "pick": 1
  }'
const url = "https://www.bitraffle.io/api/v1/draws/4b6f0c1e-8d2a-4f7b-9c3e-1a5d7e9f2b4c/reveal";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "BitRaffle-Proof": proof({ method: "POST", url }), // see Signed requests
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "clientSeed": "acme-weekly-2026-10-02",
    "candidates": [
      "alice@example.com",
      "bob@example.com",
      "carol@example.com"
    ],
    "pick": 1
  }),
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/4b6f0c1e-8d2a-4f7b-9c3e-1a5d7e9f2b4c/reveal"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
        "BitRaffle-Proof": proof("POST", url),  # see Signed requests
    },
    json={
        "clientSeed": "acme-weekly-2026-10-02",
        "candidates": ["alice@example.com", "bob@example.com", "carol@example.com"],
        "pick": 1,
    },
)
body = res.json()
Response · 200
{
  "result": {
    "winners": [
      "bob@example.com"
    ],
    "winnerIndices": [
      1
    ]
  },
  "proof": {
    "alg": "fair-draw-v1",
    "serverSeedHash": "3f1c9a7be0d24c5f8a1e6b2d9c4f7a0e5b8d1c3f6a9e2b5d8c1f4a7e0b3d6c9f",
    "serverSeed": "a41b0c7e9d2f5a8c1e4b7d0a3f6c9e2b5d8a1c4f7e0b3d6a9c2f5e8b1d4a7c0e",
    "clientSeed": "acme-weekly-2026-10-02",
    "nonce": "0",
    "candidatesHash": "9d5e2a8c1f4b7e0d3a6c9f2b5e8d1a4c7f0b3e6d9a2c5f8b1e4d7a0c3f6b9e2d",
    "candidateCount": 3,
    "pick": 1,
    "winnerIndices": [
      1
    ]
  },
  "costCents": 1
}

Balance and usage

GET/api/v1/draws/usage
scope · draws:create

Responses

200OK
curl https://www.bitraffle.io/api/v1/draws/usage \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/draws/usage";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/usage"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
body = res.json()
Response · 200
{
  "draws": 118,
  "costCents": 118,
  "balanceCents": 2382,
  "unitCostCents": 1,
  "settlement": {
    "depositAddress": "0x26A8…1324",
    "tokenAddress": "0xaf88…5831",
    "tokenSymbol": "USDC",
    "tokenDecimals": 6
  }
}

Deposits and draw charges

GET/api/v1/draws/ledger

Your organisation's 20 most recent ledger entries, newest first.

scope · draws:create

Responses

200OK
curl https://www.bitraffle.io/api/v1/draws/ledger \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY"
const url = "https://www.bitraffle.io/api/v1/draws/ledger";
const res = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
  },
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/ledger"
res = requests.get(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
    },
)
body = res.json()
Response · 200
{
  "entries": [
    {
      "kind": "draw",
      "amountCents": -1,
      "txHash": null,
      "drawId": "4b6f0c1e-8d2a-4f7b-9c3e-1a5d7e9f2b4c",
      "createdAt": "2026-10-02T08:14:03.000Z"
    },
    {
      "kind": "deposit",
      "amountCents": 2500,
      "txHash": "0x9b1c4e…e4a2",
      "drawId": null,
      "createdAt": "2026-10-01T09:30:41.000Z"
    }
  ]
}

Rotate your signing key

POST/api/v1/draws/rotate-key

Replace the registered key. The proof must be signed by the CURRENT key, so a leaked bearer key alone cannot take over your draws.

scope · draws:createsigned request

Body

publicKeystringRequired

Your Ed25519 public key: SPKI DER, base64url (no padding).

Responses

200Rotated
401Missing or invalid proof
curl -X POST https://www.bitraffle.io/api/v1/draws/rotate-key \
  -H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
  -H "BitRaffle-Proof: $PROOF" \
  -H "Content-Type: application/json" \
  -d '{
    "publicKey": "MCowBQYDK2VwAyEAq8Lm…"
  }'
const url = "https://www.bitraffle.io/api/v1/draws/rotate-key";
const res = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
    "BitRaffle-Proof": proof({ method: "POST", url }), // see Signed requests
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "publicKey": "MCowBQYDK2VwAyEAq8Lm…"
  }),
});
const body = await res.json();
import os, requests

url = "https://www.bitraffle.io/api/v1/draws/rotate-key"
res = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {os.environ['BITRAFFLE_SECRET_KEY']}",
        "BitRaffle-Proof": proof("POST", url),  # see Signed requests
    },
    json={
        "publicKey": "MCowBQYDK2VwAyEAq8Lm…",
    },
)
body = res.json()
Response · 200
{
  "ok": true,
  "keyPrefix": "sk_4f3a9c1e"
}

Outbound

Webhook events

Events we POST to your endpoint as they happen. Set the endpoint and its secret in your admin under Webhooks; the Webhooks guide shows how to verify each delivery.

The event envelope

What your webhook endpoint receives (POST, application/json). Deliveries are at-least-once: id is stable across retries, dedupe on it. Headers: X-BitRaffle-Event (the type), X-BitRaffle-Delivery (the id), X-BitRaffle-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, ${t}.${rawBody}). Verify over the RAW body, reject deliveries older than 5 minutes, answer 2xx within 10 seconds. A 4xx is final; 5xx and timeouts are retried with backoff for up to ~6 hours.

Attributes

idstring
typestring
entry.confirmedpayment.settledlot.closedlot.cancelleddraw.completedping
createdAtstring (date-time)
dataobject

Event payload. entry.confirmed: lotId, userId, quantity, ticketNumbers, source, orderId|txHash. payment.settled: orderId, lotId, userId, rail, amount, currency, quantity. lot.closed / lot.cancelled: lotId. draw.completed: lotId, winnerUserId, winnerTicketId.

EventWhen
entry.confirmedTickets were issued. data: lotId, userId, quantity, ticketNumbers, source (fiat, crypto or free), orderId or txHash.
payment.settledA card or bank payment settled. data: orderId, lotId, userId, rail, amount, currency, quantity.
lot.closedA competition reached its end time and stopped selling. data: lotId, closedAt, endDate, ticketsSold.
lot.cancelledA competition was cancelled; buyers can claim refunds. data: lotId, cancelledAt, ticketsSold.
draw.completedA winner was drawn. data: lotId, winnerUserId, winnerTicketId.
pingA test delivery from your admin.
POST to your endpoint
{
  "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
  }
}
Headers
X-BitRaffle-Event: entry.confirmed
X-BitRaffle-Delivery: evt_1042
X-BitRaffle-Signature: t=1790928843,v1=5f2b…9c1e