Pushing Automated Test Results to qTest via the REST API: Cycles, Bulk Runs, and Traceability
Learn how to integrate CI/CD pipelines with qTest's v3 API, create test cycles, submit bulk test runs, and keep traceability to Jira stories.
11 Feb 2026, 01:04 UTC

The problem: CI/CD pipelines need a reliable way to publish automated results into qTest
Most teams run Selenium, Cypress, or Playwright tests in a pipeline and then struggle to get those results into qTest so they appear alongside manual test cases and Jira stories. The qTest REST API v3 offers a direct path, but the documentation spreads the required steps across several endpoints — authentication, status‑ID discovery, test‑cycle creation, bulk run submission, and attachment upload. Missing any piece breaks traceability or causes 429 rate‑limit errors during large regression suites.
Thesis: A small, repeatable sequence of API calls — executed from the CI agent — gives you full traceability with minimal latency
By scripting the following four steps you can push every pipeline run into its own test cycle, submit up to 500 test runs in a single request, attach logs or screenshots, and keep the link to the original Jira issue (provided the qTest‑Jira connector is configured). The pattern works for cloud and on‑premise instances, though you must verify the exact API version on your server.
1. Authenticate and discover project‑specific status IDs
qTest uses bearer tokens (UI → Profile → API Token) or OAuth 2.0 client‑credentials for server‑to‑server calls. The token must belong to a user with Project Admin rights on the target project.
# Run on the CI agent (bash/PowerShell)
TOKEN="${QTEST_API_TOKEN}"
PROJECT_ID=12345
curl -s -H "Authorization: Bearer $TOKEN" \
"https://your-instance.qtestnet.com/api/v3/projects/$PROJECT_ID/test-run-statuses" | jq .
Typical response (IDs are project‑specific):
[
{"id":1,"name":"Pass"},
{"id":2,"name":"Fail"},
{"id":3,"name":"Blocked"},
{"id":4,"name":"Not Run"}
]
Check: HTTP 200 and at least one status object. Risk: Assuming default IDs (1 = Pass) without querying leads to mis‑recorded results.
2. Create a dedicated test cycle for the pipeline run
A test cycle groups all automated runs belonging to a single build, sprint, or release. Create it once per pipeline execution.
CYCLE_NAME="build-${BUILD_NUMBER}-$(date +%Y%m%d)"
START_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
END_DATE=$(date -u -d "+1 hour" +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$CYCLE_NAME\",\"planned_start_date\":\"$START_DATE\",\"planned_end_date\":\"$END_DATE\"}" \
"https://your-instance.qtestnet.com/api/v3/projects/$PROJECT_ID/test-cycles" | jq .id
Capture the returned test_cycle_id (e.g., 9876) for the next step. Verification: HTTP 201 and a numeric id field. Limitation: Deleting test cycles via API is not supported; clean‑up must be done in the UI, so use a unique name per run.
3. Submit test runs in bulk (up to 500 per request)
Collect results from your test framework (JUnit XML, Cypress JSON, etc.) and transform each case into the payload shape qTest expects. Required fields: test_case_id, test_cycle_id, status_id. Optional but useful: executed_by, started_date, note (for logs), and custom_field_values for environment/browser.
# Example payload for two automated cases
PAYLOAD=$(cat <<'EOF'
[
{
"test_case_id": 111,
"test_cycle_id": 9876,
"status_id": 1,
"executed_by": "ci-agent",
"started_date": "2026-11-12T06:00:00Z",
"note": "Cypress run #42\nAll assertions passed",
"custom_field_values": [
{"field_id": 1001, "value": "chrome-119"},
{"field_id": 1002, "value": "staging"}
]
},
{
"test_case_id": 112,
"test_cycle_id": 9876,
"status_id": 2,
"executed_by": "ci-agent",
"started_date": "2026-11-12T06:00:00Z",
"note": "Assertion failed on login",
"custom_field_values": [
{"field_id": 1001, "value": "firefox-118"},
{"field_id": 1002, "value": "staging"}
]
}
]
EOF
)
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://your-instance.qtestnet.com/api/v3/projects/$PROJECT_ID/test-runs/bulk" | jq .
Expected check: HTTP 200/201 with an array of created run IDs. Rate‑limit guard: Inspect response headers X-RateLimit-Limit and X-RateLimit-Remaining; implement exponential back‑off if remaining drops below 10. Risk: Custom fields must be pre‑created in the qTest UI and referenced by numeric field_id, not by name.
4. Attach logs or screenshots to individual runs (optional)
If you need to store a full log file or a failure screenshot, use the multipart attachment endpoint. The limit is 50 MB per file.
RUN_ID=55555
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/workspace/logs/cypress-run-42.log" \
"https://your-instance.qtestnet.com/api/v3/projects/$PROJECT_ID/test-runs/$RUN_ID/attachments"
Verification: HTTP 201 and a returned attachment ID. Limitation: Attachments cannot be removed via API; they persist until a UI admin deletes them.
Trade‑off: Bulk vs. per‑run calls
Bulk submission reduces network round‑trips dramatically (one HTTP request for hundreds of cases) and keeps pipeline duration low. The downside is that a single malformed object aborts the entire batch — qTest returns a 400 with the index of the offending item. For very large suites (>500 runs) you must chunk the payload and retry failed chunks. If you prefer simpler error isolation, fall back to individual POST /test-runs calls, accepting the added latency.
Actionable closing: Validate the end‑to‑end flow before wiring it into production pipelines
- Run the status‑ID query (step 1) in a shell or Postman to confirm the mapping for your project.
- Create a test cycle (step 2) and note the returned
test_cycle_id. - POST a single test run with a known
test_case_id(use the non‑bulk endpoint) to verify that the result appears in the qTest UI and is linked to the Jira story (if the connector is active). - Scale to the bulk endpoint once the single‑run test succeeds.
- Add rate‑limit header checks and exponential back‑off to your CI script.
After the validation run, promote the script to a shared library (e.g., a reusable Bash/Python module) so every pipeline can call publish_to_qtest() with its own result file. Remember to review qTest release notes (2024.1+ bulk‑endpoint enhancements) and the Swagger docs at https://<your-instance>.qtestnet.com/api/v3/docs/ whenever you upgrade the qTest instance, because API v3 versioning is independent of the UI version.
Note: The field IDs, status IDs, and rate‑limit defaults shown here reflect the current public documentation as of 2026‑11‑12. Verify each value against your own instance before committing to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.