Skip to content

REST API

Everything you do in the dashboard, over HTTP and JSON. The API and the MCP server share the same registry of 84 operations and the same keys. Available on Starter and Pro.

Authentication

Create a key in Settings → API. It's shown only once: store it in your secrets manager. Send it on every request in the Authorization: Bearer pk_live_… header.

Base URL: https://jotly.si/api/v1

List requests
curl "https://jotly.si/api/v1/requests?statuses=planned,in_progress&sort=top&limit=10" \  -H "Authorization: Bearer pk_live_YOUR_KEY"
Each key has full access to its workspace. Create one per integration or agent so you can revoke it without affecting the others.

Create and respond

Path parameters go in the URL; for GET and DELETE everything else goes in the query string, and for POST, PATCH and PUT in a JSON body. Every resource comes wrapped in its name ({ request }, { board }, { member }). Creating returns 201; relationships (vote, subscribe, tag) return { ok: true } and deletes return 204 with no body.

Create a request on behalf of a member
curl -X POST "https://jotly.si/api/v1/requests" \  -H "Authorization: Bearer pk_live_YOUR_KEY" \  -H "Content-Type: application/json" \  -d '{    "board_id": "brd_123",    "title": "Automatic monthly export",    "body": "Send invoices to the accountant automatically.",    "author": { "external_id": "u_812" }  }'
201
HTTP/1.1 201 CreatedX-Request-Id: req_8f2k1x0c9q7m3v5b6n4z {  "request": {    "id": "post_4j8w2q",    "number": 42,    "title": "Automatic monthly export",    "status": "pending",    "board": { "id": "brd_123", "name": "Features", "type": "public" },    "author": { "id": "cus_9x1", "external_id": "u_812", "type": "user", "first_name": "Lucía" },    "tags": [],    "attachments": [],    "vote_count": 1,    "comment_count": 0  }}

Members (author, voter, subscriber, actor, member) are referenced with { id } or { external_id } (your user id). Jotly also accepts { email }; with external_id or email, the member is created if it doesn't exist. The actor of a reply or a status change must be on your team; without one, whoever created the key acts.

Pagination

Every list returns { <resource>: [...], next_cursor }. Request the next page with cursor set to the previous page's next_cursor; when it's null, there are no more. limit ranges from 1 to 100 (10 for requests and changelog, 20 for conversations and members, 50 for votes and the timeline). The cursor is opaque: don't build it by hand.

Idempotency

POST /requests, POST /changelogs, POST /assets/import, and conversation replies and notes accept the Idempotency-Key header. For 24 hours, repeating the request with the same key and the same body returns the original response (with the Idempotent-Replayed: true header) instead of creating it again.

An agent that retries without duplicating replies
curl -X POST "https://jotly.si/api/v1/conversations/cnv_123/replies" \  -H "Authorization: Bearer pk_live_YOUR_KEY" \  -H "Idempotency-Key: run-2026-10-19-cnv_123" \  -H "Content-Type: application/json" \  -d '{ "body": "Done, all fixed now.", "actor": { "external_id": "support-agent" } }'
  • The same key with a different body returns 409 idempotency_key_reused.
  • If the first request is still in progress, 409 idempotency_in_progress: retry in a few seconds.
  • If the request fails, the key is released and you can retry with it.

Rate limits

Each key has a per-minute limit shared by the REST API and the MCP server (120 by default). Past it, the response is 429 with the Retry-After header and the wait in seconds in details.retry_after_seconds.

429
HTTP/1.1 429 Too Many RequestsRetry-After: 23 {  "error": {    "code": "rate_limited",    "message": "Too many requests: the limit is 120 per minute per key",    "details": { "retry_after_seconds": 23 }  }}

Errors

Errors respond with the matching HTTP status and { error: { code, message, details? } }. Every response, errors included, carries the X-Request-Id header: include it if you contact support.

401
{  "error": {    "code": "invalid_api_token",    "message": "Invalid API token"  }}
HTTPCodes
400invalid_request
401invalid_api_token
404not_found
409board_name_conflict, board_not_empty, tag_name_conflict, member_email_conflict, member_identity_conflict, member_not_erasable, member_erased, company_identity_conflict, conversation_status_conflict, idempotency_key_reused, idempotency_in_progress
410member_erased
422validation_error (with details.issues), invalid_parent_comment, invalid_changelog_author, invalid_conversation_actor, invalid_conversation_assignee, invalid_attachment, invalid_asset
429rate_limited

Members and companies

POST /members/sync creates or updates a member by external_id; their traits are merged (a trait set to null is removed) and companies creates or updates their companies and links the member to them. The widget's identify accepts the same companies. DELETE /members/:member_id erases their personal data and keeps their requests, comments and votes; after that, reading or deleting the member again returns 410 member_erased.

Attachments and files: Jotly doesn't have file storage yet, so attachment_urls, addRequestAttachments, addCommentAttachments, POST /assets and POST /assets/import respond 422 invalid_attachment or invalid_asset. Listing and removing attachments does work.

Operations

Every parameter is detailed in the OpenAPI spec. You can import it into Postman or Insomnia, or generate a client from it.

