Ingest API v2 reference
Generated from the same OpenAPI document published at /api/ingest/v2/openapi.json (version 2.0.0). Authentication: X-API-Key header on every request (the liveness probes below are the exception).
GET /healthz
Liveness probe - Liveness probe. No authentication. Same response shape as /api/ingest/health, and any server implementing this contract should serve both. Prefer /api/ingest/health when probing through an edge or proxy that forwards only certain path prefixes: on m00nreport.com this path is answered by the web app rather than by this API, so it returns 200 with HTML. Validate the body for ok: true rather than trusting the status, and treat the result as advisory - this contract never requires a probe before reporting.
{
"ok": true,
"service": "ingest"
}Responses: 200 (Service is alive)
GET /api/ingest/health
Liveness probe (cloud-routable) - Liveness probe. No authentication. Same semantics as /healthz, but routable through an edge or proxy that forwards only certain path prefixes, such as /api/* - including m00nreport.com, where /healthz is not one of them. Probe this path first. Validate the body for ok: true rather than trusting the status, and treat the result as advisory - this contract never requires a probe before reporting.
{
"ok": true,
"service": "ingest"
}Responses: 200 (Service is alive)
POST /api/ingest/v2/run/start
Start a run - Creates a run. The project is derived from the API key. Returns the server-generated runId used by all subsequent calls.
| Field | Type | Required | Description |
|---|---|---|---|
launch | string | yes | Launch (run) title shown in the UI. |
tags | array | no | Labels shown on the launch, usable as dashboard filters. |
total | number | no | Expected total number of tests, if known upfront. |
startedAt | string | no | ISO 8601 timestamp. |
attributes | object | no | Optional flat key-value metadata shown on the run, e.g. environment or branch. Accepted but not schema-enforced at runtime: at most 20 entries are kept, keys are trimmed and truncated at 100 characters, values are stringified and truncated at 500. Anything past the 20th entry is dropped without an error. Any other top-level field is ignored. |
{
"launch": "Nightly regression",
"tags": [
"smoke"
],
"total": 42,
"startedAt": "2026-07-17T21:00:00.000Z"
}Responses: 200 (Success), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 500 (Unexpected server error)
POST /api/ingest/v2/test/start
Register a test attempt (fire-and-forget) - Registers a client-generated testId so steps can stream before the test finishes. Safe to send without awaiting the response. Linking a test to a test case, highest precedence first: 1. `caseId` / `caseIds` in the body - internal case ids, used as sent. 2. `annotations.caseId` / `annotations.caseIds` - the same internal ids, for runners whose annotation API is the only place a test can carry metadata. 3. A `[TC-N]` marker anywhere in titlePath - N is the per-project case NUMBER the UI shows, not the internal id, and is resolved against the run's project server-side. A number that matches nothing links nothing and is not an error. All three are merged, so a test can cover several cases; precedence only decides which one is primary. Links recorded in the app for this project are merged in as well, and become primary when the body carries none of the three. A `[tag: NAME]` marker anywhere in titlePath adds NAME to the test's tags, alongside anything sent in `tags`.
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. |
testId | string | yes | Client-generated UUID identifying this test attempt group. |
titlePath | array | yes | Path from file/module to test title, e.g. ['auth.spec.ts', 'Login', 'valid credentials'] - the last element is the display title, earlier elements group the test into suites, and a file-like first element becomes the FILE badge (a trailing [chromium]/[firefox]/[webkit] segment renders a browser badge). Reused unchanged across retries. See the guide's "Naming tests: titlePath" section for full semantics and per-ecosystem examples. |
filePath | string | no | Optional source file path, shown alongside the test. Independent of titlePath; not required for the FILE badge, which is derived from titlePath[0]. |
retry | number | no | Zero-based retry attempt index. |
startedAt | string | no | ISO 8601 timestamp. |
tags | array | no | Labels for this test, merged with any [tag: NAME] markers found in titlePath. Case is preserved; duplicates are dropped case-insensitively. Accepted but not schema-enforced at runtime. |
caseId | integer | no | Internal id of the primary test case this test covers. NOT the TC-N number shown in the UI. Takes precedence over every other linking channel. Accepted but not schema-enforced at runtime. |
caseIds | array | no | Internal ids of every test case this test covers, when it covers more than one. Same id space as caseId. Accepted but not schema-enforced at runtime. |
annotations | object | no | Free-form key-value metadata for this attempt. Two keys are read: annotations.caseId and annotations.caseIds carry internal case ids, coerced from strings and dropped unless they are positive integers. Accepted but not schema-enforced at runtime. |
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"testId": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"titlePath": [
"auth.spec.ts",
"Login",
"valid credentials"
],
"filePath": "tests/auth.spec.ts",
"retry": 0,
"startedAt": "2026-07-17T21:00:01.000Z",
"tags": [
"smoke"
],
"caseIds": [
42
]
}Responses: 200 (Success), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 500 (Unexpected server error)
POST /api/ingest/v2/test/complete
Complete a test attempt - Final status, error, timings, any steps not already streamed, and any attachments sent inline. Linking a test to a test case, highest precedence first: 1. `caseId` / `caseIds` in the body - internal case ids, used as sent. 2. `annotations.caseId` / `annotations.caseIds` - the same internal ids, for runners whose annotation API is the only place a test can carry metadata. 3. A `[TC-N]` marker anywhere in titlePath - N is the per-project case NUMBER the UI shows, not the internal id, and is resolved against the run's project server-side. A number that matches nothing links nothing and is not an error. All three are merged, so a test can cover several cases; precedence only decides which one is primary. Links recorded in the app for this project are merged in as well, and become primary when the body carries none of the three. A `[tag: NAME]` marker anywhere in titlePath adds NAME to the test's tags, alongside anything sent in `tags`.
| Field | Type | Required | Description |
|---|---|---|---|
testId | string | yes | Same client-generated UUID sent to test/start for this attempt. |
runId | string | yes | Run UUID returned by run/start. |
titlePath | array | no | Path from file/module to test title, e.g. ['auth.spec.ts', 'Login', 'valid credentials'] - the last element is the display title, earlier elements group the test into suites, and a file-like first element becomes the FILE badge (a trailing [chromium]/[firefox]/[webkit] segment renders a browser badge). Reused unchanged across retries. See the guide's "Naming tests: titlePath" section for full semantics and per-ecosystem examples. |
filePath | string | no | Optional source file path, shown alongside the test. Independent of titlePath; not required for the FILE badge, which is derived from titlePath[0]. |
annotations | object | no | Free-form key-value metadata for this attempt. Two keys are read: annotations.caseId and annotations.caseIds carry internal case ids, coerced from strings and dropped unless they are positive integers. Accepted but not schema-enforced at runtime. On this endpoint annotations.tags is also read, as a fallback when tags is absent. |
retry | number | no | Zero-based retry attempt index; must match the value sent to test/start. |
startedAt | string | no | ISO 8601 timestamp. |
endedAt | string | no | ISO 8601 timestamp. |
status | passed | failed | skipped | timedOut | timedout | interrupted | yes | Final outcome of this attempt. |
duration | number | no | Milliseconds. Computed from startedAt/endedAt if omitted. |
error | object | no | Failure details (message, stack, etc). Required in practice when status is failed/timedOut. |
steps | array of EmbeddedStep | no | Steps not already sent via steps/stream, in EmbeddedStep shape. |
attachments | array of EmbeddedAttachment | no | Inline base64 attachments in EmbeddedAttachment shape; empty when files were uploaded separately via attachment/upload, attachment/stream, or presign+confirm. |
tags | array | no | Labels for this test, merged with any [tag: NAME] markers found in titlePath. Case is preserved; duplicates are dropped case-insensitively. Accepted but not schema-enforced at runtime. |
caseId | integer | no | Internal id of the primary test case this test covers. NOT the TC-N number shown in the UI. Takes precedence over every other linking channel. Accepted but not schema-enforced at runtime. |
caseIds | array | no | Internal ids of every test case this test covers, when it covers more than one. Same id space as caseId. Accepted but not schema-enforced at runtime. |
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"testId": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"titlePath": [
"auth.spec.ts",
"Login",
"valid credentials"
],
"status": "passed",
"retry": 0,
"duration": 4210,
"startedAt": "2026-07-17T21:00:01.000Z",
"endedAt": "2026-07-17T21:00:05.210Z",
"steps": [],
"attachments": []
}Responses: 200 (Success), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 500 (Unexpected server error)
POST /api/ingest/v2/steps/stream
Stream live step events (batched) - Batch of step events, possibly spanning multiple tests and runs. Every unique runId in the batch is ownership-checked.
| Field | Type | Required | Description |
|---|---|---|---|
items | array of StepItem | yes | Batch of StepItem events, up to 1000 per call. May span multiple tests and runs. |
{
"items": [
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"testId": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"title": "page.click(#login)",
"action": "end",
"status": "passed",
"duration": 120,
"nestingLevel": 0,
"stepIndex": 3
}
]
}Responses: 200 (Success), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 500 (Unexpected server error)
POST /api/ingest/v2/run/end
End a run
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. |
status | passed | failed | timedOut | timedout | interrupted | finished | completed | no | Final outcome of the run as a whole. |
endedAt | string | no | ISO 8601 timestamp. |
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"status": "finished",
"endedAt": "2026-07-17T21:05:00.000Z"
}Responses: 200 (Success), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 500 (Unexpected server error)
POST /api/ingest/v2/attachment/upload
Upload attachments (buffered multipart) - For small files. Multipart form with runId, testId fields and up to 10 file parts.
runId, testId plus up to 10 file parts (200 MB each).| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. |
testId | string | yes | Client-generated test UUID. |
file | string | yes | One or more file parts. Max 10 files, 200MB each. |
Responses: 200 (Success), 400 (runId and testId are required, or no files provided), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 413 (Per-run attachment byte quota exceeded), 500 (Unexpected server error)
POST /api/ingest/v2/attachment/stream
Upload large attachments (streaming multipart) - For large files (videos, traces). Streams to storage without buffering. Send runId and testId fields BEFORE file parts.
runId, testId plus up to 10 file parts (200 MB each).| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. |
testId | string | yes | Client-generated test UUID. |
file | string | yes | One or more file parts. Max 10 files, 200MB each. |
Responses: 200 (Success), 400 (runId and testId are required, or no files uploaded), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 413 (Per-run attachment byte quota exceeded), 500 (Unexpected server error)
POST /api/ingest/v2/attachment/presign
Get a presigned direct-upload URL - Returns a PUT URL that uploads straight to storage, so the bytes never pass through this service. Use it for files at or above the directUploadMinBytes reported by run/start; below that, attachment/upload is one request instead of three. The URL binds the exact Content-Length, Content-Type and filename you declare here. Send precisely those values on the PUT: any other length, or a chunked request with no Content-Length, is refused by storage and nothing is stored. Then call attachment/confirm. Nothing is recorded until you do, and the quota is charged there, against the size storage actually reports.
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. |
testId | string | yes | Client-generated test UUID. |
filename | string | yes | Name of the file. Stored as object metadata and used when the file is downloaded later. |
contentType | string | yes | MIME type of the file. Signed into the URL, so the PUT must send the same value. |
size | integer | yes | Exact file size in bytes. Required, and signed into the URL as the Content-Length the PUT must send. |
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"testId": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"filename": "trace.zip",
"contentType": "application/zip",
"size": 1048576
}Responses: 200 (Success), 400 (Missing a required field, a size that is not a positive integer, or a file above the 200MB per-file limit (code ATTACHMENT_TOO_LARGE)), 401 (Missing or invalid API key), 403 (Run belongs to another organization (code RUN_ACCESS_DENIED), or the subscription is paused or canceled and every write is blocked (code SUBSCRIPTION_INACTIVE, sent for any authenticated endpoint)), 409 (Run is no longer active (stopped or interrupted)), 500 (Unexpected server error), 501 (This deployment cannot issue presigned URLs; storage is not reachable from outside the server network. Use attachment/stream.), 503 (Could not record the upload authorization; retry or use attachment/stream)
POST /api/ingest/v2/attachment/confirm
Confirm a direct upload - Records the attachment after a successful presigned PUT, and charges the run quota against the size storage reports rather than the size declared at presign. Safe to retry: repeating a confirm that already succeeded returns 200 and changes nothing. If the response is lost, retry this call. Do NOT fall back to attachment/stream once the PUT succeeded, or the file is stored and charged twice. Does not require the run to still be active, because it finalises something presign authorised while the run was running.
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID returned by run/start. Must match the run the attachmentId was issued for. |
attachmentId | string | yes | The attachmentId returned by attachment/presign. Only ids this server issued are accepted. |
filename | string | yes | Name of the uploaded file. |
contentType | string | no | MIME type of the uploaded file. Defaults to the value given at presign. |
{
"runId": "3f0f1e6a-7b1c-4b6e-9a52-1c2d3e4f5a6b",
"attachmentId": "7c8d9e0f-1a2b-4c3d-8e5f-6a7b8c9d0e1f",
"filename": "trace.zip",
"contentType": "application/zip"
}Responses: 200 (Success), 400 (Missing one of runId, attachmentId, filename, or the run no longer exists (code RUN_NOT_FOUND). Neither is retryable.), 401 (Missing or invalid API key), 409 (The attachmentId was never issued for this run or has expired (code PRESIGN_BINDING_UNKNOWN), or no object was uploaded against it (code UPLOAD_NOT_FOUND). Neither is retryable; upload again via attachment/stream.), 413 (Per-run attachment byte quota exceeded. The object has already been deleted; do not retry and do not fall back.), 500 (Unexpected server error), 503 (Storage could not be reached to verify the upload. The object is intact; retry this call.)
Component schemas
Named schemas referenced above by name (e.g. an array of EmbeddedStep field in a table) are defined here.
ErrorEnvelope
| Field | Type | Required | Description |
|---|---|---|---|
error | string | no | Short error category, e.g. Unauthorized, Forbidden, Conflict. |
message | string | no | Human-readable detail; not machine-parseable, use code instead. |
code | API_KEY_REQUIRED | INVALID_API_KEY_FORMAT | INVALID_API_KEY | SUBSCRIPTION_INACTIVE | RUN_ACCESS_DENIED | RUN_NOT_ACTIVE | RUN_NOT_FOUND | ATTACHMENT_TOO_LARGE | RUN_ATTACHMENT_LIMIT_EXCEEDED | DIRECT_UPLOAD_UNAVAILABLE | PRESIGN_BINDING_UNKNOWN | UPLOAD_NOT_FOUND | PRESIGN_UNAVAILABLE | STORAGE_UNAVAILABLE | no | Machine-readable error code. Retrying changes the answer for PRESIGN_UNAVAILABLE and STORAGE_UNAVAILABLE only; every other code below answers the same way however many times it is sent, so a client must stop rather than back off. - API_KEY_REQUIRED: 401, permanent. No X-API-Key header was sent. - INVALID_API_KEY_FORMAT: 401, permanent. The key is not in the m00n_... format. - INVALID_API_KEY: 401, permanent. The key is unknown or has been revoked, or its organization is not active. - SUBSCRIPTION_INACTIVE: 403, permanent. The organization subscription is paused or canceled and every write is blocked until billing is settled. - RUN_ACCESS_DENIED: 403, permanent. The run belongs to another organization. - RUN_NOT_ACTIVE: 409, permanent. The run has already stopped, so nothing further can be added to it. - RUN_NOT_FOUND: 400, permanent. From attachment/confirm: the run no longer exists. - ATTACHMENT_TOO_LARGE: 400, permanent. The declared size is above the 200MB per-file limit. - RUN_ATTACHMENT_LIMIT_EXCEEDED: 413, permanent for the rest of the run. The per-run attachment byte quota is used up. On attachment/confirm the object has already been deleted: do not retry and do not fall back. - DIRECT_UPLOAD_UNAVAILABLE: 501, permanent for this deployment. Presigned URLs are not offered here; use attachment/upload or attachment/stream instead. - PRESIGN_BINDING_UNKNOWN: 409, permanent. The attachmentId was never issued for this run, or it has expired. Upload again via attachment/stream. - UPLOAD_NOT_FOUND: 409, permanent. No object was uploaded against that attachmentId. Upload again via attachment/stream. - PRESIGN_UNAVAILABLE: 503, RETRYABLE. The upload authorization could not be recorded; retry, or use attachment/stream. - STORAGE_UNAVAILABLE: 503, RETRYABLE. Storage could not be reached to verify the upload. The object is intact, so retry the same confirm. |
RunQuota
Per-run attachment byte quota, by pricing tier.
| Field | Type | Required | Description |
|---|---|---|---|
usedBytes | number | no | Bytes already consumed by this run. |
limitBytes | number | no | Total bytes allowed for this run. |
remainingBytes | number | no | limitBytes minus usedBytes. |
AttachmentResult
One stored attachment, as returned by attachment/upload and attachment/stream.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | no | Attachment UUID. |
name | string | no | Original file name. |
contentType | string | no | MIME type, as provided by the upload. |
size | number | no | File size in bytes. |
StepItem
One step event. Items without runId, testId and title are silently skipped by the server.
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Run UUID this step belongs to; ownership-checked per batch. |
testId | string | yes | Client-generated test UUID. |
title | string | yes | Step label shown as the row title. |
action | string | no | 'begin' while running, 'end' when finished. Only 'end' (or a final status) is persisted. |
status | string | no | 'running' while in progress, then 'passed' or 'failed'. |
category | string | no | Optional grouping tag, e.g. 'pw:api' for framework-native steps. |
duration | number | no | Milliseconds. |
error | object | no | Failure details for this step, if any. |
nestingLevel | number | no | Zero-based depth for nested steps. |
stepIndex | number | no | Stable per-test index; used to dedupe against test/complete steps. |
EmbeddedStep
One step embedded in test/complete's steps array (distinct from steps/stream's StepItem: no runId/testId per item, since the parent call already carries both for the whole array). No field is required - the server accepts partial items and falls back to array position for ordering when index/stepIndex is omitted.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | no | Step label shown as the row title. |
category | string | no | Optional grouping tag, e.g. 'pw:api' for framework-native steps. |
status | string | no | 'passed', 'failed', 'skipped' or similar. |
duration | number | no | Milliseconds. |
error | object | no | Failure details for this step, if any. |
nestingLevel | number | no | Zero-based depth for nested steps. |
index | number | no | Zero-based step order within this test attempt. Preferred over stepIndex; falls back to array position if both are omitted. |
stepIndex | number | no | Accepted as an alias for 'index' if 'index' is not present. |
EmbeddedAttachment
One inline base64-encoded attachment embedded in test/complete's attachments array (distinct from AttachmentResult, the shape the upload endpoints return). This is the fallback route for a file: base64 costs 33% in size and the whole file travels in the test/complete body, so prefer attachment/upload, attachment/stream or presign+confirm and leave this array empty. Items without data or name are silently skipped by the server.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | no | Client-generated attachment UUID. Generated server-side if omitted. |
name | string | yes | File name shown on the attachment. |
contentType | string | no | Defaults to 'application/octet-stream' if omitted. |
data | string | yes | Base64-encoded file content. |
compressed | boolean | no | If true, data is gzip-compressed before base64 encoding and is decompressed server-side. |
size | number | no | Informational only: the server recomputes size from the decoded buffer and ignores this value. |