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
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.
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" } }'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.
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.
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.
{ "error": { "code": "invalid_api_token", "message": "Invalid API token" }}| HTTP | Codes |
|---|---|
| 400 | invalid_request |
| 401 | invalid_api_token |
| 404 | not_found |
| 409 | board_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 |
| 410 | member_erased |
| 422 | validation_error (with details.issues), invalid_parent_comment, invalid_changelog_author, invalid_conversation_actor, invalid_conversation_assignee, invalid_attachment, invalid_asset |
| 429 | rate_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, soattachment_urls,addRequestAttachments,addCommentAttachments,POST /assetsandPOST /assets/importrespond422 invalid_attachmentorinvalid_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
| Method | Path | What it does |
|---|---|---|
| GET | /boards | listBoardsLists the workspace's feedback boards, in order. |
| POST | /boards | createBoardCreates a feedback board. 409 board_name_conflict if one with that name already exists. |
| GET | /boards/:board_id | getBoardGets a board. |
| PATCH | /boards/:board_id | updateBoardUpdates a board's name, color, description or visibility (public or private). |
| DELETE | /boards/:board_id | deleteBoardDeletes an empty board. 409 board_not_empty if it still has requests. |
Requests
| Method | Path | What it does |
|---|---|---|
| GET | /requests/tags | listRequestTagsLists the workspace's request tags. |
| POST | /requests/tags | createRequestTagCreates a request tag. 409 tag_name_conflict if it already exists. |
| DELETE | /requests/tags/:tag_id | deleteRequestTagDeletes a request tag and removes it from every request. |
| GET | /requests | listRequestsLists requests with filters and sorting. sort: newest, oldest, top, trending or status_updated. |
| POST | /requests | createRequestCreates a request on behalf of a member (author { id } or { external_id }). Accepts Idempotency-Key. |
| GET | /requests/:request_id | getRequestGets a request by id or number. |
| PATCH | /requests/:request_id | updateRequestUpdates a request's title or body (Jotly also accepts hidden). |
| DELETE | /requests/:request_id | deleteRequestDeletes a request along with its votes and comments. |
| GET | /requests/:request_id/similar | listSimilarRequestsRequests similar to another one (or to a text in query), to catch duplicates. similarity ranges from 0 to 1. |
| PATCH | /requests/:request_id/status | updateRequestStatusChanges the status and emails the author, voters and followers. Optional actor: who makes the change (a team member). |
| PATCH | /requests/:request_id/board | moveRequestBoardMoves a request to another board. |
| POST | /requests/:request_id/merge | mergeRequestMerges a duplicate request into the canonical one (into_request_id). Votes and comments move to the canonical request. |
| GET | /requests/:request_id/votes | listRequestVotesLists who voted on a request, newest vote first. |
| POST | /requests/:request_id/votes | addRequestVoteVotes on a request on behalf of a member (for example, after a sales call). |
| DELETE | /requests/:request_id/votes | deleteRequestVoteRemoves a member's vote (voter_id or voter_external_id). |
| GET | /requests/:request_id/subscriptions | listRequestSubscriptionsLists who follows a request (and gets its notifications). |
| POST | /requests/:request_id/subscriptions | addRequestSubscriptionSubscribes a member to a request's notifications. |
| DELETE | /requests/:request_id/subscriptions | deleteRequestSubscriptionRemoves a member's subscription (subscriber_id or subscriber_external_id). |
| GET | /requests/:request_id/tags | listRequestTagAssignmentsLists a request's tags. |
| POST | /requests/:request_id/tags | addRequestTagsAdds tags to a request without removing the ones it already has. |
| DELETE | /requests/:request_id/tags/:tag_id | deleteRequestTagAssignmentRemoves a tag from a request. |
| PUT | /requests/:request_id/tags | setRequestTagsReplaces all of a request's tags (Jotly). |
| GET | /requests/:request_id/comments | listRequestCommentsLists a request's comments in one-level threads (replies), pinned first. Includes internal notes. |
| POST | /requests/:request_id/comments | createRequestCommentComments 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_id | getCommentGets a comment. |
| PATCH | /requests/:request_id/comments/:comment_id | updateCommentEdits a comment's text. |
| DELETE | /requests/:request_id/comments/:comment_id | deleteCommentDeletes a comment and its replies. |
| GET | /requests/:request_id/comments/:comment_id/votes | listCommentVotesLists who voted on a comment. |
| POST | /requests/:request_id/comments/:comment_id/votes | addCommentVoteVotes on a comment on behalf of a member. |
| DELETE | /requests/:request_id/comments/:comment_id/votes | deleteCommentVoteRemoves a member's vote on a comment (voter_id or voter_external_id). |
| PUT | /requests/:request_id/comments/:comment_id/pin | pinCommentPins a comment to the top of the thread. |
| DELETE | /requests/:request_id/comments/:comment_id/pin | unpinCommentUnpins a comment. |
| GET | /requests/:request_id/attachments | listRequestAttachmentsLists a request's attached images (signed URLs that expire in one hour). |
| POST | /requests/:request_id/attachments | addRequestAttachmentsAttaches images to a request from URLs (up to 4 per request). |
| DELETE | /requests/:request_id/attachments/:attachment_id | deleteRequestAttachmentRemoves an attachment from a request. |
| GET | /requests/:request_id/comments/:comment_id/attachments | listCommentAttachmentsLists a comment's attached images. |
| POST | /requests/:request_id/comments/:comment_id/attachments | addCommentAttachmentsAttaches images to a comment from URLs (up to 4 per comment). |
| DELETE | /requests/:request_id/comments/:comment_id/attachments/:attachment_id | deleteCommentAttachmentRemoves an attachment from a comment. |
Files
| Method | Path | What it does |
|---|---|---|
| POST | /assets | createAssetUploads an image (multipart: file, name). Without file storage configured, it responds invalid_asset. |
| POST | /assets/import | importAssetImports 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_id | getAssetGets a workspace asset. |
Changelog
| Method | Path | What it does |
|---|---|---|
| GET | /changelogs/tags | listChangelogTagsLists changelog tags (New feature, Improvement, Fix...). |
| POST | /changelogs/tags | createChangelogTagCreates a changelog tag. 409 tag_name_conflict if it already exists. |
| DELETE | /changelogs/tags/:tag_id | deleteChangelogTagDeletes a changelog tag and removes it from every entry. |
| GET | /changelogs | listChangelogsLists changelog entries. status: draft, scheduled or published. |
| POST | /changelogs | createChangelogCreates 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_id | getChangelogGets a changelog entry with its tags, authors and linked requests. |
| PATCH | /changelogs/:changelog_id | updateChangelogEdits an entry; status publishes it, schedules it (with publish_at) or moves it back to draft. |
| DELETE | /changelogs/:changelog_id | deleteChangelogDeletes a changelog entry. |
| GET | /changelogs/:changelog_id/tags | listChangelogTagAssignmentsLists a changelog entry's tags. |
| POST | /changelogs/:changelog_id/tags | addChangelogTagsAdds tags to an entry without removing the ones it already has. |
| DELETE | /changelogs/:changelog_id/tags/:tag_id | deleteChangelogTagAssignmentRemoves a tag from an entry. |
| GET | /changelogs/:changelog_id/authors | listChangelogAuthorsLists an entry's authors. |
| POST | /changelogs/:changelog_id/authors | addChangelogAuthorsAdds authors to an entry. They must be team members (invalid_changelog_author otherwise). |
| DELETE | /changelogs/:changelog_id/authors/:member_id | deleteChangelogAuthorRemoves an author from an entry. |
| GET | /changelogs/:changelog_id/requests | listChangelogRequestsLists the requests an entry closes. |
| POST | /changelogs/:changelog_id/requests | addChangelogRequestsLinks requests (by id or number) to an entry; their voters see that it shipped. |
| DELETE | /changelogs/:changelog_id/requests/:request_id | deleteChangelogRequestUnlinks a request from an entry. |
Members
| Method | Path | What it does |
|---|---|---|
| GET | /members | listMembersLists members (people who vote, post or chat). type: user, admin or owner. company_external_id filters by company. |
| GET | /members/lookup | lookupMemberFinds a member by external_id or email. |
| POST | /members/sync | syncMemberCreates 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_id | getMemberGets a member. 410 member_erased if their data was erased. |
| PATCH | /members/:member_id | updateMemberEdits a member: external_id, email, name, avatar or traits (merged; a trait set to null is removed). |
| DELETE | /members/:member_id | deleteMemberErases 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
| Method | Path | What it does |
|---|---|---|
| POST | /companies/sync | syncCompanyCreates or updates a company by external_id. name is required when creating it; traits are merged. |
| GET | /companies | listCompaniesLists companies. Filter by search (name or domain), external_id or domain. |
| GET | /companies/lookup | lookupCompanyFinds a company by external_id. |
| GET | /companies/:company_id | getCompanyGets a company. |
| PATCH | /companies/:company_id | updateCompanyEdits a company. 409 company_identity_conflict if the new external_id already belongs to another one. |
| GET | /companies/:company_id/members | listCompanyMembersLists a company's members. |
| POST | /companies/:company_id/members | addCompanyMemberLinks a member ({ id } or { external_id }) to a company. |
| DELETE | /companies/:company_id/members/:member_id | deleteCompanyMemberUnlinks a member from a company. |
Conversations
| Method | Path | What it does |
|---|---|---|
| GET | /conversations | listConversationsLists 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_id | getConversationGets a conversation. Its messages are in listConversationTimeline. |
| GET | /conversations/:conversation_id/timeline | listConversationTimelineA conversation's timeline, oldest to newest. types: message, note, ai_handoff, resolved, reopened, assigned, unassigned. |
| POST | /conversations/:conversation_id/replies | createConversationReplyReplies 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/notes | createConversationNoteAdds an internal note (the customer doesn't see it). Accepts Idempotency-Key. |
| PATCH | /conversations/:conversation_id/status | updateConversationStatusResolves or reopens a conversation. 409 conversation_status_conflict if it's archived or marked as spam. |
| PATCH | /conversations/:conversation_id/assignee | updateConversationAssigneeAssigns the conversation to a team member (assignee { id } or { external_id }, or assignee_email) or unassigns it with null. |
See also MCP server