Skip to main content

Quickstart for Testiny REST API

To get started and play around with the API, we recommend using an API client like PostMan or Bruno or any other API client. You can also start making requests directly from a shell or script.

Authentication

To authenticate, you need to provide an API key in your requests. If you don't have a Testiny API key yet, see our documentation on how to create an API key.

info

Treat the API key like a password and keep it safe.

Add the following header to your requests:

X-Api-Key: <API_KEY_VALUE>

Using a shell or script

If you want to make API requests in PowerShell, use curl or create a script in Python, Node, Go, Ruby or another language, our API documentation provides boilerplate code snippets for each API call. You can find the code snippets for different tools and languages next to each API call:

Api doc boilerplate code snippets

Reading data

Fetch a single entity by ID

GET https://app.testiny.io/api/v1/<entity>/:id

For example, to fetch a test case with ID 1234, replace ":id" with "1234": GET https://app.testiny.io/api/v1/testcase/1234.

Fetch multiple entities

If you want to fetch multiple entities, you can do a "find" request by a GET or POST request:

GET https://app.testiny.io/api/v1/<entity>
POST https://app.testiny.io/api/v1/<entity>/find

The difference between the GET and POST request is the way you pass parameters. For the GET request you can pass query parameters and for the POST request you can pass the parameters via the body.

For example, to receive all projects in your Testiny account, you could use either route:
GET https://app.testiny.io/api/v1/project
POST https://app.testiny.io/api/v1/project/find

Most of the time, you probably don't want to receive all instances of an entity but want to filter or order them, or omit some values. For this, you can pass DataReadParams.

Enforced Pagination

You'll always just receive the maximum number of entities even if no pagination is specified. The limit is in the response in "meta".

Best Practices

Note that 'filter' option in DataReadParams should include a 'project_id' parameter if API key permissions are restricted to a single project. Read more about filtering.

DataReadParams

To filter, order or paginate fetched data you can pass parameters to the "find" requests. For POST requests, pass the DataReadParams in the body. For GET requests, pass the DataReadParams to the query parameter q.

The DataReadParams are also described in the POST "find" routes. For example, for testcases, you can find the route here: Find 'TestCase' entities.

You can define parameters for pagination, ordering, filtering, mappings, fetching historical data and other options for omitting values or for fetching just the ids or the count.

Here's an example how to pass a DataReadParams object to a POST request. This example has the option "filter" and "idOnly" specified and fetches all test case IDs from project with ID "3":

POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
filter: { "project_id": 3 } },
idOnly: true
}

Here are all available options for DataReadParams:

ParameterDescription
paginationPagination settings control the amount of data fetched from a result
orderControls the ordering of the result
includeTotalCountIncludes the total result count if pagination options were given
filterFilter/search terms
computedColumnsList of computed columns/properties to include (by default includes all)
idOnly return the entity with the given id, returns one or none
idsOnly return the entities with the given ids
mapMap expression to expand properties or relationships with other entities
includeDeletedIncludes soft-deleted entities in the result (if applicable)
omitLargeValuesOmits 'large' column/prop values (if available), that may contain long values, for example, text fields for descriptions or test case steps. Default value: true
omitForbiddenOmits entities which are not allowed to be read rather than return an error. Default value: false
asOfPerform a history query given the asOf timestamp (if available)
idOnlyOnly returns ids in the result, omitting all other properties
countOnlyReturns only the length of the result list instead of the result list itself

Pagination

Pagination settings control the amount of data fetched from a result. Note that you will always receive the maximum number of entities, even if no pagination is specified. The limit is indicated in the response object's "meta" property.

You can set the following properties in the "pagination" object:

  • offset
    The offset the result starts at. Default value: 0
  • pageAtId
    Given an entity id and a limit, offset is ignored and the page is automatically selected. The offset is returned in the "meta" object.
  • inflate
    Adds a number of items before the start offset and after the limit.
  • limit
    The page size/maximum number of result rows. Even if no limit is defined, you'll always just receive the allowed maximum number of entities.

If you also want to know the total number of entities available, you can set "includeTotalCount" to true. The totalCount is then included in the response object in the "meta" property.

Here are a few concrete examples:

