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).