Votera API, as shipped in the download
Votera REST API & Webhooks (v2.0)
Votera ships a JSON REST API and signed webhooks so your support desk, CRM or CI can file feedback,
move items across the roadmap, and reply — without anyone opening the board.
Migrating from Canny (or Nolt / Featurebase)
Export your posts to CSV and import them with their vote counts intact.Admin → Import accepts title (required), body, board, votes, author, with the usual
header-name aliases (Post Title, Description, Vote Count, …) and a dry-run report first.
Those tools export a vote count, not individual voters, while Votera stores one row per voter.
The import therefore synthesises one placeholder voter per counted vote, keyed imported:<post>:<n>,
so a migrated post keeps its standing on the board on day one. Real visitors vote on top of that
normally — a synthetic key can never collide with a real one.
Authentication
Authorization: Bearer apk_...
Create a key in Settings → API & Webhooks; shown once, stored hashed.
Endpoints
| Method & path | What it does | ||
|---|---|---|---|
GET /api/ping | Verify a key | ||
GET /api/boards | Boards and statuses — the status ids you need to move things | ||
GET /api/posts | ?board= (slug or id) · ?status= (label or id) · `?sort=top\ | new\ | trending` |
POST /api/posts | File a post as the key's user | ||
GET /api/posts/{id} | One post | ||
POST /api/posts/{id}/status | Move it across the roadmap | ||
GET /api/posts/{id}/comments | Public comments | ||
POST /api/posts/{id}/comments | Reply as the team | ||
GET /api/openapi.json | OpenAPI 3.0 |
File a post from your helpdesk
curl -X POST https://feedback.example.com/api/posts \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"board":"feature-requests","title":"Bulk export","body":"Asked by 3 customers this week."}'
Move it when you start work
curl -X POST https://feedback.example.com/api/posts/42/status \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"status_id":3}'
status_id must be a real status (GET /api/boards lists them); send null to clear it.
What the API deliberately will NOT give you
- Author emails. A public board collects them so posters can be notified, not so an integration
can build a mailing list. author_email is never serialized.
- Internal staff notes. Notes written with the "internal" flag stay in the admin UI. The comments
endpoint calls the same reader the public board uses, with internal notes excluded.
Webhooks
| Event | Fires when |
|---|---|
post.created | A post is filed (UI or API) |
post.status_changed | A post moves — payload carries previous_status_id |
comment.created | A public comment is added |
X-Votera-Event: post.status_changed
X-Votera-Signature: sha256=<hmac_sha256(raw_body, your_webhook_secret)>
Errors
| Code | Meaning |
|---|---|
401 | Missing, unknown or revoked key |
404 | No such post / board |
422 | Validation — detail names the problem |
Own It 3.0
MCP — the agent endpoint
Votera speaks MCP (Model Context Protocol) at POST /mcp, authenticated with the same revocable
API keys the REST API uses. Stateless streamable-HTTP: one JSON-RPC message per request.
GET /mcp answers 405 with Allow: POST — deliberately before the API-key check, so a
client probing for MCP support with no credentials learns the transport is here rather than being
told 401, which would mean "no MCP endpoint". The unauthenticated hint names no tools.
| Tool | Writes? | What it does |
|---|---|---|
list_boards | no | The boards and how many open ideas each holds. |
list_posts | no | Ideas with vote counts and status. Sort by top / trending / new. |
post_detail | no | One idea, its body, its tally and its public comments. |
create_post | YES | Files an idea, through the public form's own validation. |
set_status | YES | Moves an idea to a status, by label. |
top_voted_report | no | The tally: most-wanted ideas and the status mix. |
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should
answer questions but never change anything — the endpoint then lists only the read tools and refuses the
rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://feedback.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http votera https://feedback.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the
same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "votera",
"server_url": "https://feedback.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
A client that keeps its servers in a config file — Own Your AI, editors like VS Code and Cursor, and
most desktop clients read a list like this:
{
"mcpServers": [
{ "id": "votera", "name": "Votera", "url": "https://feedback.example.com/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the sameAuthorization: Bearer header; a model running on your own machine reaches it through any MCP client, so
nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://feedback.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet:
Votera implements the protocol, not an integration with a particular vendor.
⛔ There is deliberately no cast_vote tool
This was a design decision, and it is worth stating plainly: an agent cannot vote. A vote here is
the entire signal — the product exists to tell you what people actually want. Votes are keyed to a
browser or an address rather than a verified identity, and the rate limiter exists because a real
attack of 1,239 votes per minute from one machine was measured. An authenticated voting endpoint
would industrialise exactly that. An agent may read the tally and file an idea; the wanting has to
come from someone who wants it.
What the endpoint refuses
create_postuses the public form's rules, so a two-character title is refused with the same
sentence a visitor would see.
- An unknown status names the valid ones. An agent told only "invalid" learns nothing.
post_detaildoes not return author email addresses. An agent reading a public board has no
business collecting the address of everyone who filed a bug.
Verified voting
With Settings → Voting → require an email and confirm the address first, a vote is held
until a link sent to that address is clicked (GET /vote/verify/{token}, single use, two-day life).
The anonymity property is preserved, and it is the point. The voter key is a salted hash of the
address; the verification row keys on that same hash and has no email column. So the database can
answer "did someone at this address verify?" and can never answer "which address voted for what".
The audit trail redacts the voter key for the same reason.
Scheduled changelog releases
An entry whose published_at is in the future is not public yet. The public page, the widget and
the release feed all read Board::changelog(true), which now compares the date against now — so a
scheduled entry becomes visible through the same query an immediate one does. There is no second
publish path to keep in step.
Scheduled backup
GET /backup/scheduled?t=<token>
A token-guarded URL for cron. The token is minted once in Backup, shown once, stored only as a
SHA-256 hash. It writes a dated JSON backup, keeps the newest N and deletes older ones. Password
hashes, API-key hashes, webhook secrets, the OIDC client secret, the backup and calendar tokens and
2FA seeds are blanked — the same list /backup.json uses.
Release calendar
GET /feed/releases.ics?t=<token>
An iCalendar feed of changelog releases, published and scheduled, with stable UIDs.