# fetches the first 100 entities and includes the total count
body:
{
pagination: {
offset: 0,
limit: 100,
},
includeTotalCount: true
}
# fetches the next 100 entities
body:
{
pagination: {
offset: 100,
limit: 100,
}
}
# fetches the page that contains the entity with id "3124" with a limit/page size of 25 entities
body:
{
pagination: {
pageAtId: 3214,
limit: 25,
}
}

Order

Define how to order the fetched results by passing an array of "order" objects. If "order" is not specified, the results are ordered by the entity's sort index. You can set the following properties in the "order" object:

  • column
    The name of the column to order by
  • order
    Possible values: [asc, desc]

Here's a concrete example:

body:
{
order: [{
column: "title",
order: "asc",
}]
}

As it is an array, you can pass multiple "order" objects to sort by multiple columns.

In addition, you can also specify a sequence array, which defines a pre-defined partial ordering. That means, the entities are ordered as specified in the sequence array and the rest is ordered by the default value, or as defined by an additional order object:

  • seq
    list of entity ids
body:
{
order: [{
column: "id",
order: "seq",
seq: [3215, 3214]
}]
}

Filter

If you want to search for and find certain entities, you can pass filters to the request. You can define a single condition or connect multiple conditions with or or and.
The Testiny REST API offers the following operations for filtering:

  • eq - Checks for equality. Default operator for basic values.
  • not_eq - Checks for non-equality.
  • like - Checks that the value matches the specified pattern. The percent sign % represents zero, one, or multiple characters.
  • not_like - Checks that the value does not match the specified pattern.
  • in - Checks that the value is in the specified array. Default operator for array values.
  • not_in - Checks that the value is not in the specified array.
  • like_in - Checks that the value matches one of the specified patterns.
  • not_like_in - Checks that the value does not match any of the specified patterns.
  • gt - Checks that the value is greater than the specified number.
  • lt - Checks that the value is less than the specified number.
  • gte - Checks that the value is greater than or equal to the specified number.
  • lte - Checks that the value is less than or equal to the specified number.
  • range - Checks that the value is within the specified range, e.g. the range filter [0,10] equals to value >= 0 and value <= 10.
  • has_any - Used for multi value fields, e.g. custom fields with type "multi-select". Checks that values include (at least) one of the specified values.
  • has_none - Used for multi value fields, e.g. custom fields with type "multi-select". Checks that values do not include the specified values.
  • has_all - Used for multi value fields, e.g. custom fields with type "multi-select". Checks that values include all of the specified values.

Here's a concrete example how to fetch test cases where the title starts with "App":

POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
"filter": { "title": { "op": "like", "value": "App%" } }
}

Use (nested) and and or operators to join conditions:

# fetches all entities where project_id equals 3 (`eq` operator is used by default) AND where
# priority is in [0,1] (`in` operator is used by default)
body:
{
"filter": { "and": ["project_id": 3, "priority": [0,1]] }
}

