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.

Open beta122 endpoints across 11 resource groups, plus 8 worked workflows, as of 2026-10-05
New here? Start with a worked workflowRecord a test run, create a test case with its steps, page with a cursor, count results by status: complete multi-step flows, each as a script you can copy and run.

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.

Every request

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.

How to create, rotate and revoke a token

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.

RegionBase URL
Europehttps://prod-eu.getbestest.com/api/v1
North Americahttps://prod-us.getbestest.com/api/v1
Indiahttps://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).

Filters

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")
  • sort takes a comma-separated field list; prefix a field with a minus for descending, as in sort=-updatedAt,name.
  • page[size] is 25 by default and at most 100. Page with page[number], or switch to cursor paging by sending an empty page[after]= and then following links.next verbatim. 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 example fields[test_cases]=testCaseKey,name,status. On the wide entities that cuts the payload several-fold.
  • Every collection has an /aggregate twin for counts, sums, minimums and maximums, optionally grouped: aggregations[count]=true plus aggregations[groupBy][]=result returns 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's projectId, which is the id GET /projects returns, plus a rank (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 ETag response header, and send it back as If-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, and POST /{id}/actions/restore brings it back. Test cycles cannot be deleted through the API.
  • Custom fieldsare read and written through each record's customFields attribute 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.
See it end to end in the worked workflows

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_LIMITED and no Retry-After header, 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.

Error

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]"
      }
    }
  ]
}
StatusUsually means
400Something in the query string is wrong: an unknown field or relation, a filter that does not parse, or page[number] and page[after] together.
401The token has been revoked.
403No token, an expired or malformed one, a token from another region, or a read-only token attempting a write.
404No such record, or it is outside the Space your token reaches.
406Accept asked for application/json. Send application/vnd.api+json or leave it out.
409The write conflicts with existing data: a duplicate, or a reference to a record that does not exist.
412Your If-Match ETag is stale. Read the record again and retry.
415Content-Type was not application/vnd.api+json.
422A value is invalid, a required attribute is missing, or you sent an attribute the server sets itself.
428The record is version-locked and the request had no If-Match header.
429Rate limited. Back off and retry.

Resources

Pick a group to see its endpoints, parameters and examples.