API

Build on Qwizbee

Let your app know the moment a quiz starts or finishes. Users connect with one click, you receive signed webhooks, and nobody ever shares a password or account token.

https://qwizbee-api-production.up.railway.app

How the connection works

Three steps, no developer portal and no pre-registration. Your app generates a PKCE verifier, the user approves once, and you store the API key that comes back.

  1. Add a Connect button

    Send the user to qwizbee.com/connect with your app name, a https redirect URI, a PKCE code challenge and a random state value.

  2. The user approves

    Qwizbee shows a consent page in the user’s own language. After Allow, the browser returns to your redirect URI with a one-time code (valid 2 minutes) and your state.

  3. Exchange the code

    Your server trades the code plus the PKCE verifier at POST /api/integrations/token for a qbw_… API key. That key only manages webhooks — nothing else in the account.

The Connect link

Generate the verifier and challenge on your server and keep the verifier in the user’s session:

// on your server
const verifier  = base64url(crypto.randomBytes(32));
const challenge = base64url(sha256(verifier));

// your "Connect to Qwizbee" button points at:
https://www.qwizbee.com/connect
  ?name=Supergood Mail
  &redirect_uri=https://www.supergood-mail.com/callback
  &code_challenge=${challenge}
  &state=${randomState}

// after the user approves you are called back at:
https://www.supergood-mail.com/callback?code=${code}&state=${randomState}

The state value comes back untouched so you can protect against CSRF, and the redirect URI must be https (http://localhost is allowed for testing).

// exchange the code for the scoped API key
const res = await fetch('https://qwizbee-api-production.up.railway.app/api/integrations/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code, code_verifier: verifier }),
});
const { access_token } = await res.json(); // qbw_… — store it safely

The events

After connecting, register a webhook with the API key. From then on Qwizbee POSTs a signed JSON message to your URL whenever one of the user’s quizzes starts or finishes.

quiz.started

Fires the moment the host presses Start.

POST https://your-app.com/webhooks/qwizbee
X-Qwizbee-Event: quiz.started
X-Qwizbee-Signature: <hmac-sha256>

{
  "event": "quiz.started",
  "timestamp": "2026-10-08T13:05:22.410Z",
  "data": {
    "sessionId": "s_ab12cd",
    "roomCode": "KGMZ34",
    "quizId": "qz_9x8y",
    "quizTitle": "Friday afternoon quiz",
    "playerCount": 14,
    "questionCount": 10
  }
}

quiz.completed

Fires when the final standings appear. Adds the winner.

{
  "event": "quiz.completed",
  "timestamp": "2026-10-08T13:12:40.911Z",
  "data": {
    ...same fields as quiz.started,
    "winner": { "nickname": "BeeMercia", "score": 8420 }
  }
}

Payload fields

event
quiz.started or quiz.completed
timestamp
ISO 8601 moment of the event
data.sessionId
internal session id, stable per game
data.roomCode
the 6-character room code players join with
data.quizId / quizTitle
which quiz it was
data.playerCount / questionCount
how many players and questions
data.winner
quiz.completed only: nickname and score of number 1, or null

Verifying the signature

Every delivery is signed with the webhook’s own secret (shown once when you register it). Verify the raw request body — not a re-serialized object — before trusting a delivery:

import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/webhooks/qwizbee', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex');
  const got = req.headers['x-qwizbee-signature'] || '';
  const valid = expected.length === got.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(got));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body);
  if (event.event === 'quiz.started') { /* … */ }
  res.status(200).end();
});

API reference

Base URL of the production API. JWT means the user’s login token; API key means a qbw_… key from the Connect flow. Webhook routes accept both.

Connect flow

  • GET/api/connect/authorizeJWT

    Validate the connect parameters; returns the app name for the consent page.

  • POST/api/connect/approveJWT

    Approve and get the redirect URL containing the one-time code and state.

  • POST/api/integrations/tokenpublic

    Exchange code + code_verifier for the scoped API key (qbw_…).

  • GET/api/integrationsJWT

    List the user’s connected apps with last-used timestamps.

  • DELETE/api/integrations/:idJWT

    Break a connection. Its webhooks are deleted immediately and the key stops working.

Webhooks

  • POST/api/webhooksJWT or API key

    Register a webhook: { url, events }. The response contains the signing secret — shown only once.

  • GET/api/webhooksJWT or API key

    List registered webhooks.

  • DELETE/api/webhooks/:idJWT or API key

    Remove a webhook.

  • GET/api/webhooks/:id/deliveriesJWT or API key

    Last 50 delivery attempts with status codes, for debugging.

Delivery behaviour

  • Failed deliveries are retried after 1, 5 and 25 seconds (4 attempts total).
  • Your endpoint must answer within 5 seconds with any 2xx status.
  • Client errors (4xx) are not retried — you clearly refused, so we stop.
  • Every attempt is logged and visible via the deliveries endpoint.
  • Breaking the connection in the user’s Account page stops all deliveries instantly.

Testing your integration

  • Register a webhook pointing at a https://webhook.site URL to inspect live deliveries.
  • http://localhost and http://127.0.0.1 are accepted as webhook and redirect targets, so you can develop locally.
  • The delivery log (GET /api/webhooks/:id/deliveries) shows the exact status codes we received from you.

Questions or missing an event?

contact@kylevanraaij.com Mail and tell us what you are building — player-level events are on the roadmap.