Send test results
TestLookup accepts four kinds of input: finished report files, JSON batches of results, live events sent while your tests run, and report files saved in its file storage. The way you choose decides what you send and which status you can follow.
Choose how to send results
Section titled “Choose how to send results”- Report file: send a supported report file to
POST /api/v1/ingest/file. Use this for most CI pipelines. The upload form in the web app uses the same route. You find the form under Runs › Upload report, or at/runs?upload=1, while themanual_uploadflag is on. The CLI uses it too, withtestlookup upload fileandtestlookup upload dir. - JSON batch: post results you have already collected to
POST /api/v1/ingest. - Live events: stream results while the suite runs, through the live session and event routes. See live protocols.
- Drop folder: save results in the results bucket of TestLookup’s file storage, under a new
<project>/runs/<build>/folder. Write a marker file namedupload_complete.jsonlast, to tell TestLookup the upload is finished. A scheduled job then picks up the new folder within about a minute. It runs while thedrop_folder_watcherflag is on, which is the default, and it needs the scheduler (beat), which the smaller dev-lite setup leaves out. - Storage webhook: an optional, older way to send results. It suits an external S3 server that sends notifications with a credential. It is not connected to the bundled S3 store.
The drop folder and the storage webhook read only two formats, from folders next to the marker file:
- Allure results named
*-result.json, from theallure/folder; - TestNG reports named
*.xml, from thetestng/folder.
They do not detect formats, read other formats or open ZIP files. They also finish the run straight away, instead of passing it through the file worker. Send every other format to the file route.
Credentials
Section titled “Credentials”Send either a sign-in token (JWT) in an Authorization: Bearer header, or an API key in the X-API-Key header. TestLookup refuses an API key sent as a bearer token (HTTP 401). Your project membership, your role, the key’s project and the actions the key allows all still apply. Knowing a run’s ID does not give you access to it.
Sending a JSON batch or a file, and opening or closing a live session, needs the QA engineer role or higher. An API key acts as the person who owns it. Only a QA engineer or higher can create one. There are two kinds:
- Personal key: create it under Settings › Account › My API keys (
/settings/my-api-keys). Use it for your own scripts and CI jobs. - Project key: only an administrator can create one, under Settings › Project › Streaming API keys. An administrator can also create one with
POST /api/v1/keysand aproject_id.
Two live routes accept only a project key. The first is POST /api/v1/stream/ingest. The second is POST /ws/events/{run_id}, while LIVE_EVENTS_REQUIRE_PROJECT_KEY=true, which is the default. If the project key is limited to certain actions, those must include stream:write. These routes refuse a personal key (HTTP 403). The session routes under /api/v1/stream/sessions accept a sign-in token or any API key.
Request fields
Section titled “Request fields”Send JSON to the batch route, with Content-Type: application/json. Send multipart form fields to the file route. The working examples below show both. To sign in from a script, send OAuth2 form fields to the login route. The endpoint pages in the API reference list the exact required fields, limits and responses.
A JSON batch takes:
- a build label of 1 to 100 characters;
- 1 to 50,000 results;
- the optional fields
run_contextandrun_ai; - for each result, optional timing fields,
service_nameandhttp_exchanges.
The file form takes:
- a
build_number, which must not be empty (an empty one gets HTTP 422); - an optional
run_context, as JSON text of at most 16,384 characters; - the optional fields
format(defaultauto),branch,commit_hash,release_nameandenvironment; - the optional CI fields
ci_provider,ci_repo,pr_number,ci_actor,ci_run_urlandjenkins_job; - the optional fields
executed_at,commit_range(as JSON text) andrun_ai.
TestLookup removes NUL bytes from the form fields. Only the file route accepts executed_at. The JSON batch does not, so do not assume that a client option with the same name works the same way on both routes.
The accepted response and its IDs
Section titled “The accepted response and its IDs”TestLookup answers an accepted request with HTTP 202, a run_id and a task_id. Keep both. The 202 response and its total_results value mean that TestLookup accepted the request. They do not mean that it read the report or stored the results.
The two routes treat run_id differently:
- JSON batch: if you send it again with the same project, build label and CI details, TestLookup returns the existing run and updates it.
- File upload: every upload creates a new run, even when its build label matches an earlier one.
Check a file upload’s status
Section titled “Check a file upload’s status”After a file upload, check its status with GET /api/v1/ingest/uploads/{task_id}. The status moves through pending, parsing and ingesting, and ends as succeeded or failed.
The route answers 404 when the status is unknown or has expired after 24 hours. It answers 403 when you cannot access the run’s project. A missing or expired status does not mean that no run was stored. The upload form in the web app checks for about 60 seconds, then says that the report is still being processed.
Read the finished run
Section titled “Read the finished run”Next, read the run with GET /api/v1/runs/{run_id}. Check its finalization, its analysis and its review separately. A JSON batch has no upload task to check, so read its run instead.
Limits and retry
Section titled “Limits and retry”Limits apply at different layers: files, archives, request rates and the server itself.
- Retry rule: on HTTP 429 or 503, wait for the time in the
Retry-Afterheader before you retry. - Rate limits: by default, each project can send 200 batches a minute and 20,000 single live events a minute.
- Busy server: once the job queue’s memory (Redis) passes 75% of its
maxmemorysetting, new requests get HTTP 503 withRetry-After: 5. - Storage webhook: it has none of these limits.
Do not repeat any other request without thought. Retry it only when its endpoint says that sending it twice is safe.
How TestLookup handles your results
Section titled “How TestLookup handles your results”Formats and file limits
Section titled “Formats and file limits”The file route accepts these format values: auto, junit, testng, allure, cypress, playwright, pytest, robot, cucumber, nunit, trx and xunit. With auto, TestLookup looks at the file’s content to find the format, because a file extension alone is not reliable. It opens ZIP reports with safety checks.
The Cypress and Playwright readers are on by default. An administrator can turn them off with feature flags. A file sent while its reader is off gets HTTP 503.
The file route reads uploads in 1 MiB pieces. It refuses an empty file and a file over 50 MiB. For ZIP archives, the defaults are:
- at most 200 MiB in total once unpacked;
- at most 5,000 files;
- at most 50 MiB for each file;
- at most 100 times larger once unpacked than the archive itself.
These limits apply at different layers. They are not promises about how much TestLookup can handle. See the archive safety code and the configuration reference.
A JSON batch needs a build label of at most 100 characters, at least one result, and a test name and status for each result. The accepted statuses are PASSED, FAILED, SKIPPED and BROKEN. Durations are whole milliseconds of 0 or more. Results read from report files can also be stored as UNKNOWN. The environment field is optional.
What happens after you send a report
Section titled “What happens after you send a report”- TestLookup checks your credential and finds the project. It checks your access to the project before it counts the request against the rate limits.
- It checks the request, the rate limits and the job queue’s memory. The memory check can also use an optional fixed limit.
- It stores the data you sent and hands the work to a background worker (Celery).
- The worker reads the report and turns each test into a standard result.
- When a run contains the same test twice, TestLookup keeps one result, with the worse outcome.
- It saves the run, its test results and their evidence.
- It finalizes the run: its totals, history, suites and link to a release.
- It records the follow-up jobs for the run, such as AI analysis and notifications, so that none are lost.
- Those jobs run later. Analysis can fail or be retried without changing the saved run.
The rate-limit and memory checks let traffic through when they cannot read their own data, so watch them in your monitoring. The feature page on collecting results shows this path as a diagram.
How TestLookup recognises a run
Section titled “How TestLookup recognises a run”A JSON batch sent again goes into the same run, while a file upload always creates a new run. A specific CI run URL keeps separate jobs apart, even when they share a build label. The API and the worker use the same rules to find a run. When TestLookup rejects rows, it records them without saving the full database values. See the import pipeline code.
The drop folder and the storage webhook
Section titled “The drop folder and the storage webhook”The drop folder job lists the build folders under each active project’s ID or slug, and looks for each new folder’s marker file. Folders that already had a marker file the first time the job looked are recorded, but never imported. A folder without a marker file stops being checked after 24 hours. Each new folder is then processed like a storage webhook notification.
TestLookup first checks a storage webhook notification and its credential. Then it fetches the marker file and the results, and stores and finalizes the run straight away. It reads Allure and TestNG results only. See the drop folder guide for setup.
Live events take a separate path. TestLookup holds them in Redis first, then stores them and finishes the run when the session closes.
When something goes wrong after the 202
Section titled “When something goes wrong after the 202”A file upload’s status is kept in Redis. Once it reaches succeeded or failed, it does not change, and it expires after 24 hours. You can read it only with access to the project.
Errors in reading, storing or the worker can happen after TestLookup answered 202. To find them, check:
- the upload’s error and progress;
- the rows the run rejected;
- the
run.receivedentry in the project’s activity; - the worker logs and the task attempts;
- the import metrics;
- where it applies, the dead-letter queue, a holding area for jobs that kept failing.
A status update can fail on its own, even when the run is stored. Run totals count the results actually stored, not the rows you sent. A queued request can contain sensitive test details, so control who can reach the file storage and how long it keeps files.
TestLookup writes to several stores and starts some work directly. So an upload is not one single transaction across file storage, the job queue and the database. TestLookup also does not promise to process everything exactly once. Once a run is stored, its list of follow-up jobs helps it recover. See the test-run pipeline guide and the known limits.
API reference pages
Section titled “API reference pages”For exact fields, credentials and errors, see the reference pages for file and batch uploads, live streams, live reporting and webhooks. The full app guide to sending results is on GitHub.
Working examples
Section titled “Working examples”These Bash examples use placeholder values. Use a project you can access and your own credentials. In PowerShell, use curl.exe with the right quoting, or run the examples in Git Bash or WSL.
export TL_URL='http://localhost:8000'export TL_PROJECT='replace-with-your-project-id'export TL_API_KEY='replace-with-your-project-key'
curl --fail-with-body "$TL_URL/api/v1/projects" \ -H "X-API-Key: $TL_API_KEY"
curl --fail-with-body "$TL_URL/api/v1/ingest" \ -H "X-API-Key: $TL_API_KEY" -H 'Content-Type: application/json' \ --data-binary @- <<JSON{"project_id":"$TL_PROJECT","build_number":"docs-example-1","framework":"pytest","environment":"staging","results":[{"test_name":"test_checkout","status":"FAILED","duration_ms":120,"error_message":"Expected 200, received 500"}]}JSON
curl --fail-with-body "$TL_URL/api/v1/ingest/file" \ -H "X-API-Key: $TL_API_KEY" \ -F "project_id=$TL_PROJECT" -F 'build_number=docs-example-2' \ -F 'format=junit' -F 'file=@results.xml'An accepted request gets a response like this one. The IDs here are examples:
{"status":"accepted","run_id":"00000000-0000-0000-0000-000000000002","task_id":"00000000-0000-0000-0000-000000000003","total_results":1}The total_results value counts the results you sent, not the results stored. For a file upload, follow the returned task ID. A separate background job processes a JSON batch. Either way, you may not be able to read a run yet just after TestLookup accepts it.
export TL_TASK_ID='task_id-from-the-response'export TL_RUN_ID='run_id-from-the-response'curl --fail-with-body "$TL_URL/api/v1/ingest/uploads/$TL_TASK_ID" \ -H "X-API-Key: $TL_API_KEY"curl --fail-with-body "$TL_URL/api/v1/runs/$TL_RUN_ID" \ -H "X-API-Key: $TL_API_KEY"
# Sign in as a person. If the account uses MFA, the response asks for a code instead of returning a token.export TL_USERNAME='your-username'read -r -s -p 'Password: ' TL_PASSWORD; echocurl --fail-with-body "$TL_URL/api/v1/auth/login" \ --data-urlencode "username=$TL_USERNAME" \ --data-urlencode "password=$TL_PASSWORD"For PDF downloads, evidence bundles and share links, see the Reports API. For the exact settings of an agent run, see Agent Invocations. Ask the agent catalog for agent IDs instead of copying them from a diagram. Downloads of files and streams may not have a complete JSON description.
Errors and edge cases
Section titled “Errors and edge cases”| Status | What it usually means (an endpoint’s own page comes first) |
|---|---|
| 400 | The format is wrong, the file is empty, or the request is malformed |
| 401 | The credential is missing, wrong or expired, or a sign-in or MFA code was rejected |
| 403 | Your role, the project, the API key or a policy refuses the action |
| 404 | The item does not exist or you cannot access it, or an upload status has expired |
| 409 | The item changed or already exists, or the report review rules refuse to send a report |
| 413 | The upload, form or archive is too large |
| 422 | A field failed validation, or the request was refused as invalid |
| 429 | Too many requests, or a sign-in lockout. Wait for the time in Retry-After |
| 503 | TestLookup is busy, storage or a model provider is unavailable, or the feature is off |
Most errors return a body like {"detail": ...}, where detail can be text, an object or a list of validation problems. SCIM, the automatic user setup from your company directory, uses its own error format. File downloads and live streams return different bodies. The API reference lists the errors each endpoint raises itself, and other layers can add more. Some endpoints return plain JSON objects, and the OpenAPI document cannot fully describe their fields.
Each endpoint has its own paging and filter names and its own maximums, so read its parameters. To pass several values, repeat the parameter (?field=a&field=b) without brackets. Time ranges, release filters and the environment change what a figure counts. Do not retry a POST request unless its endpoint says that sending it twice is safe. The build and CI details that link a retried batch to its run do not make other requests safe to repeat.
Use the API reference for the exact details of each operation.