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.

A first call
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

The idea object

idea
{
  "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"
}
Mark an idea done and email everyone who asked for it
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.

ParameterTypeNotes
statusstring or listStatus keys or names, e.g. planned
topicIdinteger
tagstring
authorIdintegerA person id
companyIdintegerIdeas by that company's people
sourcestringportal, team, import, api or meeting
searchstringMatches title and details
createdAfterISO 8601
sortstringnewest (default), oldest, votes, statusChanged, activity
includeDeletedbooleanDefault false
limitinteger1 to 100, default 20
skipintegerDefault 0

Returns {ideas: [idea], hasMore}.

POST /api/v1/ideas/retrieve read

One idea, deleted ones included.

ParameterTypeNotes
id requiredinteger

Returns idea.

POST /api/v1/ideas/create

Post an idea for a customer, or with no author.

ParameterTypeNotes
title requiredstring3 to 300 characters
detailsstring
authorIdintegerAn existing person
authorEmailstringFound or created; the author votes for their idea
authorNamestringUsed when the person is created
topicIdslist of integers
tagslist of strings
statusstringStarts 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.

ParameterTypeNotes
id requiredinteger
titlestring
detailsstring
topicIdslist of integersReplaces the topics
tagslist of stringsReplaces 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.

ParameterTypeNotes
id requiredinteger
status requiredstringKey or name
notestringPublic note, shown on the idea and in the email
notifybooleanDefault true

Returns idea + {changed, emailed}.

POST /api/v1/ideas/add_tag

Tags are team-only labels; a new name creates the tag.

ParameterTypeNotes
id requiredinteger
tag requiredstring

Returns idea.

POST /api/v1/ideas/remove_tag

ParameterTypeNotes
id requiredinteger
tag requiredstring

Returns idea.

POST /api/v1/ideas/merge

Merge a duplicate into another idea: its votes, comments and files move over.

ParameterTypeNotes
id requiredintegerThe duplicate
intoId requiredintegerThe idea that stays
notifybooleanEmail 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).

ParameterTypeNotes
id requiredinteger
spamboolean

Returns {success}.

Comments

POST /api/v1/comments/list read

Newest first. Internal notes only when asked for.

ParameterTypeNotes
ideaIdinteger
authorIdintegerA person id
includeInternalbooleanDefault false
limitinteger
skipinteger

Returns {comments: [comment], hasMore}.

POST /api/v1/comments/retrieve read

ParameterTypeNotes
id requiredinteger

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.

ParameterTypeNotes
ideaId requiredinteger
body requiredstringUp to 5,000 characters
authorIdinteger
authorEmailstring
authorNamestring
internalbooleanTeam-only note
notifybooleanEmail the people following the idea; default true

Returns comment.

POST /api/v1/comments/delete

ParameterTypeNotes
id requiredinteger

Returns {success}.

Votes

POST /api/v1/votes/list read

ParameterTypeNotes
ideaIdinteger
personIdinteger
limitinteger
skipinteger

Returns {votes: [vote], hasMore}.

POST /api/v1/votes/create

Vote on behalf of a customer, for example from your support tool.

ParameterTypeNotes
ideaId requiredinteger
personIdinteger
personEmailstringFound or created
personNamestring

Returns {created, ideaId, personId, voteCount}.

POST /api/v1/votes/delete

ParameterTypeNotes
ideaId requiredinteger
personIdinteger
personEmailstring

Returns {deleted, ideaId, personId}.

People

POST /api/v1/people/list read

Everyone who posted, voted, commented or was identified.

ParameterTypeNotes
searchstringEmail or name
limitinteger
skipinteger

Returns {people: [person], hasMore}.

POST /api/v1/people/retrieve read

By one of id, externalId or email. Includes their companies.

ParameterTypeNotes
idinteger
externalIdstringYour user id
emailstring

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.

ParameterTypeNotes
externalIdstring
emailstring
namestring
avatarURLstring
languagestringe.g. de
countrystringe.g. DE
planstring
mrrnumberMonthly revenue, whole units
companyobject{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.

ParameterTypeNotes
idinteger
externalIdstring
emailstring

Returns {success}.

Companies

POST /api/v1/companies/list read

ParameterTypeNotes
searchstringName or domain
limitinteger
skipinteger

Returns {companies: [company], hasMore}.

POST /api/v1/companies/retrieve read

ParameterTypeNotes
idinteger
externalIdstring
domainstring

Returns company.

POST /api/v1/companies/create_or_update

Matched by externalId, then domain; created otherwise.

ParameterTypeNotes
externalIdstringYour company id
namestring
domainstring
planstring
mrrnumber

Returns company.

POST /api/v1/companies/delete

ParameterTypeNotes
idinteger
externalIdstring
domainstring

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

ParameterTypeNotes
name requiredstring

Returns {id, name}.

POST /api/v1/topics/delete

ParameterTypeNotes
id requiredinteger

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.

ParameterTypeNotes
ideaIdinteger
limitinteger
skipinteger

Returns {statusChanges: [{ideaId, from, to, by, createdAt}], hasMore}.

Announcements

POST /api/v1/announcements/list read

Published posts, newest first.

ParameterTypeNotes
includeDraftsboolean
limitinteger
skipinteger

Returns {announcements: [announcement], hasMore}.

POST /api/v1/announcements/retrieve read

ParameterTypeNotes
id requiredinteger

Returns announcement.

POST /api/v1/announcements/create

Write a changelog post, as a draft or published.

ParameterTypeNotes
title requiredstring
bodystringThe board's formatting: **bold**, lists, links
categorystringannouncement (default), feature, improvement or fix
ideaIdslist of integersIdeas this post ships
publishbooleanDefault false: a draft
notifybooleanEmail 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.

EventWhen
idea.createdAn idea is posted, by a customer, a teammate, an import or the API
idea.status_changedAn idea moves to another status
idea.deletedAn idea is deleted or marked as spam
comment.createdA comment, team reply or internal note is added
comment.deletedA comment is deleted
vote.createdSomeone votes for an idea
vote.deletedA vote is taken back
nps.responseSomeone answers the NPS survey (after the follow-up, when there is one)
A delivery
{
  "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.

Python
# 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
Node
// 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
Without a library
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:

CannyKjerr
posts/*, postIDideas/*, id / ideaId
categories/*topics/*
users/*, userIDpeople/*, externalId
entries/* (changelog)announcements/*
boards/*workspace/retrieve: one board per workspace
post.created, post.status_changedidea.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