# the "and" operator can be omitted in this case, as multiple filters are joined with "and" by default:
body:
{
"filter": { "project_id": 3, "priority": [0,1] }
}
# fetches all entities where project_id equals 3 AND (where the id equals 99901 OR where the title contains 99901)
body:
{
"filter": { "and": [{ "project_id": 3 }, { "or": { "id": { "op": "eq", "value": 99901 }, "title": { "op": "like", "value": "%99901%" }
}

Here are more example on how to use different operators:

# fetch all entities where the value is greater than the specified number
body:
{
"filter": { "some_column": { "op": "gte", "value": 4 } }
}
# fetch all entities where the value is in specified range
body:
{
"filter": { "some_column": { "op": "range", "value": [0, 10] } }
}
# fetch all entities where the boolean value is true
body:
{
"filter": { "some_boolean_column": true }
}
# fetch all entities where the multi value field contains one of the specified values
body:
{
"filter": { "multi_value_column": { "op": "has_any", "value": ["linux", "osx"] } }
}
# fetch all entities where the multi value field contains all of the specified values
body:
{
"filter": { "multi_value_column": { "op": "has_all", "value": ["linux", "osx"] } }
}
# fetch all entities where the multi value field does not contain value "null" (i.e. all entities where the field does have a value set)
body:
{
"filter": { "multi_value_column": { "op": "has_none", "value": [null] } }
}

```bash
# fetch all entities where the multi value field does not contain value "linux" (i.e. the entities where other values might be set, but not "linux")
body:
{
"filter": { "multi_value_column": { "op": "has_none", "value": ["linux"] } }
}

#### ComputedColumns

Computed columns are prefixed with `$` and automatically resolve some attributes for improved performance. For example, the [mapping between testruns and test cases](/docs/rest-api/add-remove-update-mappings-between-test-run-entities-and-other-entities) includes the computed values "$assignee_name" "$comment_count" and "$workitem_count". Computed columns are always read-only.

By default, all computed columns are included. If you want to omit all computed columns, specify an empty array `[]` or pass in a list of column names that should be included, for example for testrun-testcase mapping `["$assignee_name"]`.

#### Id

Only return the entity with the given id. If the entity does not exist, no entities are returned.
This is a shortcut for using the [filter](#filter) `"filter": { "id": 3124 }`.

#### Ids

Only return the entities with the given ids. If one or more of the entities does not exist, only the existing entities are returned.
This is a shortcut for using the [filter](#filter) `"filter": { "ids": [3124, 3215] }`.

#### Map

<!-- TODO: api doc for test case query: describe type and query object -->
<!-- TODO: workitems are not in API => add api docs -->
<!-- TODO: "includeDeleted": update text in api doc as well to "soft-deleted" insteaf of "deleted" -->
<!-- TODO: why is omitLargeValues here false by default and in the datareadparams it's true by default? => fix in BE code -->
<!-- TODO: put default values everywhere? -->
<!-- TODO: describe maxSteps prop in API call -->
<!-- go through all boolean values and check default values -->

Entities might be mapped to other entities. For example, a test run can include multiple test cases, a test case folder can include multiple test cases, or a test plan can include multiple test case queries.
These mappings can exist between two or more entities.
The mapping might also have additional attributes. For example, the mapping between a run and test cases has the additional attributes "result_status" or "assigned_user_id".

If you not only want to fetch one entity, but also related entities you can do so by passing a `map` object. You can pass a single object to `map` or an array of objects. `Map` objects can also be nested if you want to extend the relationship over more than two entities, for example, when fetching test runs, test cases mapped to test runs, and comments mapped to test cases.

You can set the following properties in the `map` object:

| Parameter | Description |
| --------------------- | ------------------------------------- |
| entities | Used to specify mapped/associated entities. Pass the name of the database table or a string array of mapped entities, for example ["testcase", "testrun"]. The mapped entity values (i.e. the values of the mapping) are added as a property to the source entity object in the response. |
| entity| Used for direct mappings via foreign key. For example, "testplan" for test runs. If a direct mapping is used, `result`and `resultIncludeMapping` cannot be used. The mapped entity values (i.e. the values of the mapping) are added as a property to the source entity object in the response. |
| ids | Define a list of id tuples to filter the result. |
| inverse | Inverts the result to retrieve non-mapped entities. For example, retrieve all test cases not mapped to a test run. Default value: `false` |
| filter | Filter parameters for the source entity. [Learn more about filtering](#filter). |
| resultFilter | Filter parameters for the mapped entity. [Learn more about filtering](#filter). |
| countOnly | Only returns a count instead of a list of entities. Default value: `false` |
| idOnly | Only returns ids in the result, omitting all other properties. Default value: `false` |
| optional | Indicates whether the mapping is optional and may be 'null' in the result. If set to `false`, then only entities are returned that are mapped to the other entitiy. If set to `true`, then all (also unmapped) entities are retrieved. Note that if the `id` filter is set, include `null` in the filter if unmapped entities should be in the result. Default value: `false`. |
| includeDeleted | Includes soft-deleted member entities in the mapping search result. Default value: `false` |
| omitLargeValues | If set to `true`, omits 'large' column/prop values, that may contain long values, for example, text fields for descriptions or test case steps. Default value: `false` |
| omit | Omits the mapping result from result entities. This is used for intermediate nested mappings. |
| result | If no result entity is defined, the mapped entity values (i.e. the values of the mapping) are added as a property to the source entity object in the response. If `result` is specified (and set to a mapped entity), the entity values are returned instead of the mapped entity values (and also added as a property to the source entity object). |
| resultIncludeMapping | If `result` is specified, the mapped entity values are not returned. If you also want to include the mapped entity values, set `resultIncludeMapping` to `true`. Default value: `false` |
| map | Nested mapping starting from one of the member entities of this mapping. |

Here are a few examples for using `map`:

```bash
# retrieve all test cases and the test runs they are mapped to where the testrun_id is 10
# Note: if you don't filter by testrun_id, all testcase to testrun mappings are returned.
POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
"filter": { "project_id": 3 },
"map": [
{
"entities": ["testcase", "testrun"],
"filter": { "testrun_id": 10 }
}
]
}

# Example response:
{
"meta": {
"offset": 0,
"limit": 2000,
"count": 5
},
"data": [
{
"id": 999101, # test case values
"title": "Signing up a new user",
...,
"testrun_testcase_values": { # mapped entity values
"assigned_user_id": 108,
"$assignee_name": "John Smith",
"result_status": "FAILED",
"$comment_count": 2,
"$workitem_count": 0,
...
}
},
...
]
}
# retrieve all test cases mapped to a folder, where the testcase_folder_id is 9991 
# and with setting `result` to `testcase_folder` do not retrieve the mapped entities
# values but the entity values of the test case folder
POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
"map": {
"entities": ["testcase", "testcase_folder"],
"ids": { "testcase_folder_id": 9991 },
"result": "testcase_folder"
}
}
# retrieve (only) the total count of comments on a test case in a test run, where the 
# testcase_id is 999101 and the testrun_id is 10
POST 'https://app.testiny.io/api/v1/comment/find'

body:
{
"filter": { "project_id": 3 },
"includeTotalCount": true,
"countOnly": true,
"map": {
"entities": ["comment", "testcase", "testrun"],
"ids": { "testcase_id": 999101,"testrun_id": 10 },
}
}
# retrieve all test cases that are not mapped to a folder: 
# * map testcase to testcase_folder, retrive the entity values by setting `result` and
# set `optional` to true to include non-mapped test cases as well
# * filter by project_id is 3 AND testcase_folder.id is null (to retrieve only not mapped test cases)
POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
"map": {
"entities": ["testcase","testcase_folder"],
"result": "testcase_folder",
"optional": true,
"idOnly": true
}
"filter": { "and": [{ "project_id": 3 }, { "testcase_folder.id": null }]},
"order": [{ "column": "sort_index","order": "asc" }],
}
# retrieve all test cases that are not mapped to a test run with testrun_id 10 by
# setting `inverse` to true
POST 'https://app.testiny.io/api/v1/testcase/find'

body:
{
"filter": { "project_id": 3 },
"includeTotalCount": true,
"map": [
{
"entities": ["testcase", "testrun"],
"ids": { "testrun_id": 10 },
"inverse": true
}
]
}

OmitForbidden

This is false by default. If entities are requested that are not allowed to be read i.e. the requesting user has no permissions, an error is thrown. If omitForbidden is set to true the forbidden entities are omitted and will not be returned, and no error is thrown.

AsOf

Define an asOf timestamp to fetch a historical version of an entity (if available). The timestamp is in millisecond precision and UTC.

body:
{
"asOf": "2024-06-18T14:00:21.991Z"
}

Modifying entities

As well as retrieving data, you can also add, update, delete and undelete entities. You can modify single entities or perform bulk operations and modify multiple entities at once.

Creating entities

Use the "create" POST requests to create entities. To create a single entity use the route /api/v1/<entity>/:id. To create multiple entities at once use the route /api/v1/<entity>/bulk.

The response is the created entity, or for bulk creates an array of the created entities.

Here's an example how to create a single test run:

POST https://app.testiny.io/api/v1/testrun

body:
{
"title": "My test run",
"project_id": 3,
"description": "created via API"
}

Use the "bulk" route to create multiple test runs at once:

POST https://app.testiny.io/api/v1/testrun/bulk

body:
{
"title": "My test run",
"project_id": 3,
},{
"title": "Another test run",
"project_id": 3,
}

Updating entities

Use the "update" PUT requests to update entities. To update a single entity use the route /api/v1/<entity>/:id. To update multiple entities at once use the route /api/v1/<entity>/bulk.

Here's an example how to update a single test run with ID 3 and add the run to the test plan with ID 10:

PUT https://app.testiny.io/api/v1/testrun/3

body:
{
"id": 3, "testplan_id": 10
}

Use the "bulk" route to update multiple test runs at once:

PUT https://app.testiny.io/api/v1/testrun/bulk

body:
{
"id": 3, "testplan_id": 10
},{
"id": 4, "testplan_id": 10
}

Confligt tag _etag

When updating an entity, a conflict tag must be passed along (or the update must be forced), otherwise you'll receive the error Conflict tag is enabled for this entity but missing in the input data. When fetching data, the _etag is included in the response. Pass it forward when updating entities to detect conflicts.

When receiving the error Update conflict: The entity was already modified elsewhere., the passed conflict tag was not up to date anymore. Re-fetch the data to get the new data; or force the update if you're sure you want to potentially overwrite data.

Deleting and undeleting entities

Use DELETE requests to delete entities. To delete a single entity use the route /api/v1/<entity>/:id. To delete multiple entities at once use the route /api/v1/<entity>/bulk. If the entity type supports soft-deletion, these operations only soft-delete the entity meaning that you can undelete an entity to restore it.

The response is the deleted entity, or for bulk delets, an array of the deleted entities.

Here's an example how to delete a single test case with ID 3124:

DELETE https://app.testiny.io/api/v1/testcase/3124

Use the "bulk" route to delete multiple test cases at once:

DELETE https://app.testiny.io/api/v1/testcase/bulk

body:
{
"id": 3124
},{
"id": 3125
}

If you want to undelete an entity, use the following post request:

https://app.testiny.io/api/v1/testcase/undelete/:id

DELETE https://app.testiny.io/api/v1/testcase/undelete/3124

Use the "bulk" route to undelete multiple test cases at once:

POST https://app.testiny.io/api/v1/testcase/bulk/undelete

body:
{
"id": 3124
},{
"id": 3125
}

Modifying mappings

To create a mapping between two entities, or update mapping values, you have to use "mapping" API calls: /api/v1/<entity>/mapping/bulk/:otherEntities.

The path parameter :otherEntities needs to replaced with the name(s) of the other entities to map to, separated by colon ':'. For example, to modify a mapping of testcases to testruns, we would use the route /api/v1/testrun/mapping/bulk/testcase:testrun.

In general, the Testiny REST API offers the following operations for mappings: add, delete, update, add_or_update.
You also need to specify an operation with the query parameter op. To extend our example from above, we would use the following route to create a mapping between a test case and a run (i.e. to add the test case to a test run): /api/v1/testrun/mapping/bulk/testcase:testrun?op=add

Mappings between entities can also have attributes. For example, the mapping between a test case and a test run have, among others, the additional attributes "result_status" or "assigned_user_id".

note

Learn about fetching entities and entities mapped to them in the section above.

The following sections describe the most common use cases.

Add a test case to a test run

The following example shows how to add a test case to a test run. To achieve this, we need to create (use operation add or add_or_update) the testrun-testcase mapping.
We'll use the test run mapping route and set the entities to testcase:testrun and define an update operation with op=add_or_update.

In the body, we define which test case to add to which test run. In this example, we add the test case with ID 1234 to the test run with ID 3 and set the result_status to NOTRUN. Possible values for the result are "NOTRUN", "PASSED", "FAILED", "BLOCKED", "SKIPPED".

POST 'https://app.testiny.io/api/v1/testrun/mapping/bulk/testcase:testrun?op=add_or_update'

body:
{
"ids": { "testcase_id": 1234, "testrun_id": 3 },
"mapped": { "result_status": "NOTRUN" }
}

If you want to add multiple test cases to a run at once, you can simply specify multiple elements as shown here:

POST 'https://app.testiny.io/api/v1/testrun/mapping/bulk/testcase:testrun?op=add_or_update'

body:
[{
"ids": { "testcase_id": 1234, "testrun_id": 3 },
"mapped": { "result_status": "NOTRUN" }
},{
"ids": { "testcase_id": 1235, "testrun_id": 3 },
"mapped": { "result_status": "NOTRUN" }
}]

Set a test result

The following example shows how to set the result of a test case in a test run. To achieve this, we need to update the testrun-testcase mapping.
We'll use the test run mapping route and set the entities to testcase:testrun and define an update operation with op=update. This is the same API route as the sample above on how to add a test case to a test run.

In the body, we define which test case in which test run we want to update. In this example, we update the test case with ID 1234 in test run with ID 3 and set the result_status to PASSED. Possible values for the result are "NOTRUN", "PASSED", "FAILED", "BLOCKED", "SKIPPED".

For more information on this particular API call, see its description.

POST 'https://app.testiny.io/api/v1/testrun/mapping/bulk/testcase:testrun?op=update'

body:
{
"ids": { "testcase_id": 1234, "testrun_id": 3 },
"mapped": { "result_status": "PASSED" }
}

If you want to update multiple test cases from a test run at once, you can simply specify multiple elements in the body.

Removing a test case from a test run

In order to remove a test case from a run, you have to delete the testrun-testcase mapping.
We'll use the test run mapping route and set the entities to testcase:testrun with operation op=delete.

In the body, we define which test case we want to remove from which test run. In this example, we remove the test case with ID 1234 from test run with ID 3:

POST 'https://app.testiny.io/api/v1/testrun/mapping/bulk/testcase:testrun?op=delete'

body:
[{
"ids": { "testcase_id": 1234, "testrun_id": 3 }
}]

If you want to remove multiple test cases from a test run at once, you can simply specify multiple elements in the body.

More examples for common use cases

Add a comment to a result

To add a comment to a result, you first need to create a comment:

POST 'https://app.testiny.io/api/v1/comment/bulk'

body: [{ "project_id": 1," type": "TEXT", "text": "My comment" }]

The response includes the comment id of the newly created comment. Here's an example response:

[
{
"id": 205,
"created_at": "2024-09-25T21:09:57.317Z",
"created_by": 112,
"project_id": 1,
"status": "OPEN",
"type": "TEXT",
"target": "TRTC",
"text": "My comment",
...
}
]

Then, add/map the newly created comment to the test case in a test run:

POST 'https://app.testiny.io/api/v1/comment/mapping/bulk/comment:testcase:testrun?op=add'

body: [{"ids":{"comment_id":270,"testcase_id":9995001,"testrun_id":10} }]

Add an attachment to a test case

For this use case, our API offers an easy-to-use endpoint to upload files and attach them to a test case:

POST https://app.testiny.io/api/v1/testcase/upload-attachment/:id

Query parameters:
* title (REQUIRED)
* filename (REQUIRED)
* mime_type (optional) - if not set the content-type header is used

body: raw (image/file) data

Here's an example:

POST 'https://app.testiny.io/api/v1/testcase/upload-attachment/1234?title=MyScreenshot&filename=screenshot.jpeg&mime_type=image/jpeg'

body: raw (image/file) data

This request uploads the file and adds it as an attachment to test case with ID "1234". Here's an example response (returns the uploaded blob):

{
"id": 15,
"title": "MyScreenshot",
"project_id": 1,
"created_at": "2024-09-25T21:33:43.429Z",
"created_by": 112,
"blob_id": "a886df4673e7e778cfdaaae5a50f8807",
"mime_type": "image/jpeg",
"filename": "screenshot.jpeg",
"size": 191646,
...
}

Add an attachment to a result

For this use case, our API offers an easy-to-use endpoint to upload files and attach them to a result of a test case in a test run:

POST https://app.testiny.io/api/v1/testrun/upload-attachment/:tr-id/:tc-id

Query parameters:
* title (REQUIRED)
* filename (REQUIRED)
* mime_type (optional) - if not set the content-type header is used

body: raw (image/file) data

Here's an example:

POST 'https://app.testiny.io/api/v1/testrun/upload-attachment/10/1234?title=MyXMLFile&filename=results.xml'

body: raw (image/file) data

This request uploads the file and adds it as an attachment to the test case with ID "1234" in the test run with ID "10". Here's an example response (returns the uploaded blob):

{
"id": 17,
"title": "MyXMLFile",
"project_id": 1,
"created_at": "2024-09-25T21:44:12.545Z",
"created_by": 112,
"blob_id": "1fac4d1b0390766c80b8ae2f296fbc3f",
"mime_type": "text/xml",
"filename": "results.xml",
"size": 4523,
...
}