REST API reference
The BesTest REST API
Read and write the same requirements, test cases, cycles, executions and traceability links your team works with in Jira, from a script, a pipeline or your own tooling. This page is the reference. If you are setting up for the first time, start with the REST API guide.
Authentication
Every request carries a bearer token. You create tokens yourself in BesTest, under API & MCP tokens in the app menu. A token reaches exactly one Space, is either read only or read and write, and always expires, at most a year out. It is a long string starting with eyJ, shown once when you create it.
the Accept header may be left out, but never set to application/json
-H "Authorization: Bearer $BESTEST_TOKEN"
-H "Accept: application/vnd.api+json"This API is strict JSON:API. You can leave Accept out, but a request that asks for application/json is answered with 406 Not Acceptable. Any request with a body must send Content-Type: application/vnd.api+json.
Bring your own OpenAPI file. GET /api/v1/openapi.json with your token returns the machine-readable spec, trimmed to exactly what that token can do. Import it into Postman, Insomnia or a client generator. Without a token it answers 403.
Base URLs
BesTest runs independent servers in three regions and your data lives in exactly one of them. A token only works against its own region: anywhere else it gets 403.
| Region | Base URL |
|---|---|
| Europe | https://prod-eu.getbestest.com/api/v1 |
| North America | https://prod-us.getbestest.com/api/v1 |
| India | https://prod-in.getbestest.com/api/v1 |
Not sure which is yours? Call GET /api/v1/projects against each: only your region answers 200, and it returns the one Space your token reaches. Examples throughout this reference use the Europe host.
Filtering, sorting and paging
Every collection takes the same query parameters. Filtering uses FiltrQL, a readable expression rather than a nest of bracketed parameters. Strings go in double quotes, and in practice you url-encode the whole thing (curl's --data-urlencode does it for you).
query parameter, before url-encoding
# Find a record by the key you see in the app
filter=testCaseKey = "KAN-TC-42"
# Combine conditions; relative dates work
filter=result = "FAILED" AND executedAt > now - 7d
# Through a relation, with dot notation
filter=testCycle.testCycleKey = "KAN-CY-38"
# Relation quantifiers: ANY, ALL, NONE
filter=testCaseExecutions ANY (result = "FAILED")
# Functions: yourself, and calendar boundaries
filter=ownerId = currentUser() AND updatedAt >= startOfWeek()
# Case-insensitive contains, and lists
filter=name ~ "checkout" AND priority in ("HIGH", "CRITICAL")sorttakes a comma-separated field list; prefix a field with a minus for descending, as insort=-updatedAt,name.page[size]is 25 by default and at most 100. Page withpage[number], or switch to cursor paging by sending an emptypage[after]=and then followinglinks.nextverbatim. Cursors are stable while data changes underneath you; never build or decode one yourself.includeembeds related to-one resources (a test case's folder or owner) in the same response, up to three levels deep. To-many relations have their own URLs, such as/test_cases/{id}/testSteps.fields[type]returns only the attributes you name, for examplefields[test_cases]=testCaseKey,name,status. On the wide entities that cuts the payload several-fold.- Every collection has an
/aggregatetwin for counts, sums, minimums and maximums, optionally grouped:aggregations[count]=trueplusaggregations[groupBy][]=resultreturns one count per result.
Each list endpoint below shows its own filterable fields and the operators each one accepts. Functions available in filters: currentUser(), startOfDay(), endOfDay(), startOfWeek(), endOfWeek(), startOfMonth(), endOfMonth(), startOfYear(), endOfYear(), membersOf(role).
Writing data
- Create with a POST whose body is a JSON:API resource:
{"data": {"type": "test_cases", "attributes": {…}}}. Most creates need your Space'sprojectId, which is the idGET /projectsreturns, plus arank(its position in lists; 1 is fine). - Update with a PATCH carrying only the attributes that change. Updates and deletes are version-locked: read the resource, take the
ETagresponse header, and send it back asIf-Match. Leave it out and you get 428; if someone changed the record since you read it, 412. Read again and retry. - Delete is soft for requirements, test cases, steps, collections and campaigns. The row stays reachable with
withDeleted=true, andPOST /{id}/actions/restorebrings it back. Test cycles cannot be deleted through the API. - Custom fieldsare read and written through each record's
customFieldsattribute as plain key-value pairs. An unknown key is rejected with 422. - The server stamps what it owns, such as keys, the test case version on an execution, and run times. Sending one returns 422 naming the attribute. A request body can be at most 1 MB.
Permissions and limits
- A token never sees more than its owner does, and only inside the one Space it was created for.
- A read-only token is refused on every write with 403.
- Requests are rate-limited per person, shared between the REST API and MCP, and set well above normal pipeline and agent use. Over a limit you get 429 with code
RATE_LIMITEDand noRetry-Afterheader, so back off for a few seconds and retry. - Writes are attributed to the token owner, so history still shows a person.
Errors
Every error is a JSON:API errors array. code is the stable, machine-readable part; detail is written for a person and usually names the fix; source points at the offending parameter or attribute.
every 4xx and 5xx has this shape
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"code": "UNKNOWN_FIELD",
"detail": "Unknown field \"displayName\" on resource type \"users\". Valid fields: id, externalId, role, ...",
"source": {
"parameter": "fields[users]"
}
}
]
}| Status | Usually means |
|---|---|
| 400 | Something in the query string is wrong: an unknown field or relation, a filter that does not parse, or page[number] and page[after] together. |
| 401 | The token has been revoked. |
| 403 | No token, an expired or malformed one, a token from another region, or a read-only token attempting a write. |
| 404 | No such record, or it is outside the Space your token reaches. |
| 406 | Accept asked for application/json. Send application/vnd.api+json or leave it out. |
| 409 | The write conflicts with existing data: a duplicate, or a reference to a record that does not exist. |
| 412 | Your If-Match ETag is stale. Read the record again and retry. |
| 415 | Content-Type was not application/vnd.api+json. |
| 422 | A value is invalid, a required attribute is missing, or you sent an attribute the server sets itself. |
| 428 | The record is version-locked and the request had no If-Match header. |
| 429 | Rate limited. Back off and retry. |
Resources
Pick a group to see its endpoints, parameters and examples.
Requirements
12What the product is supposed to do, and how well it is covered.
Test cases
19Test cases, their steps, and the folders that organise them.
Test cycles
10Planned runs of a set of test cases, and their folders.
Executions
10Results: what was run, when, by whom, and how it went.
Test collections
16Reusable groupings of test cases and requirements.
Test campaigns
16Multi-cycle programmes of work, grouped for reporting.
Links and traceability
10The relationships between requirements, test cases, runs and Jira work items.
Custom fields
3The custom fields defined in your Space, and their types.
Spaces and people
18Your Space, its users, environments and versions.
Comments
5Discussion threads on testing objects.
Saved filters
3Stored filter definitions shared across the Space.
