Votero

API documentation

Create lobbies and read their results programmatically. Generate a key from the API keys menu (sign in first) in the header, then follow the examples below.

Quickstart

  1. Sign in, then generate a key from API keys in the header — see Authentication.
  2. Create a lobby — see POST /api-v1-create-lobby. The response includes the lobby's join code.
  3. Fetch progress and results any time — see GET /api-v1-lobby-results.

Base URL

All endpoints are Supabase Edge Functions under:

https://hjhlsvjmxadedijpofvg.supabase.co/functions/v1/api-v1-<endpoint>

Authentication

Every request needs Authorization: Bearer <key>. The raw key (vk_live_...) is shown exactly once when generated — copy it immediately, only its hash is stored afterward. A key is tied to your account and inherits your normal lobby-creation limits (including the 10-lobby cap — see Errors).

Revoked or unrecognized keys get a 401:

{ "error": "INVALID_API_KEY" }

Rate limits

Each endpoint has its own bucket, separate from the web app's own limits:

EndpointLimit
POST /api-v1-create-lobby20 requests / hour / key
GET /api-v1-lobby-results60 requests / hour / key

Exceeding a limit returns 429 with {"error": "RATE_LIMITED"}. Lobby creation also has a second, stricter limit underneath the API's own bucket: your account can create at most 5 lobbies per 10 minutes (the same limit the web app's creation form is subject to) — whichever limit is hit first returns RATE_LIMITED.

GET/api-v1-me

Confirms a key is valid. No side effects, no rate limit.

curl https://hjhlsvjmxadedijpofvg.supabase.co/functions/v1/api-v1-me \
  -H "Authorization: Bearer vk_live_..."
{ "ok": true, "userId": "3f06f4ea-d868-4ce2-9308-b979b0797979" }

POST/api-v1-create-lobby

Creates a lobby, identical in shape to what the web form's "Create lobby" button sends.

curl -X POST https://hjhlsvjmxadedijpofvg.supabase.co/functions/v1/api-v1-create-lobby \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Where should we eat lunch?",
    "questions": [
      { "title": "Pick a spot", "type": "choice", "options": ["Tacos", "Sushi", "Salad"] }
    ],
    "voterCap": 30,
    "ballotMode": "anonymous",
    "tallyVisibility": "live"
  }'

Body fields

FieldTypeRequiredDescription
titlestringYes1–200 characters.
questionsarrayYesAt least 1 question — see question fields below.
voterCapintegerYes1–10,000. Max number of participants who can join.
ballotMode"anonymous" | "open"Yes"anonymous": nobody — including you — can see who voted for what, only aggregate tallies. "open": you can see each voter's individual ballot via ballotDetail on the results endpoint.
tallyVisibility"live" | "hidden"Yes"live": results are visible while the lobby is still open. "hidden": results are only visible once you close the lobby.
closesAtISO 8601 timestampNoSchedules an automatic close. Must be in the future. Omit for no scheduled close.

Question object fields

FieldTypeRequiredDescription
titlestringYes1–200 characters.
type"choice" | "text" | "ranked"Yes"choice": pick one, or up to N — see maxSelections. "text": free-response, no options. "ranked": rank every option in order (instant-runoff tallying).
optionsstring[]For choice/rankedAt least 2 options, each 1–200 characters. Ignored for text questions.
maxSelectionsintegerNochoice only, 1..options.length. Omit for classic single-select; set above 1 for "choose up to N." Not applicable to ranked — every option is always ranked.

Example: mixed question types

A 3-question survey combining all 3 types — one ranked-choice question, one multi-select ("choose up to N") question, and one free-text question:

{
  "title": "Team offsite planning",
  "questions": [
    { "title": "Rank these venues", "type": "ranked", "options": ["Beach house", "Mountain cabin", "City loft"] },
    { "title": "Which activities interest you?", "type": "choice", "options": ["Hiking", "Cooking class", "Board games", "Spa"], "maxSelections": 2 },
    { "title": "Anything else we should plan for?", "type": "text" }
  ],
  "voterCap": 25,
  "ballotMode": "open",
  "tallyVisibility": "hidden"
}

The response includes the generated code voters use to join at votero.app/vote/<code>.

GET/api-v1-lobby-results

Reads progress/tally/ballot-detail for a lobby you created— a foreign or nonexistent code both return 404, deliberately indistinguishable, so a wrong code can't be used to probe whether someone else's code exists.

Query parameters

FieldTypeRequiredDescription
codestringYesThe lobby's human-readable join code, e.g. 7S8XDH6C.
curl "https://hjhlsvjmxadedijpofvg.supabase.co/functions/v1/api-v1-lobby-results?code=7S8XDH6C" \
  -H "Authorization: Bearer vk_live_..."
{
  "progress": { "joined": 12, "cap": 30, "completedCount": 10 },
  "tally": [
    {
      "questionId": "...",
      "questionTitle": "Pick a spot",
      "type": "choice",
      "tally": [{ "optionId": "...", "count": 7 }, { "optionId": "...", "count": 3 }]
    }
  ],
  "ballotDetail": null
}

tally is null until the lobby is closed or tally visibility is live. ballotDetail is only populated for open-ballot lobbies.

Errors

All errors are {"error": "SOME_CODE"} with a matching HTTP status:

CodeStatusApplies toMeaning
INVALID_API_KEY401All endpointsmissing, unrecognized, or revoked key
AUTH_UNAVAILABLE503create-lobby, lobby-resultsyour key is valid, but minting a session for your account failed transiently — retry
RATE_LIMITED429create-lobby, lobby-resultstoo many requests in the current window (see Rate limits)
MISSING_CODE400lobby-resultscode query param missing
LOBBY_NOT_FOUND404lobby-resultsno lobby with that code owned by this key's account
AT_LEAST_ONE_QUESTION_REQUIRED400create-lobbyquestions array is empty
AT_LEAST_TWO_OPTIONS_REQUIRED400create-lobbya choice or ranked question has fewer than 2 options
INVALID_MAX_SELECTIONS400create-lobbymaxSelections is below 1 or above the question's option count
INAPPROPRIATE_CONTENT400create-lobbythe lobby title, a question title, or an option label failed the profanity filter
CLOSES_AT_MUST_BE_FUTURE400create-lobbyclosesAt is not in the future
LOBBY_LIMIT_REACHED400create-lobbyyour account already has 10 lobbies (the same cap the web app enforces)

Field-length/range violations (e.g. voterCap outside 1–10,000, a title over 200 characters) return a 400 with a raw database error message rather than one of the codes above — validate against the limits in the parameter tables above to avoid hitting these.

What's not in v1

  • No vote-casting or lobby-joining via API key — that represents an end-user voting, not a server acting on their behalf.
  • No standalone lobby-read endpoint — create-lobby's response already has everything a caller who just created a lobby needs.
  • No integration (Zapier, HubSpot, etc.) — this is the raw API; where to point it is a separate, later decision.

Full machine-readable reference: docs/openapi.yaml in the repo.