The Quevell API is the same set of methods the product's own interface uses, opened up for integrations. It speaks JSON both ways and acts only as a service account: people do not get personal tokens. This page covers how to call it and what it returns, and ends with the methods you may call.
Address
https://<key>.quevell.com/api
<key> is your company's key, the one in the product's address bar. Call your own company's address. A token belongs to one company, and at another company's address the request is rejected with auth.tenant_mismatch (403). There is no version number in the path.
Signing in
Every request carries a service account token:
Authorization: Bearer qvl_...
Service accounts explains how to mint a token, how to narrow its rights, and how to get a short-lived token through an OAuth client. A request with no token, or with one that is unknown, expired or revoked, gets 401 auth.required. A token that lacks the right for an action gets 403 authorization.forbidden.
Responses and errors
A successful response is the object, list or page itself, with no wrapper around it. An error always arrives in the same envelope:
{
"code": "concurrency.version_conflict",
"message": "Issue was modified concurrently",
"status": 409,
"path": "/api/issues/6f1c.../transitions/...",
"correlationId": "9b2e...",
"timestamp": "2026-09-17T08:00:00Z",
"details": {}
}
codeis stable. Branch on it.messageis a sentence for a person, in English. It may be reworded any day, so never branch onmessage.detailsholds specifics when there are any: field errors indetails.fieldErrors, how long to wait indetails.retryAfterSeconds.correlationIdidentifies the request. Every response also carries it in theX-Correlation-Idheader; quote it when you contact support.
The most common codes:
| Code | Status | Meaning |
|---|---|---|
request.malformed | 400 | the body or a parameter does not parse: bad JSON, UUID or number |
request.validation | 400 | a field did not pass validation; see details.fieldErrors |
request.unknown_parameter | 400 | a query parameter the method does not take; the message lists the ones it does |
query.syntax | 400 | the QQL query has an error |
auth.required | 401 | no token, or a token that no longer works |
authorization.forbidden | 403 | not enough rights |
auth.tenant_mismatch | 403 | the token belongs to another company |
*.not_found | 404 | the object does not exist or is not visible to you |
concurrency.version_conflict | 409 | a stale version; see Versions and conflicts |
files.storage_quota_exceeded | 409 | the company's attachments have used all the space allowed |
tenant.rate_limited | 429 | calling too often; see Limits |
tenant.record_quota_exceeded | 429 | the daily allowance of records is used up |
internal.error | 500 | something failed on our side; send us the correlationId |
Modules add codes in their own namespaces, such as note.*, sprint.* and identity.*. Handle an unfamiliar code by its status.
Dates and times
Moments are sent and accepted in UTC, as ISO-8601 strings ending in Z. Calendar days, such as an issue's due date or the day of a worklog, travel as year-month-day with no time. Neither the server's time zone nor the person's affects a response; converting to local time is up to your program.
Pages and cursors
Long lists come in pages. A page has items and nextCursor, and some methods add total. To get the next page, repeat the request with cursor=<nextCursor>; an empty nextCursor means you have them all. limit sets the page size, and each method's maximum is in the reference. There are no page numbers or offsets.
A cursor is tied to the sort order it was issued under. Used with a different order, it is rejected.
Search and QQL
GET /api/issues/search searches every project visible to you. text takes either plain words or a QQL query:
curl -G https://<key>.quevell.com/api/issues/search \
-H "Authorization: Bearer qvl_..." \
--data-urlencode 'text=project = OFFICE and status != Done order by updated desc' \
--data-urlencode 'mode=QUERY'
Without mode, the server guesses whether text is words or a query and reports its guess in the response's mode. An integration should set mode=QUERY outright. Order comes from ORDER BY inside the query, most recently updated first by default. filterId runs a saved filter.
Issues the token may not see are left out entirely: they appear in neither items nor total.
Versions and conflicts
Objects that change carry a version. When you change one, send the version you read as expectedVersion. If somebody changed the object first, you get 409 concurrency.version_conflict, or a module's code ending in version_conflict. Read the object again and decide whether your change still applies.
Sending the same change to an issue twice does nothing the second time and writes no new event. Creating is different: a repeated POST creates a second object. If a create request was cut off, check whether the object exists before you send it again.
Limits
| What is counted | Limit | Response past it |
|---|---|---|
| calls by one service account | 120 a minute | 429 tenant.rate_limited |
| calls by all of a company's service accounts | 600 a minute | 429 tenant.rate_limited |
| records by one service account | 10 000 a day | 429 tenant.record_quota_exceeded |
| records by all of a company's service accounts | 50 000 a day | 429 tenant.record_quota_exceeded |
| tokens handed out to an OAuth client | 10 a minute per client, 30 per network address | 429 oauth.rate_limited |
| the company's attachment storage | set by the company's plan | 409 files.storage_quota_exceeded |
How the counting works:
- A minute is a calendar minute in UTC, not a sliding window. Every request with a working token counts, including one rejected for lack of rights.
- A day is a calendar day in UTC. A record is every event the account leaves in the audit trail: creating or changing an issue, a comment, a worklog, and also downloading an attachment or exporting CSV. Reading that leaves no event uses no records.
- All of an account's tokens, minted by hand or handed out by an OAuth client, share one allowance.
Every 429 says how long to wait, in the Retry-After header and as the same number in details.retryAfterSeconds. For the per-minute limits that is the seconds left in the minute; for the daily ones, the time until midnight UTC.
HTTP/1.1 429 Too Many Requests
Retry-After: 17
{"code":"tenant.rate_limited","status":429,"details":{"retryAfterSeconds":17},...}
Wait that long, then retry. Retrying sooner is pointless, because those requests count too. A 409 files.storage_quota_exceeded carries usedBytes and limitBytes in details.
Examples
Create an issue:
curl -X POST https://<key>.quevell.com/api/projects/<projectId>/issues \
-H "Authorization: Bearer qvl_..." \
-H "Content-Type: application/json" \
-d '{"summary": "Replace the toner", "description": "The printer on the third floor"}'
Move an issue to another status: first ask which transitions are available, then perform the one you want, passing the issue's version.
curl https://<key>.quevell.com/api/issues/<issueId>/transitions \
-H "Authorization: Bearer qvl_..."
curl -X POST https://<key>.quevell.com/api/issues/<issueId>/transitions/<transitionId> \
-H "Authorization: Bearer qvl_..." \
-H "Content-Type: application/json" \
-d '{"expectedVersion": 3}'
Add a comment:
curl -X POST https://<key>.quevell.com/api/issues/<issueId>/comments \
-H "Authorization: Bearer qvl_..." \
-H "Content-Type: application/json" \
-d '{"body": "Toner replaced"}'
Method reference
This list is read from the product's code and checked against it on every build, so it never names a method that is gone. Sign-in, company administration and the helpers behind individual screens are left out, because their shape changes with the interface. A right named in a description is required of both the account's role and the token; seeing the project is required everywhere and is not repeated.
| Method | Path | What it does |
|---|---|---|
GET | / | Looks up issues by the keys in keys (up to 50); keys that match nothing or that you can't see are silently left out |
PUT | / | Replaces the issue's fields, including summary, description and issueTypeId. Needs issue.edit; returns 409 if expectedVersion is stale |
GET | / | Returns the issue's history, oldest first: who changed what and when, including changes made by automation rules |
GET | / | Returns the issue's checklist in order, with each item's done state, who checked it and when, plus the done count |
POST | / | Adds an item to the end of the checklist from text (up to 500 characters). Needs issue.edit; returns the whole checklist |
PUT | / | Reorders checklist items by itemIds; items you leave out keep their place after the ones you list. Needs issue.edit |
DELETE | / | Removes an item from the issue's checklist and returns what remains. Needs issue.edit |
PUT | / | Changes an item's text and/or sets done, recording who checked it and when. Needs issue.edit |
GET | / | Returns the issues one level below this one; children hidden from you by issue security are silently left out |
GET | / | Lists the teams contributing to the issue, with mine set for teams you belong to |
PUT | / | Sets the issue's contributing teams from teamIds; you can only add or remove teams you are a member of |
GET | / | Returns the issue's labels in alphabetical order |
PUT | / | Replaces the issue's labels with the words in names (up to 30, 60 characters each); a new word becomes a label. Needs issue.edit |
GET | / | Lists the issue's links in both directions; links to issues you can't see are silently left out |
POST | / | Links the issue to another one given typeId, issueKey and direction. Needs issue.edit on both issues |
DELETE | / | Removes an issue link and returns the remaining links. Needs issue.edit |
GET | / | Counts the issues below this one by status category, in total and per child; only issues you can see are counted |
PUT | / | Sets or clears (null) the issue's securityLevelId. Needs issue.security.manage; returns 409 if expectedVersion is stale |
GET | / | Lists the transitions available from the issue's current status in its workflow |
POST | / | Deprecated: moves the issue to a status in the done category. Needs issue.transition; send expectedVersion in the body |
POST | / | Performs a transition available from the issue's current status. Needs issue.transition; send expectedVersion in the body |
GET | / | Lists the issue's watchers with display names and when each started watching |
DELETE | / | Stops watching the issue as the caller |
GET | / | Tells whether the caller is watching the issue |
POST | / | Starts watching the issue as the caller |
GET | / | Returns an issue by UUID or by a key like PROJ-12; a former key of the issue still finds it |
GET | / | Lists the project's issues newest first, limit 1 to 200 (default 100); issues hidden by issue security are silently left out |
POST | / | Creates an issue in the project; summary and description are required, and omitted fields take the project's defaults. Needs issue.create |
| Method | Path | What it does |
|---|---|---|
GET | / | Lists the fields a QQL query can use, with their type, allowed operators, sortability, and whether each is custom |
GET | / | The same value suggestions as values, each with a note: a sprint name that several projects share comes with the project key beside it, and only the name goes into the query |
GET | / | Suggests values for a field while writing a QQL query, drawn only from projects the caller can view |
GET | / | Searches issues across every project you can view, by text or a QQL query with mode=QUERY; limit defaults to 40, max 1000; returns items, nextCursor, total |
GET | / | Exports matching issues as CSV using the same criteria as search; limit defaults to 1000 and caps at 10000 |
GET | / | Searches issues within one project with filters and a cursor; limit defaults to 40, max 100; issues you may not see are silently left out |
| Method | Path | What it does |
|---|---|---|
GET | / | Lists the projects you can open, with their last activity time and their issue counts: issueCount in all and openIssueCount not in a Done status, counting only the issues you can see; pass archived=true to list archived ones instead |
POST | / | Creates a project with a key, a name, and the default configuration, making you its project administrator; needs projects.create |
DELETE | / | Permanently deletes a project that is already archived; company administrators only, with expectedVersion in the query string |
PUT | / | Renames a project or changes its key, with expectedVersion and 409 on a stale version; needs project.access.manage, and a key change needs a company administrator |
POST | / | Archives a project and sets the date it will be purged; needs project.access.manage, expectedVersion in the body, 409 on a stale version |
GET | / | Returns the project configuration: fields, issue types, the default issue type, and per-type field defaults |
PUT | / | Sets the issue security level new issues in the project start with, or null to clear it; needs issue.security.manage |
GET | / | Lists the project's issue security levels with their grants and how many issues each guards; needs issue.security.manage |
POST | / | Creates an issue security level with a name and grants to users, project roles, or groups; needs issue.security.manage |
DELETE | / | Deletes an issue security level, refused while any issue still sits behind it; needs issue.security.manage |
PUT | / | Replaces an issue security level's name and grants as a whole; needs issue.security.manage, takes expectedVersion, 409 on a stale version |
GET | / | Lists project participants with names and roles; limit defaults to 50, max 200; returns items, nextCursor, total |
PUT | / | Assigns, changes, or removes roles for up to 500 participants in one call; needs project.access.manage |
GET | / | Lists the groups that hold a role in the project, with their names and roles |
DELETE | / | Removes a group's role in the project; its people keep any roles held in their own name; needs project.access.manage |
PUT | / | Gives a group a role in the project: viewer, contributor, or admin; needs project.access.manage |
DELETE | / | Removes a participant from the project; needs project.access.manage; you cannot remove yourself or the last project administrator |
PUT | / | Gives a user a role in the project: viewer, contributor, or admin; needs project.access.manage, and you cannot change your own role |
POST | / | Restores an archived project; needs project.access.manage, expectedVersion in the body, 409 on a stale version |
GET | / | Returns a project; {projectReference} accepts the project key, a former key, or the id |
GET | / | Counts the project's issues per status, only among issues you can see; {projectReference} accepts the project key or id |
| Method | Path | What it does |
|---|---|---|
GET | / | Lists the boards you can see plus each project's default board; pass archived=true for the archive |
POST | / | Creates a KANBAN or SCRUM board scoped by either projectIds or a query, never both |
PUT | / | Sets a project's default board via boardId (null clears it); the board must include the project, needs board.manage (or be the board's creator) |
DELETE | / | Permanently deletes an archived board with its columns, filters and sprints; issues stay, needs board.manage (or be the board's creator) |
GET | / | Returns the board with columns, cards and swimlanes; filters applies quick filters by id |
POST | / | Archives or restores a board based on archived; a project's default board can't be archived, needs board.manage (or be the board's creator) |
GET | / | Returns the ranked backlog and the open sprints with their issues and estimate totals |
PUT | / | Replaces the board's columns (columns, enforceWip); send expectedVersion, 409 if stale, needs board.manage (or be the board's creator) |
PUT | / | Puts a card (issueId) into a status-less column (columnId) without changing its status; needs issue.rank |
DELETE | / | Sends a card back to the column its status maps to; needs issue.rank |
PUT | / | Replaces the board's quick filters (filters of name and query, up to 20); send expectedVersion, 409 if stale, needs board.manage (or be the board's creator) |
POST | / | Ranks issueId before beforeIssueId on the board; send expectedVersion, 409 if stale, needs issue.rank |
PUT | / | Sets the swimlane mode, with lanes for QUERIES mode; send expectedVersion, 409 if stale, needs board.manage (or be the board's creator) |
| Method | Path | What it does |
|---|---|---|
GET | / | Sprint report for a SCRUM board: closed sprints with results and velocity averaged over the last three |
POST | / | Creates a planned sprint on a SCRUM board with a name and optional goal; needs board.manage (or be the board's creator) |
GET | / | Returns the issue's current sprint, active first, then planned; null when it's in no open sprint |
GET | / | Lists every sprint the issue has been in, closed ones included, with board names |
PUT | / | Replaces the issue's open sprints with sprintIds; closed sprints stay put, needs issue.rank |
GET | / | Lists started sprints, active and closed, on the project's boards, the ones a burndown can show |
GET | / | Sprint burndown: remaining issue count per day alongside the ideal line |
POST | / | Completes an active sprint, optionally moving open issues to sprint moveOpenTo; send expectedVersion, 409 if stale, needs board.manage (or be the board's creator) |
GET | / | Returns the sprint's issues you can see, with summed estimates and time spent |
POST | / | Adds issueId to an open sprint; an issue can sit in only one open sprint per board, needs issue.rank |
DELETE | / | Removes an issue from an open sprint, returning it to the backlog; needs issue.rank |
POST | / | Starts a planned sprint with endsAt and optional startsAt; send expectedVersion, 409 if stale, needs board.manage (or be the board's creator) |
| Method | Path | What it does |
|---|---|---|
GET | / | Returns the issue's comments, oldest first |
POST | / | Adds a comment from body (up to 10,000 characters); mentions in the text are recorded. Needs comment.create |
| Method | Path | What it does |
|---|---|---|
GET | / | Lists the issue's attachments with file name, type, size, and who attached each one and when |
POST | / | Attaches a file to the issue (multipart field file, up to 10 MiB) within the storage the company's plan allows. Needs attachment.create |
DELETE | / | Deletes an attachment from storage; the history keeps its name. attachment.delete covers your own files; others' need a project administrator (project.access.manage) |
GET | / | Downloads the attachment's file; each download is recorded as an event in the history, and repeat downloads are fine |
GET | / | Returns the small copy of an image attachment (PNG, 256 on its long side). Only files whose bytes turned out to be an image have one; anything else answers 404. Same rights as the download |
GET | / | Returns the original of an image to be looked at: Content-Disposition: inline and the type the bytes turned out to be rather than the declared one. Anything that is not an image answers 404. Same rights as the download |
| Method | Path | What it does |
|---|---|---|
PUT | / | Sets originalEstimate and remainingEstimate as duration strings like "1w 2d 4h 30m"; send expectedVersion, 409 if stale, needs issue.edit |
GET | / | Time tracking for an issue: estimate, remaining estimate, time spent and all worklogs |
POST | / | Logs work: spent as a duration like "2h 30m", optional spentOn and note; lowers the remaining estimate, needs work.log |
DELETE | / | Deletes a worklog without restoring the remaining estimate; needs work.log, plus project.access.manage for someone else's |
PUT | / | Edits a worklog's spent duration string and note; needs work.log, plus project.access.manage for someone else's |
GET | / | Time report: minutes per person for calendar days from through to, up to a year; without projectId needs company right reports.view |
| Method | Path | What it does |
|---|---|---|
GET | / | Knowledge base pages linked to an issue, limited to the pages the caller can open |
GET | / | Page tree of a project's knowledge base with projectId (needs project.view), or of your personal space plus pages others shared with you; archived pages come flagged archived |
POST | / | Creates a page in a project's knowledge base (projectId, needs note.edit) or your personal space (personal); also title, body, parentId, draft, linkedIssueKeys |
GET | / | Searches page titles and bodies for q in a project (projectId) or your personal space; archived pages are left out |
DELETE | / | Deletes a page with its revisions, page access and issue links; a page that still has child pages is refused |
GET | / | Returns a page with its body, version and canEdit flag; answers 403 when you have no page access |
PUT | / | Saves a page's title and body with expectedVersion, 409 on a stale version; saving the same title and body writes no revision |
GET | / | Lists page access: the people and groups named on the page, each with READ or EDIT |
PUT | / | Grants a person or group page access, or changes its level; body has subjectKind, subjectId, level; you must be able to edit the page |
DELETE | / | Revokes a person's or group's page access and returns the remaining access list |
POST | / | Archives or restores a page together with all its child pages; body is archived |
POST | / | Copies a page into the same place with the same body and "(копия)" appended to the title |
GET | / | Lists a page's linked issues with each issue's key, summary and status |
POST | / | Links a page to an issue by its key; the caller must be able to see the issue |
DELETE | / | Removes a page's link to an issue and returns the remaining linked issues |
POST | / | Moves a page under another parent (parentId) or into another space, children included; takes expectedVersion, 409 on a stale version |
POST | / | Hands a personal page and its children to a new page owner (ownerId); only the current owner or an administrator may |
GET | / | Lists a page's revisions newest first, with version, title, editor and edit time |
GET | / | Returns one page revision by version, including its title and body |
| Method | Path | What it does |
|---|---|---|
GET | / | Reference data: the field types available in the company |
GET | / | Reference data: the company's fields, archived ones excluded, with required flags and default values |
GET | / | Reference data: the company's hierarchy levels, top level first |
GET | / | Reference data: the company's issue types, archived ones excluded, ordered by hierarchy level |
GET | / | Reference data: active link types between issues, with their outward and inward wording |
GET | / | Reference data: the statuses defined in the company |
GET | / | Reference data: the company's workflows |
GET | / | The company's labels in alphabetical order; pass query to match part of a name |
| Method | Path | What it does |
|---|---|---|
GET | / | Names of the company's groups in alphabetical order, for group pickers |
GET | / | Lists the people in the company with name, status and account type |
GET | / | A person's page: contact details, time away, deputy and filled-in profile fields |
PUT | / | Updates a person's profile field values from an object of field id to value, an empty value clears it; needs people.edit |
GET | / | Lists every team in the company with member count and the caller's own role |
POST | / | Creates a team from name and description; the caller becomes its team creator |
GET | / | The teams a person belongs to, with their role in each |
DELETE | / | Deletes a team and removes it wherever it is used; only the team creator or an administrator may |
GET | / | Returns a team with its members and their roles |
PUT | / | Renames a team or changes its description with expectedVersion, 409 on a stale version; only the team creator or an administrator may |
POST | / | Adds a person to a team (userId, role); the team creator, a manager or an administrator may |
DELETE | / | Removes a person from a team; anyone but the team creator may leave, removing others takes the creator, a manager or an administrator |
PUT | / | Changes a team member's role; the team creator's role cannot be changed |
| Method | Path | What it does |
|---|---|---|
GET | / | Your personal filters plus everyone's shared filters, with query text and mine and shared flags |
POST | / | Saves a filter from name, query and shared to make it a shared filter; names must be unique among your filters |
DELETE | / | Deletes a filter; only the person who saved it may |
PUT | / | Updates a filter's name, query and shared with expectedVersion, 409 on a stale version; only its author may |