Skip to main content

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-token header 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:

FieldTypeNotes
data[name]stringRun name
data[project]stringThe project's projectId
data[source]stringWhere it ran
data[tags][]stringRepeat once per tag
data[milestone]stringMilestone name, case-insensitive, within this project. Optional
xmlReportsfileRepeat 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/.jpeg and .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:

FieldTypeNotes
manifeststringA text field holding the JSON manifest below
snapshotsfileimage/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
}
]
}
FieldNotes
branchOptional. The branch the snapshots belong to. Omitted or empty means the default branch
defaultBranchOptional. Your default branch. When omitted, main and master count as default
entriesOne per image, at most 100
entries[].fileThe filename of its snapshots part. A .png name, at most 200 characters
entries[].nameThe snapshot's name, at most 200 characters
entries[].browserThe Playwright project name, at most 100 characters
entries[].viewportThe CSS viewport, width and height
entries[].platformOptional. linux, darwin or win32, at most 20 characters
entries[].testTitleOptional, at most 500 characters
entries[].testFileOptional, at most 300 characters
entries[].thresholdOptional. Per-pixel colour tolerance, 0 to 1. Default 0.1
entries[].maxDiffPixelsOptional. Whole number, 0 or more. Default 0
entries[].maxDiffPixelRatioOptional. 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": ["…"]
}
  • result is unchanged, changed, new or failed. A failed snapshot says why in reason. See Results.
  • review is what a person has decided since: null, approved or rejected. It is always null for a snapshot compared in this request, and matters when snapshots are sent to a run again (below).
  • diffRatio is the share of the image's pixels that differ, from 0 to 1.
  • data counts each result, and approved and rejected count the reviews.
  • reviewUrl is the run's page on Visual Testing.
  • warnings is 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 } }.

StatusMeaning
400The 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
403Invalid, expired or missing token (Invalid or missing API token), or details.name is FeatureNotInPlan: visual testing is not on your plan
404The run id does not exist, or belongs to another organization
503Sekee 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​

StatusMeaning
200 / 201Accepted
400Missing required fields, or the project was not found
403Invalid, expired or missing token
404The run id does not exist, or belongs to another organization
503snapshots 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, not files. A request sending files was answered 400 No XML files found in the request.
  • complete is a PUT, not a POST.
  • thread and complete now check that the run belongs to the organization the token names, and answer 404 when it does not. PUT and DELETE on /api/automation-runs/:id have 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.