poll.json.systems New poll

JSON API

Create, manage and read polls from code. The same text is at /llms.txt for agents.

# poll.json.systems

> Anonymous, link-shareable polls. Create a poll, share its link, watch results.
> No accounts. Voting happens in a browser; this API creates, manages and reads.

Base URL: https://poll.json.systems
All bodies are JSON. Errors are {"error": "...", "errors": ["..."]} with a 4xx status.
CORS is open (Access-Control-Allow-Origin: *). No cookies are used by the API.

## Auth

Creating a poll returns a manage_key. It is shown ONCE; only its hash is stored.
Send it as:   Authorization: Bearer <manage_key>
The same key works in the browser as https://poll.json.systems/p/<id>/manage?k=<manage_key>

## Endpoints

POST   /api/v1/polls                    create a poll -> 201 {id, url, manage_url, manage_key, poll}
GET    /api/v1/polls/<id>               the poll; includes "results" with the key, or once closed
PATCH  /api/v1/polls/<id>        [key]  change any top-level field below (partial)
DELETE /api/v1/polls/<id>        [key]  delete permanently
GET    /api/v1/polls/<id>/results       results; needs the key while the poll is open
POST   /api/v1/polls/<id>/close  [key]  stop accepting votes
POST   /api/v1/polls/<id>/reopen [key]  accept votes again (removes a deadline that has passed)
POST   /api/v1/polls/<id>/clear  [key]  delete every vote; everyone may vote again
POST   /api/v1/polls/<id>/duplicate [key]  new poll, same questions/settings -> 201 like create
GET    /api/v1/polls/<id>/ballots.csv [key]  one row per ballot
GET    /api/v1/polls/<id>/totals.csv  [key]  counts per choice

## Poll fields

title               string, required
description         string, optional; URLs are auto-linked on the page
allow_change        bool, default false: may voters change their vote?
writein_visibility  "all" (default) or "creator": who sees "Other" write-in text
deadline            unix seconds, ISO 8601 string, or null; must be in the future
questions           list, at least one; every question is required for voters

Questions can only be changed while the poll has zero ballots (409 otherwise).
Title, description, allow_change, writein_visibility and deadline can always change.

## Question fields

kind      "single" | "multi" | "ranked" | "scale"
prompt    string, required
description  string, optional: guidance shown under the prompt (500 chars),
          e.g. "Pick your favourite DINNER food specifically"
choices   list of strings (single/multi/ranked: 2+; scale with preset "custom": its labels)
other     bool, default false: offer a write-in "Other" (not for scale)
shuffle   bool, default false: randomise choice order per voter (not for scale)
min, max  multi only: fewest/most picks (default 1 / no limit)
method    ranked only: "irv" (default, instant runoff) | "borda" | "both"
preset    scale only, default "agree5":
    agree5        Agree (5-point): Strongly disagree | Disagree | Neutral | Agree | Strongly agree
    agree4        Agree (4-point, no neutral): Strongly disagree | Disagree | Agree | Strongly agree
    satisfaction  Satisfaction: Very dissatisfied | Dissatisfied | Neutral | Satisfied | Very satisfied
    frequency     Frequency: Never | Rarely | Sometimes | Often | Always
    importance    Importance: Not important | Slightly important | Moderately important | Very important | Extremely important
    num5          1 to 5: 1 | 2 | 3 | 4 | 5
    num10         1 to 10: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
    custom        Custom labels (pass "choices": [...], 2-11 labels in order)

A scale whose labels are all integers is numeric and also reports a mean.

## Results

single/multi  choices sorted by count; percent base is voters who answered
scale         bins in scale order, median label, mean (numeric scales)
ranked        irv: rounds (votes per standing choice, exhausted ballots, eliminated),
              winner or tied; borda: points (rank r of n scores n-r+1, unranked 0).
              IRV ties for last are broken by lower Borda score, then later position.
write-ins     grouped case/space-insensitively, as {text, count}

Compare by: GET .../results?by=<question id> (a single-choice or scale question)
adds "breakdown": every other question tallied per group of voters, grouped by
their answer to that question. Without the key, a group with fewer than
25% as many people as the largest group is withheld (counted in
withheld_groups / withheld_people); with the key every group is returned and
small ones are flagged "small": true. Write-in text is never broken down.

## Example

curl -s https://poll.json.systems/api/v1/polls -H 'Content-Type: application/json' -d '{
  "title": "Team offsite",
  "questions": [
    {"kind": "single", "prompt": "Which city?", "choices": ["Lisbon", "Oslo", "Kyoto"], "other": true},
    {"kind": "ranked", "prompt": "Rank the activities", "choices": ["Hike", "Cooking class", "Museum"], "method": "both"},
    {"kind": "scale",  "prompt": "The last offsite was worth the time", "preset": "agree5"}
  ]
}'

## Limits

title 200 chars, description 1000 chars, 20 questions per poll, 20 choices per question,
200 chars per choice/prompt/write-in, 5000 ballots per poll. Rate limits per IP per hour:
1200 API calls, 30 polls created.
Polls are deleted 30 days after their last activity (creation, a vote, or an edit).