Boards

MethodPathWhat it does
GET/boardslistBoardsLists the workspace's feedback boards, in order.
POST/boardscreateBoardCreates a feedback board. 409 board_name_conflict if one with that name already exists.
GET/boards/:board_idgetBoardGets a board.
PATCH/boards/:board_idupdateBoardUpdates a board's name, color, description or visibility (public or private).
DELETE/boards/:board_iddeleteBoardDeletes an empty board. 409 board_not_empty if it still has requests.

Requests

MethodPathWhat it does
GET/requests/tagslistRequestTagsLists the workspace's request tags.
POST/requests/tagscreateRequestTagCreates a request tag. 409 tag_name_conflict if it already exists.
DELETE/requests/tags/:tag_iddeleteRequestTagDeletes a request tag and removes it from every request.
GET/requestslistRequestsLists requests with filters and sorting. sort: newest, oldest, top, trending or status_updated.
POST/requestscreateRequestCreates a request on behalf of a member (author { id } or { external_id }). Accepts Idempotency-Key.
GET/requests/:request_idgetRequestGets a request by id or number.
PATCH/requests/:request_idupdateRequestUpdates a request's title or body (Jotly also accepts hidden).
DELETE/requests/:request_iddeleteRequestDeletes a request along with its votes and comments.
GET/requests/:request_id/similarlistSimilarRequestsRequests similar to another one (or to a text in query), to catch duplicates. similarity ranges from 0 to 1.
PATCH/requests/:request_id/statusupdateRequestStatusChanges the status and emails the author, voters and followers. Optional actor: who makes the change (a team member).
PATCH/requests/:request_id/boardmoveRequestBoardMoves a request to another board.
POST/requests/:request_id/mergemergeRequestMerges a duplicate request into the canonical one (into_request_id). Votes and comments move to the canonical request.
GET/requests/:request_id/voteslistRequestVotesLists who voted on a request, newest vote first.
POST/requests/:request_id/votesaddRequestVoteVotes on a request on behalf of a member (for example, after a sales call).
DELETE/requests/:request_id/votesdeleteRequestVoteRemoves a member's vote (voter_id or voter_external_id).
GET/requests/:request_id/subscriptionslistRequestSubscriptionsLists who follows a request (and gets its notifications).
POST/requests/:request_id/subscriptionsaddRequestSubscriptionSubscribes a member to a request's notifications.
DELETE/requests/:request_id/subscriptionsdeleteRequestSubscriptionRemoves a member's subscription (subscriber_id or subscriber_external_id).
GET/requests/:request_id/tagslistRequestTagAssignmentsLists a request's tags.
POST/requests/:request_id/tagsaddRequestTagsAdds tags to a request without removing the ones it already has.
DELETE/requests/:request_id/tags/:tag_iddeleteRequestTagAssignmentRemoves a tag from a request.
PUT/requests/:request_id/tagssetRequestTagsReplaces all of a request's tags (Jotly).
GET/requests/:request_id/commentslistRequestCommentsLists a request's comments in one-level threads (replies), pinned first. Includes internal notes.
POST/requests/:request_id/commentscreateRequestCommentComments on a request on behalf of a member (author). Without author, the team comments. visibility internal = a note the customer doesn't see. A reply to a reply stays in the same thread.
GET/requests/:request_id/comments/:comment_idgetCommentGets a comment.
PATCH/requests/:request_id/comments/:comment_idupdateCommentEdits a comment's text.
DELETE/requests/:request_id/comments/:comment_iddeleteCommentDeletes a comment and its replies.
GET/requests/:request_id/comments/:comment_id/voteslistCommentVotesLists who voted on a comment.
POST/requests/:request_id/comments/:comment_id/votesaddCommentVoteVotes on a comment on behalf of a member.
DELETE/requests/:request_id/comments/:comment_id/votesdeleteCommentVoteRemoves a member's vote on a comment (voter_id or voter_external_id).
PUT/requests/:request_id/comments/:comment_id/pinpinCommentPins a comment to the top of the thread.
DELETE/requests/:request_id/comments/:comment_id/pinunpinCommentUnpins a comment.
GET/requests/:request_id/attachmentslistRequestAttachmentsLists a request's attached images (signed URLs that expire in one hour).
POST/requests/:request_id/attachmentsaddRequestAttachmentsAttaches images to a request from URLs (up to 4 per request).
DELETE/requests/:request_id/attachments/:attachment_iddeleteRequestAttachmentRemoves an attachment from a request.
GET/requests/:request_id/comments/:comment_id/attachmentslistCommentAttachmentsLists a comment's attached images.
POST/requests/:request_id/comments/:comment_id/attachmentsaddCommentAttachmentsAttaches images to a comment from URLs (up to 4 per comment).
DELETE/requests/:request_id/comments/:comment_id/attachments/:attachment_iddeleteCommentAttachmentRemoves an attachment from a comment.

Files

