Reference: POST /runs/ingest
Endpoint for recording the results of a run in Qably as JSON: authentication, examples, fields, and responses.
Ingests execution results for a test suite. Authenticate using Authorization: Bearer <project API key>. The target project and organization are resolved directly from the key.
Example request
curl --fail --silent \
--request POST \
"https://api.qably.dev/runs/ingest" \
--header "Authorization: Bearer $QABLY_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"externalId": "gh-run-482913",
"source": "github_actions",
"suiteId": "suite_123",
"name": "Checkout regression - main",
"startedAt": "2026-09-01T10:00:00Z",
"finishedAt": "2026-09-01T10:04:12Z",
"commitSha": "a1b2c3d",
"commitMessage": "fix: checkout rounding",
"commitAuthor": "Ada Lovelace",
"cases": [
{ "name": "Adds an item to the cart", "status": "pass" },
{
"name": "Applies a discount code",
"steps": ["open cart", "apply code SAVE10"],
"expectedResult": "total is reduced by 10%",
"status": "fail"
}
]
}'const response = await fetch('https://api.qably.dev/runs/ingest', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QABLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalId: 'gh-run-482913',
source: 'github_actions',
suiteId: 'suite_123',
name: 'Checkout regression - main',
startedAt: '2026-09-01T10:00:00Z',
finishedAt: '2026-09-01T10:04:12Z',
commitSha: 'a1b2c3d',
commitMessage: 'fix: checkout rounding',
commitAuthor: 'Ada Lovelace',
cases: [
{ name: 'Adds an item to the cart', status: 'pass' },
{
name: 'Applies a discount code',
steps: ['open cart', 'apply code SAVE10'],
expectedResult: 'total is reduced by 10%',
status: 'fail',
},
],
}),
});import os
import requests
response = requests.post(
"https://api.qably.dev/runs/ingest",
headers={"Authorization": f"Bearer {os.environ['QABLY_API_KEY']}"},
json={
"externalId": "gh-run-482913",
"source": "github_actions",
"suiteId": "suite_123",
"name": "Checkout regression - main",
"startedAt": "2026-09-01T10:00:00Z",
"finishedAt": "2026-09-01T10:04:12Z",
"commitSha": "a1b2c3d",
"commitMessage": "fix: checkout rounding",
"commitAuthor": "Ada Lovelace",
"cases": [
{"name": "Adds an item to the cart", "status": "pass"},
{
"name": "Applies a discount code",
"steps": ["open cart", "apply code SAVE10"],
"expectedResult": "total is reduced by 10%",
"status": "fail",
},
],
},
)
response.raise_for_status()Request fields
| Field | Required | Notes |
|---|---|---|
externalId | yes | Non-empty string. Idempotency key used to update existing records instead of creating duplicates. |
source | no | "api" (default) or "github_actions". |
suiteId / suiteName | exactly one | An unresolvable suiteId returns 404. An unresolvable suiteName registers the suite automatically. |
name | yes | Display name for the test run, up to 200 characters. |
startedAt / finishedAt | no | ISO 8601 timestamps with explicit time zone offset. |
commitSha / commitMessage / commitAuthor | no | Optional commit metadata, up to 64, 2000, and 200 characters respectively. |
cases | yes | Array containing at least one test case. |
Case fields
| Case field | Required | Notes |
|---|---|---|
name | yes | Up to 372 characters. |
suiteName | no | Defaults to the resolved suite name; allows attaching auxiliary labels (such as Playwright project names) for audit trails. |
steps | no | Array of strings, up to 50 items (500 characters max each). Empty by default on JUnit imports; can be populated when posting JSON directly. |
expectedResult | no | Up to 1000 characters. Empty by default on JUnit imports. |
status | yes | One of pending, running, pass, fail, skip, blocked. |
recordedAt | no | ISO 8601 timestamp with explicit time zone offset. |
Run status
Run status is determined on the server and does not trust client-calculated summaries. A single failing case marks the entire run as failed. Unresolved pending or running cases maintain the run in an executing state. A run passes only when at least one case passes or is skipped with no accompanying failures. Runs where all cases are blocked count as failed because no verifications took place.
Replays and idempotency
Submitting matching (project, source, externalId) tuples updates the existing run instead of duplicating records. The case list is replaced entirely, and optional metadata fields are updated when provided in subsequent payloads. The API responds with 200 OK on both initial creation and replays.