CI/CD Results Upload
Push automated test results from your CI/CD pipeline into a Testably run. Supports JSON and JUnit XML formats. This endpoint is called automatically by the @testably.kr/playwright-reporter, @testably.kr/cypress-reporter, and @testably.kr/jest-reporter SDKs.
https://api.testably.app/v1SDK handles this automatically
In most cases you do not need to call this endpoint directly. Install one of the reporter SDKs and configure your TESTABLY_TOKEN and TESTABLY_RUN_ID environment variables — the SDK will call this endpoint after your test suite finishes. Direct calls are for advanced use cases.
/v1/resultsUpload one or more test results into an existing run. Results are matched by test_case_id. Each upload is idempotent — re-uploading the same test_case_id overwrites the previous result.
Authentication
Use a project-scoped CI token from Settings → API Tokens. The token must have write access to the target project.
Request Body Parameters
| Name | Type | Required | Description |
|---|---|---|---|
run_id | string | Required | The test run ID to upload results into |
results | array | Optional | Array of result objects (JSON format). Required if junit_xml is not provided. |
results[].test_case_id | string | Required | Test case ID to match within the run |
results[].status | string | Required | Result status: passed, failed, blocked, retest, not_tested |
results[].note | string | Optional | Optional note or error message from the test |
results[].elapsed | integer | Optional | Time spent in seconds |
format | string | Optional | Input format: "json" (default) or "junit" |
junit_xml | string | Optional | Raw JUnit XML string. Required if format is "junit". |
dry_run | boolean | Optional | If true, validates the payload without persisting results. Defaults to false. |
Example — JSON Format
{
"run_id": "run_abc123",
"format": "json",
"results": [
{
"test_case_id": "tc_001",
"status": "passed",
"elapsed": 12
},
{
"test_case_id": "tc_002",
"status": "failed",
"note": "AssertionError: expected 200 but got 500",
"elapsed": 8
},
{
"test_case_id": "tc_005",
"status": "blocked",
"note": "Staging DB unavailable"
}
]
}Example — JUnit XML Format
{
"run_id": "run_abc123",
"format": "junit",
"junit_xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n <testsuite name=\"Login Tests\" tests=\"3\">\n <testcase name=\"login with valid credentials\" time=\"0.012\" />\n <testcase name=\"login with invalid password\" time=\"0.008\">\n <failure>AssertionError: expected 200 but got 500</failure>\n </testcase>\n </testsuite>\n</testsuites>"
}Response — 200 Success
{
"success": true,
"uploaded_count": 3,
"failed_count": 0,
"stats": {
"passed": 2,
"failed": 1,
"blocked": 0,
"retest": 0,
"not_tested": 0
}
}Response — 207 Partial Failure
{
"success": false,
"partial_failure": true,
"uploaded_count": 2,
"failed_count": 1,
"stats": {
"passed": 1,
"failed": 1,
"blocked": 0,
"retest": 0,
"not_tested": 0
},
"errors": [
{
"test_case_id": "tc_999",
"error": "test_case_id not found in this run"
}
]
}Rate Limiting
Limit
60
requests per minute per token
Payload Size
10 MB
max request body size
On Limit Exceeded
429
check Retry-After header
Error Codes
| Status | Meaning | Description |
|---|---|---|
| 200 | OK | All results uploaded successfully. |
| 207 | Multi-Status | Partial success. Some results uploaded; see failed_count and errors array. |
| 400 | Bad Request | Invalid payload structure, missing required fields, or unknown status value. |
| 401 | Unauthorized | Missing or invalid Authorization header. |
| 403 | Forbidden | Token does not have write access to this project. |
| 404 | Not Found | run_id does not exist or does not belong to the authenticated project. |
| 429 | Too Many Requests | Rate limit exceeded. Retry after the time specified in Retry-After header. |
| 500 | Internal Server Error | Unexpected server error. Contact support if this persists. |
Using the SDKs (Recommended)
Rather than calling this endpoint directly, use one of the reporter SDKs. They handle authentication, run creation, and result upload automatically.
@testably.kr/playwright-reporter// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['@testably.kr/playwright-reporter', {
token: process.env.TESTABLY_TOKEN,
runId: process.env.TESTABLY_RUN_ID,
}]
],
});@testably.kr/jest-reporter// jest.config.ts
export default {
reporters: [
'default',
['@testably.kr/jest-reporter', {
token: process.env.TESTABLY_TOKEN,
runId: process.env.TESTABLY_RUN_ID,
}]
],
};