ClickTheWheel public API v1

Add a stateless random spin to your website

Send a bounded list of text entries and receive one server-selected result. The first version is intentionally small: one public endpoint, uniform selection by array position, predictable JSON, and no account or saved data.

Free usage: up to 10 requests per website per UTC day, subject to a shared service capacity of 1,000 free API requests per UTC day.

Endpoint

POST /api/v1/spin

Use Content-Type: application/json. Browser requests are supported with public CORS and no credentials.

Browser calls are grouped by normalized Origin. Server and other no-Origin calls fall back to the best available proxy-controlled client address. Every POST attempt counts before its body is processed; CORS preflight OPTIONS requests do not.

curl -X POST https://www.clickthewheel.com/api/v1/spin \
  -H 'Content-Type: application/json' \
  -d '{"entries":["Tea","Coffee","Water"],"removeSelected":false}'

Request

{
  "entries": ["Red", "Blue", "Green"],
  "removeSelected": true
}
entries — required
2100 nonblank strings. Each entry is trimmed and may contain up to 120 characters.
removeSelected — optional
Boolean, default false. When true, remainingEntries excludes only the selected array position.

Success response

{
  "ok": true,
  "data": {
    "selected": { "index": 1, "value": "Blue" },
    "entryCount": 3,
    "removeSelected": true,
    "remainingEntries": ["Red", "Green"]
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "generated-request-id"
  }
}

Duplicate values are allowed and occupy separate positions, so duplicates intentionally increase that value's chance of selection.

Browser JavaScript

const response = await fetch("https://www.clickthewheel.com/api/v1/spin", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    entries: ["Red", "Blue", "Green"],
    removeSelected: true
  })
});

const payload = await response.json();
if (!response.ok) throw new Error(payload.error.message);

console.log(payload.data.selected.value);
console.log(payload.data.remainingEntries);

Server-side JavaScript

const response = await fetch("https://www.clickthewheel.com/api/v1/spin", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ entries: ["Alice", "Bob", "Casey"] })
});

const result = await response.json();
if (!result.ok) {
  console.error(result.error.code, result.error.message);
  process.exitCode = 1;
} else {
  console.log(result.data.selected);
}

Errors

{
  "ok": false,
  "error": {
    "code": "TOO_FEW_ENTRIES",
    "message": "Provide at least 2 entries.",
    "details": { "minEntries": 2 }
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "generated-request-id",
    "docsUrl": "https://www.clickthewheel.com/developers"
  }
}

Expected statuses are 400 for invalid JSON or fields, 413 for an oversized body, 415 for the wrong content type, 429 for rate limiting, and 500 for an unexpected server error.

Free daily limit response

{
  "ok": false,
  "error": {
    "code": "FREE_SITE_DAILY_LIMIT_REACHED",
    "message": "Free usage is limited to 10 requests per website per UTC day. Try again after the UTC reset.",
    "details": {
      "limit": 10,
      "period": "UTC day",
      "resetAt": 1788048000
    }
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "generated-request-id",
    "docsUrl": "https://www.clickthewheel.com/developers"
  }
}

A 429 response includes Retry-After, UTC reset time, and X-RateLimit-Policy. A website that reaches its allowance does not consume more shared daily capacity. If shared free capacity is exhausted, availability can pause for every integration until the next UTC day.

Website exhaustion uses FREE_SITE_DAILY_LIMIT_REACHED. Shared service exhaustion uses the distinct FREE_GLOBAL_DAILY_CAPACITY_REACHED code without returning any website, client, or internal bucket identifier.

Limits and safety

  • Maximum request body: 16 KB.
  • Maximum combined entry text: 5,000 characters.
  • Free usage: 10 POST attempts per normalized website Origin per UTC day.
  • Shared free capacity: 1,000 POST attempts per UTC day across the running service.
  • Additional safety limits: 60 requests per minute per client and 600 per minute per running server instance.
  • Successful response rate-limit headers describe the website's free daily allowance. A blocking response identifies the policy that paused the request.
  • The API uses Node's cryptographic randomInt to select uniformly among submitted array positions.
  • The endpoint does not read or mutate accounts, rooms, saved wheels, admin data, or Supabase records.

These limits are best-effort, in-memory, and single-process. They reset on restart, do not coordinate across multiple instances, and are not a billing guarantee. A non-browser caller can forge or rotate Origin values, while proxies and distributed callers can change the fallback identity. Stronger protection would require API keys, a persistent distributed counter, and AWS budget alarms.

Responsible use

This API is for ordinary website choices, games, classroom prompts, and similar low-stakes uses. It is not a certified lottery system, audit log, randomness beacon, or substitute for legal, medical, financial, safety, eligibility, or professional judgment. Do not send confidential entries; standard hosting infrastructure may retain ordinary access logs even though the API itself does not persist request bodies or results.