Docs › REST API
The Kjerr feedback API: REST endpoints and webhooks
Read and change your feedback board from your own code: ideas, statuses, comments, votes, people, companies, topics and announcements. Webhooks tell your server when something changes. Every workspace has it, at no cost.
On this page: Keys · Conventions · Workspace · Ideas · Comments · Votes · People · Companies · Topics, tags and statuses · Announcements · Webhooks · Coming from Canny
Keys
An admin makes a key under Settings → API. It is shown once, so copy it straight into your server's secrets. Make one key per tool; revoking one leaves the others working. A key can be read only, which allows the list and retrieve calls only.
A key belongs to one workspace and acts as the teammate who made it: a status change it makes emails voters with that teammate's name and Reply-To, the way the inbox would, and the key stops working if that teammate leaves the team or stops being an admin. Keep keys on your server; never put them in a web page or a mobile app.
curl https://kjerr.io/api/v1/ideas/list \
-H "Authorization: Bearer $KJERR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "planned", "sort": "votes", "limit": 10}'
Conventions
- Base URL.
https://kjerr.io/api/v1/{resource}/{action}. Every call is aPOST; the read calls also answerGETwith the parameters in the query string. - The key.
Authorization: Bearer kjerr_sk_…, or anapiKeyfield in the body, as Canny takes it. - Bodies. JSON (
Content-Type: application/json) or an HTML form. Lists can be JSON arrays or comma-separated text. - Paging.
limit(1 to 100, default 20) andskip; list calls answerhasMore. - Times. ISO 8601 in UTC, like
2026-10-20T09:30:12Z. Ids are integers. - Errors.
{"error": "…"}with a sentence you can show a person, and the status: 400 bad parameters, 401 bad or revoked key, 403 read-only key, 404 nothing with that id, 405 wrong method, 429 over the rate limit. - Rate limits. Per key: 10 per second, 300 per minute, 5000 per hour. Every answer carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a 429 carriesRetry-Afterin seconds. For a large backfill, pace your calls to stay under the hourly limit. - People the API names are trusted. An email you send for an author, voter or commenter counts as confirmed, as with single sign-on, so those people get status and reply emails without a confirmation step.
The idea object
{
"id": 482,
"title": "Export ideas to CSV",
"details": "We review feedback in a spreadsheet every Friday.",
"fields": {"Company size": "50-200"},
"status": {"key": "planned", "name": "Planned"},
"voteCount": 31,
"commentCount": 4,
"source": "portal",
"author": {"id": 77, "email": "ann@example.com", "name": "Ann", "externalId": "u_1029", ...},
"topics": [{"id": 3, "name": "Reporting"}],
"tags": ["enterprise"],
"mergedIntoId": null,
"deleted": false,
"spam": false,
"url": "https://yourname.kjerr.io/i/482-export-ideas-to-csv",
"adminURL": "https://kjerr.io/w/yourname/admin?id=482",
"createdAt": "2026-09-02T08:14:55Z",
"updatedAt": "2026-10-01T16:02:11Z",
"statusChangedAt": "2026-10-01T16:02:11Z"
}
curl https://kjerr.io/api/v1/ideas/change_status \
-H "Authorization: Bearer $KJERR_API_KEY" \
-d id=482 -d status=done -d note="Shipped in 4.2, see the changelog."
Workspace
POST /api/v1/workspace/retrieve
read
The workspace the key belongs to.
Returns workspace.
Ideas
POST /api/v1/ideas/list
read
Ideas, newest first unless sorted otherwise. Merged ideas are left out.
| Parameter | Type | Notes |
|---|---|---|
status | string or list | Status keys or names, e.g. planned |
topicId | integer | |
tag | string | |
authorId | integer | A person id |
companyId | integer | Ideas by that company's people |
source | string | portal, team, import, api or meeting |
search | string | Matches title and details |
createdAfter | ISO 8601 | |
sort | string | newest (default), oldest, votes, statusChanged, activity |
includeDeleted | boolean | Default false |
limit | integer | 1 to 100, default 20 |
skip | integer | Default 0 |
Returns {ideas: [idea], hasMore}.
POST /api/v1/ideas/retrieve
read
One idea, deleted ones included.
| Parameter | Type | Notes |
|---|---|---|
id required | integer |
Returns idea.
POST /api/v1/ideas/create
Post an idea for a customer, or with no author.
| Parameter | Type | Notes |
|---|---|---|
title required | string | 3 to 300 characters |
details | string | |
authorId | integer | An existing person |
authorEmail | string | Found or created; the author votes for their idea |
authorName | string | Used when the person is created |
topicIds | list of integers | |
tags | list of strings | |
status | string | Starts in this status, without emails |
Returns idea.
POST /api/v1/ideas/update
Change the text, topics or tags. Fields you leave out stay as they are.
| Parameter | Type | Notes |
|---|---|---|
id required | integer | |
title | string | |
details | string | |
topicIds | list of integers | Replaces the topics |
tags | list of strings | Replaces the tags |
Returns idea.
POST /api/v1/ideas/change_status
Move an idea, exactly as the inbox does: voters and the author are emailed unless notify is false or the workspace turned status emails off.
| Parameter | Type | Notes |
|---|---|---|
id required | integer | |
status required | string | Key or name |
note | string | Public note, shown on the idea and in the email |
notify | boolean | Default true |
Returns idea + {changed, emailed}.
POST /api/v1/ideas/add_tag
Tags are team-only labels; a new name creates the tag.
| Parameter | Type | Notes |
|---|---|---|
id required | integer | |
tag required | string |
Returns idea.
POST /api/v1/ideas/remove_tag
| Parameter | Type | Notes |
|---|---|---|
id required | integer | |
tag required | string |
Returns idea.
POST /api/v1/ideas/merge
Merge a duplicate into another idea: its votes, comments and files move over.
| Parameter | Type | Notes |
|---|---|---|
id required | integer | The duplicate |
intoId required | integer | The idea that stays |
notify | boolean | Email the duplicate's author; default true |
Returns the idea that stays.
POST /api/v1/ideas/delete
Move an idea to the trash (it can be restored in the inbox).
| Parameter | Type | Notes |
|---|---|---|
id required | integer | |
spam | boolean |
Returns {success}.
Comments
POST /api/v1/comments/list
read
Newest first. Internal notes only when asked for.
| Parameter | Type | Notes |
|---|---|---|
ideaId | integer | |
authorId | integer | A person id |
includeInternal | boolean | Default false |
limit | integer | |
skip | integer |
Returns {comments: [comment], hasMore}.
POST /api/v1/comments/retrieve
read
| Parameter | Type | Notes |
|---|---|---|
id required | integer |
Returns comment.
POST /api/v1/comments/create
With authorId or authorEmail it is the customer's comment. Without, it is a team reply from the key's teammate, or an internal note with internal: true.
| Parameter | Type | Notes |
|---|---|---|
ideaId required | integer | |
body required | string | Up to 5,000 characters |
authorId | integer | |
authorEmail | string | |
authorName | string | |
internal | boolean | Team-only note |
notify | boolean | Email the people following the idea; default true |
Returns comment.
POST /api/v1/comments/delete
| Parameter | Type | Notes |
|---|---|---|
id required | integer |
Returns {success}.
Votes
POST /api/v1/votes/list
read
| Parameter | Type | Notes |
|---|---|---|
ideaId | integer | |
personId | integer | |
limit | integer | |
skip | integer |
Returns {votes: [vote], hasMore}.
POST /api/v1/votes/create
Vote on behalf of a customer, for example from your support tool.
| Parameter | Type | Notes |
|---|---|---|
ideaId required | integer | |
personId | integer | |
personEmail | string | Found or created |
personName | string |
Returns {created, ideaId, personId, voteCount}.
POST /api/v1/votes/delete
| Parameter | Type | Notes |
|---|---|---|
ideaId required | integer | |
personId | integer | |
personEmail | string |
Returns {deleted, ideaId, personId}.
People
POST /api/v1/people/list
read
Everyone who posted, voted, commented or was identified.
| Parameter | Type | Notes |
|---|---|---|
search | string | Email or name |
limit | integer | |
skip | integer |
Returns {people: [person], hasMore}.
POST /api/v1/people/retrieve
read
By one of id, externalId or email. Includes their companies.
| Parameter | Type | Notes |
|---|---|---|
id | integer | |
externalId | string | Your user id |
email | string |
Returns person + {companies}.
POST /api/v1/people/create_or_update
The Identify call, with a key instead of a signature. externalId (your user id) is the stable match; without it the email is.
| Parameter | Type | Notes |
|---|---|---|
externalId | string | |
email | string | |
name | string | |
avatarURL | string | |
language | string | e.g. de |
country | string | e.g. DE |
plan | string | |
mrr | number | Monthly revenue, whole units |
company | object | {id, name, domain, plan, mrr}, with externalId only |
Returns person + {companies}.
POST /api/v1/people/delete
Removes the person and their personal data. Their ideas and comments stay, unattributed; their votes go.
| Parameter | Type | Notes |
|---|---|---|
id | integer | |
externalId | string | |
email | string |
Returns {success}.
Companies
POST /api/v1/companies/list
read
| Parameter | Type | Notes |
|---|---|---|
search | string | Name or domain |
limit | integer | |
skip | integer |
Returns {companies: [company], hasMore}.
POST /api/v1/companies/retrieve
read
| Parameter | Type | Notes |
|---|---|---|
id | integer | |
externalId | string | |
domain | string |
Returns company.
POST /api/v1/companies/create_or_update
Matched by externalId, then domain; created otherwise.
| Parameter | Type | Notes |
|---|---|---|
externalId | string | Your company id |
name | string | |
domain | string | |
plan | string | |
mrr | number |
Returns company.
POST /api/v1/companies/delete
| Parameter | Type | Notes |
|---|---|---|
id | integer | |
externalId | string | |
domain | string |
Returns {success}.
Topics, tags and statuses
POST /api/v1/topics/list
read
The public topics voters put on ideas.
Returns {topics: [{id, name}]}.
POST /api/v1/topics/create
| Parameter | Type | Notes |
|---|---|---|
name required | string |
Returns {id, name}.
POST /api/v1/topics/delete
| Parameter | Type | Notes |
|---|---|---|
id required | integer |
Returns {success}.
POST /api/v1/tags/list
read
Team-only tags in use.
Returns {tags: [{name, ideaCount}]}.
POST /api/v1/statuses/list
read
The workspace's statuses in board order.
Returns {statuses: [{key, name, color, onRoadmap}]}.
POST /api/v1/status_changes/list
read
Every status change, newest first.
| Parameter | Type | Notes |
|---|---|---|
ideaId | integer | |
limit | integer | |
skip | integer |
Returns {statusChanges: [{ideaId, from, to, by, createdAt}], hasMore}.
Announcements
POST /api/v1/announcements/list
read
Published posts, newest first.
| Parameter | Type | Notes |
|---|---|---|
includeDrafts | boolean | |
limit | integer | |
skip | integer |
Returns {announcements: [announcement], hasMore}.
POST /api/v1/announcements/retrieve
read
| Parameter | Type | Notes |
|---|---|---|
id required | integer |
Returns announcement.
POST /api/v1/announcements/create
Write a changelog post, as a draft or published.
| Parameter | Type | Notes |
|---|---|---|
title required | string | |
body | string | The board's formatting: **bold**, lists, links |
category | string | announcement (default), feature, improvement or fix |
ideaIds | list of integers | Ideas this post ships |
publish | boolean | Default false: a draft |
notify | boolean | Email subscribers when publishing; default false |
Returns announcement.
Webhooks
Add an endpoint under Settings → API and pick its events. We send a
POST with a JSON body whenever one happens, from any source: the board, the inbox, an import or the
API. Answer with any 2xx status within 10 seconds. Anything else is retried after 1 minute, 5 minutes,
30 minutes, 2, 5 and 10 hours, then given up; the settings page shows the recent deliveries of each endpoint.
Endpoints must be public https:// addresses, and redirects are not followed.
| Event | When |
|---|---|
idea.created | An idea is posted, by a customer, a teammate, an import or the API |
idea.status_changed | An idea moves to another status |
idea.deleted | An idea is deleted or marked as spam |
comment.created | A comment, team reply or internal note is added |
comment.deleted | A comment is deleted |
vote.created | Someone votes for an idea |
vote.deleted | A vote is taken back |
nps.response | Someone answers the NPS survey (after the follow-up, when there is one) |
{
"type": "idea.status_changed",
"timestamp": "2026-10-20T09:30:12Z",
"data": {
"previousStatus": "planned",
"idea": { ...the idea object... }
}
}
Idea events carry the idea object; comment events carry comment; vote events carry
vote, person, ideaTitle and the new voteCount. The
Send test button posts a ping.
Checking the signature
Deliveries follow Standard Webhooks: headers
webhook-id (the same on every retry, so you can skip duplicates), webhook-timestamp and
webhook-signature. The secret (whsec_…) is on the settings page. When you rotate it, the old
one keeps signing next to the new one for 24 hours; Revoke now stops it at once.
# pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["KJERR_WEBHOOK_SECRET"]) # whsec_...
event = wh.verify(request.body, request.headers) # raises if forged or stale
// npm i standardwebhooks
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.KJERR_WEBHOOK_SECRET); // whsec_...
const event = wh.verify(rawBody, req.headers); // throws if forged or stale
import base64, hashlib, hmac
def valid(secret, headers, body: bytes) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + body
expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split())
Coming from the Canny API
The shape is the same: POST /api/v1/resource/action, a key in the body if you like,
skip and limit, {"error"}. The words are Kjerr's:
| Canny | Kjerr |
|---|---|
posts/*, postID | ideas/*, id / ideaId |
categories/* | topics/* |
users/*, userID | people/*, externalId |
entries/* (changelog) | announcements/* |
boards/* | workspace/retrieve: one board per workspace |
post.created, post.status_changed | idea.created, idea.status_changed |
Not in Kjerr's API: Jira links, opportunities, ETA fields and roadmaps (the roadmap is the statuses). Read-only access for AI assistants is the MCP server.
Questions: hello@kjerr.io