Ingestion API
The endpoints sekee-cli calls. Use them directly if
you would rather not add a dependency to your pipeline.
All of them:
- Live under
{base-url}/api/ - Authenticate with an
x-api-tokenheader carrying an API token - Derive the organization from that token
:::caution auth: false does not mean public
These routes are declared without Strapi's own auth because they use the token
header instead. The controller validates the token, checks it is active and
unexpired, stamps its last-used time, and derives the tenant from it. Any new
ingestion route must follow the same path.
:::
POST /api/automation-runs
Creates a completed run from one or more reports, in one request.
multipart/form-data:
| Field | Type | Notes |
|---|---|---|
data[name] | string | Run name |
data[project] | string | The project's projectId |
data[source] | string | Where it ran |
data[tags][] | string | Repeat once per tag |
data[milestone] | string | Milestone name, case-insensitive, within this project. Optional |
xmlReports | file | Repeat once per XML report |
curl -X POST "$SEKEE_URL/api/automation-runs" \
-H "x-api-token: $SEKEE_API_TOKEN" \
-F 'data[name]=Nightly regression' \
-F 'data[project]=1' \
-F 'data[source]=GitHub Actions' \
-F 'data[tags][]=nightly' \
-F 'data[milestone]=2.4 GA' \
-F 'xmlReports=@./reports/results.xml'
An unknown or closed milestone name does not fail the request. The run is
created without one, and the response carries a warnings array:
{ "data": { "id": 42, "...": "..." }, "warnings": ["Milestone \"2.4 GA\" not found in this project"] }
POST /api/automation-runs/init
Creates a run with status running and returns its id. It takes the same
optional milestone, with the same warnings behavior.
curl -X POST "$SEKEE_URL/api/automation-runs/init" \
-H "x-api-token: $SEKEE_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"data":{"name":"PR #42","project":"1","source":"CI","tags":["pr"],"milestone":"2.4 GA"}}'
POST /api/automation-runs/:id/thread
Appends one report to a running run and increments its counts.
multipart/form-data with the thread data[name], one xmlReports entry, and
optionally one screenshots entry per image.
curl -X POST "$SEKEE_URL/api/automation-runs/5/thread" \
-H "x-api-token: $SEKEE_API_TOKEN" \
-F 'data[name]=Shard 1' \
-F 'xmlReports=@./results.xml' \
-F 'screenshots=@./test-results/login/test-failed-1.png;filename=login__test-failed-1.png'
Screenshots
A screenshot is matched to its test by a key. In the report, the failed test's
<system-out> names it with [[ATTACHMENT|<key>]] — the line Playwright
writes, with its local path replaced by the key — and the uploaded part's
filename is that same key.
- Keys are matched within the thread; the same key in another thread is a different screenshot.
- Only
.png,.jpg/.jpegand.webp, up to 10 MB each and 50 per thread, sent with their real image type. - A screenshot is kept only when a test written from this report references it.
- Screenshots count against your storage, and are kept for your plan's artifact retention (see Plans and billing).
- A screenshot never fails the request. One that is refused — wrong type, too
large, unreferenced, over the cap, or over your storage — is left out and
explained in the response's
warnings:
{ "data": { "id": 5, "...": "..." }, "warnings": ["Screenshot \"stray.png\" was not stored: no test result written from this report references it."] }
sekee-cli thread does all of this for you: see the CLI.
:::note Recently changed
thread used to take the report only. Screenshots are accepted from October 2026;
a server from before then ignores the screenshots parts.
:::
POST /api/automation-runs/:id/snapshots
Sends visual snapshots for a run. Sekee compares each one against its approved baseline before it answers, and the answer says what it found. Business plan.
:::note Recently changed This endpoint is new. :::
A run takes snapshots while it is running, and once completed until it is 24 hours old.
multipart/form-data:
| Field | Type | Notes |
|---|---|---|
manifest | string | A text field holding the JSON manifest below |
snapshots | file | image/png. Repeat once per image. Each part's filename is one manifest entry's file |
The manifest:
{
"branch": "feature/checkout",
"defaultBranch": "main",
"entries": [
{
"file": "chromium__inventory.png",
"name": "inventory",
"testTitle": "Inventory > shows items",
"testFile": "tests/inventory.spec.ts",
"browser": "chromium",
"viewport": { "width": 1920, "height": 1080 },
"platform": "linux",
"threshold": 0.1,
"maxDiffPixels": 0,
"maxDiffPixelRatio": 0
}
]
}
| Field | Notes |
|---|---|
branch | Optional. The branch the snapshots belong to. Omitted or empty means the default branch |
defaultBranch | Optional. Your default branch. When omitted, main and master count as default |
entries | One per image, at most 100 |
entries[].file | The filename of its snapshots part. A .png name, at most 200 characters |
entries[].name | The snapshot's name, at most 200 characters |
entries[].browser | The Playwright project name, at most 100 characters |
entries[].viewport | The CSS viewport, width and height |
entries[].platform | Optional. linux, darwin or win32, at most 20 characters |
entries[].testTitle | Optional, at most 500 characters |
entries[].testFile | Optional, at most 300 characters |
entries[].threshold | Optional. Per-pixel colour tolerance, 0 to 1. Default 0.1 |
entries[].maxDiffPixels | Optional. Whole number, 0 or more. Default 0 |
entries[].maxDiffPixelRatio | Optional. 0 to 1. Default 0 |
The name, browser, viewport and platform together are the snapshot's identity. See Visual testing for what each one means, and Tolerances for the last three.
curl -X POST "$SEKEE_URL/api/automation-runs/5/snapshots" \
-H "x-api-token: $SEKEE_API_TOKEN" \
-F 'manifest=<manifest.json' \
-F 'snapshots=@./test-results/sekee-snapshots/chromium__inventory.png;type=image/png'
manifest=<manifest.json sends the file's contents as a text field, which is
what the manifest is. snapshots=@… sends a file part named after the file.
Each image is PNG only, at most 20 MB and 16,777,216 pixels (4096 × 4096). A run holds at most 2,000 snapshots. Images count against your storage, and the ones Sekee keeps follow your plan's artifact retention.
The response
{
"data": { "runId": 5, "total": 3, "unchanged": 1, "changed": 1, "new": 1, "failed": 0, "duplicates": 0, "approved": 0, "rejected": 0 },
"snapshots": [
{ "name": "inventory", "browser": "chromium", "viewport": "1920x1080", "result": "changed", "review": null, "diffRatio": 0.0123, "reason": null }
],
"reviewUrl": "https://app.sekee.io/acme/visual/5",
"warnings": ["…"]
}
resultisunchanged,changed,neworfailed. Afailedsnapshot says why inreason. See Results.reviewis what a person has decided since:null,approvedorrejected. It is alwaysnullfor a snapshot compared in this request, and matters when snapshots are sent to a run again (below).diffRatiois the share of the image's pixels that differ, from 0 to 1.datacounts each result, andapprovedandrejectedcount the reviews.reviewUrlis the run's page on Visual Testing.warningsis there only when Sekee has something to say: a manifest entry it could not use and skipped, snapshots over the run's limit, a storage allowance reached. Print them.
Sending snapshots to a run again is safe. A snapshot the run already has is
a duplicate: Sekee answers with its first result and its review, counts it in
duplicates as well as under that result, and writes nothing. That is what lets
a CI job re-run against the same run pass once its changes are approved.
A different image for a snapshot the run already has — a re-run job capturing again — is answered the same way, with the run's first capture, plus a warning. The run keeps its first capture, and the new image is not compared.
Errors
All of them are { data: null, error: { status, name, message, details } }.
| Status | Meaning |
|---|---|
400 | The request carries no snapshots parts, or the manifest is not valid JSON or lists no entries. Or details.code is RunClosed: the run is completed and more than 24 hours old, so it takes no more snapshots |
403 | Invalid, expired or missing token (Invalid or missing API token), or details.name is FeatureNotInPlan: visual testing is not on your plan |
404 | The run id does not exist, or belongs to another organization |
503 | Sekee is busy comparing. Nothing from the request was written. Wait for the number of seconds in the Retry-After header and send the same request again |
sekee-cli snapshots does all of this for you, including the batching and the
retries: see the CLI.
PUT /api/automation-runs/:id/complete
Marks a running run completed.
curl -X PUT "$SEKEE_URL/api/automation-runs/5/complete" \
-H "x-api-token: $SEKEE_API_TOKEN"
The Milestones routes are not part of this API
/api/milestones (list, read, create, update, delete, close, reopen) and
PUT /api/automation-runs/:id/milestone all exist, but every one of them is
declared without the token auth this page otherwise documents — they take
an ordinary signed-in session, the same as the rest of the app, and follow the
roles in Roles and permissions.
An x-api-token gets nowhere against them.
They are listed here only so a 404 or 403 from one of them is not mistaken
for a bug in the ingestion routes above. If a script needs to put a run on a
Milestone, milestone on /api/automation-runs or /init is the way in — see
above. For anything else, the AI connector
reaches Milestones (sekee_list_milestones, sekee_get_milestone,
sekee_create_milestone, sekee_update_milestone, sekee_set_milestone) as
you, through your own session.
Responses and errors
| Status | Meaning |
|---|---|
200 / 201 | Accepted |
400 | Missing required fields, or the project was not found |
403 | Invalid, expired or missing token |
404 | The run id does not exist, or belongs to another organization |
503 | snapshots only: Sekee is busy comparing. Retry after the Retry-After header's seconds |
:::note Recently changed
Three corrections, all of them things this page previously got wrong:
- The file field is
xmlReports, notfiles. A request sendingfileswas answered400 No XML files found in the request. completeis aPUT, not aPOST.threadandcompletenow check that the run belongs to the organization the token names, and answer404when it does not.PUTandDELETEon/api/automation-runs/:idhave been removed — they were never documented here, had no token check, and nothing called them.
:::
What ingest does besides storing the report
For each test named in a report, ingest also:
- Finds or creates an automation test by a fingerprint of
"<suite> :: <name>"within the project - Writes an automation test result for that test in that run
- Writes or increments an error row for each distinct failure, guarded so a re-sent report cannot inflate the counts
- Refreshes Jira remote links for any linked test case, if the integration is on
Other APIs
The rest of the product's API is not a documented public surface. If you want an assistant or a script to read and write your test library, use the AI connector — it goes through the same permissions you have, rather than around them.