MethodPathWhat it does
POST/assetscreateAssetUploads an image (multipart: file, name). Without file storage configured, it responds invalid_asset.
POST/assets/importimportAssetImports an image from a public HTTPS URL (up to 3 redirects, 10 MiB, PNG, JPEG, GIF or WebP). Returns the asset with its permanent URL.
GET/assets/:asset_idgetAssetGets a workspace asset.

Changelog

MethodPathWhat it does
GET/changelogs/tagslistChangelogTagsLists changelog tags (New feature, Improvement, Fix...).
POST/changelogs/tagscreateChangelogTagCreates a changelog tag. 409 tag_name_conflict if it already exists.
DELETE/changelogs/tags/:tag_iddeleteChangelogTagDeletes a changelog tag and removes it from every entry.
GET/changelogslistChangelogsLists changelog entries. status: draft, scheduled or published.
POST/changelogscreateChangelogCreates a changelog entry. It stays a draft unless status=published (or a future publish_at to schedule it). Without authors, the key's creator signs it. Accepts Idempotency-Key.
GET/changelogs/:changelog_idgetChangelogGets a changelog entry with its tags, authors and linked requests.
PATCH/changelogs/:changelog_idupdateChangelogEdits an entry; status publishes it, schedules it (with publish_at) or moves it back to draft.
DELETE/changelogs/:changelog_iddeleteChangelogDeletes a changelog entry.
GET/changelogs/:changelog_id/tagslistChangelogTagAssignmentsLists a changelog entry's tags.
POST/changelogs/:changelog_id/tagsaddChangelogTagsAdds tags to an entry without removing the ones it already has.
DELETE/changelogs/:changelog_id/tags/:tag_iddeleteChangelogTagAssignmentRemoves a tag from an entry.
GET/changelogs/:changelog_id/authorslistChangelogAuthorsLists an entry's authors.
POST/changelogs/:changelog_id/authorsaddChangelogAuthorsAdds authors to an entry. They must be team members (invalid_changelog_author otherwise).
DELETE/changelogs/:changelog_id/authors/:member_iddeleteChangelogAuthorRemoves an author from an entry.
GET/changelogs/:changelog_id/requestslistChangelogRequestsLists the requests an entry closes.
POST/changelogs/:changelog_id/requestsaddChangelogRequestsLinks requests (by id or number) to an entry; their voters see that it shipped.
DELETE/changelogs/:changelog_id/requests/:request_iddeleteChangelogRequestUnlinks a request from an entry.

Members

MethodPathWhat it does
GET/memberslistMembersLists members (people who vote, post or chat). type: user, admin or owner. company_external_id filters by company.
GET/members/lookuplookupMemberFinds a member by external_id or email.
POST/members/syncsyncMemberCreates or updates a member by external_id (your user id). Traits are merged. companies[] creates or updates their companies and links them.
GET/members/:member_idgetMemberGets a member. 410 member_erased if their data was erased.
PATCH/members/:member_idupdateMemberEdits a member: external_id, email, name, avatar or traits (merged; a trait set to null is removed).
DELETE/members/:member_iddeleteMemberErases a member's personal data (email, name, avatar, external_id, traits and companies). Their requests, comments and votes stay. 410 if already erased; 409 member_not_erasable if they're on the team.

Companies

MethodPathWhat it does
POST/companies/syncsyncCompanyCreates or updates a company by external_id. name is required when creating it; traits are merged.
GET/companieslistCompaniesLists companies. Filter by search (name or domain), external_id or domain.
GET/companies/lookuplookupCompanyFinds a company by external_id.
GET/companies/:company_idgetCompanyGets a company.
PATCH/companies/:company_idupdateCompanyEdits a company. 409 company_identity_conflict if the new external_id already belongs to another one.
GET/companies/:company_id/memberslistCompanyMembersLists a company's members.
POST/companies/:company_id/membersaddCompanyMemberLinks a member ({ id } or { external_id }) to a company.
DELETE/companies/:company_id/members/:member_iddeleteCompanyMemberUnlinks a member from a company.

Conversations

MethodPathWhat it does
GET/conversationslistConversationsLists conversations. inbox_state: needs_team (waiting on the team), ai_handling (handled by the AI) or waiting_on_customer. For scheduled agents: updated_since + inbox_state=needs_team.
GET/conversations/:conversation_idgetConversationGets a conversation. Its messages are in listConversationTimeline.
GET/conversations/:conversation_id/timelinelistConversationTimelineA conversation's timeline, oldest to newest. types: message, note, ai_handoff, resolved, reopened, assigned, unassigned.
POST/conversations/:conversation_id/repliescreateConversationReplyReplies to the customer as a team member (actor; without actor, the key's creator). The AI steps out of the conversation; it reopens if it was resolved. Accepts Idempotency-Key.
POST/conversations/:conversation_id/notescreateConversationNoteAdds an internal note (the customer doesn't see it). Accepts Idempotency-Key.
PATCH/conversations/:conversation_id/statusupdateConversationStatusResolves or reopens a conversation. 409 conversation_status_conflict if it's archived or marked as spam.
PATCH/conversations/:conversation_id/assigneeupdateConversationAssigneeAssigns the conversation to a team member (assignee { id } or { external_id }, or assignee_email) or unassigns it with null.

See also MCP server