M00N ReportIngest API Reference

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.

FieldTypeRequiredDescription
launchstringyesLaunch (run) title shown in the UI.
tagsarraynoLabels shown on the launch, usable as dashboard filters.
totalnumbernoExpected total number of tests, if known upfront.
startedAtstringnoISO 8601 timestamp.
attributesobjectnoOptional 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`.

FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start.
testIdstringyesClient-generated UUID identifying this test attempt group.
titlePatharrayyesPath 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.
filePathstringnoOptional source file path, shown alongside the test. Independent of titlePath; not required for the FILE badge, which is derived from titlePath[0].
retrynumbernoZero-based retry attempt index.
startedAtstringnoISO 8601 timestamp.
tagsarraynoLabels 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.
caseIdintegernoInternal 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.
caseIdsarraynoInternal 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.
annotationsobjectnoFree-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`.

FieldTypeRequiredDescription
testIdstringyesSame client-generated UUID sent to test/start for this attempt.
runIdstringyesRun UUID returned by run/start.
titlePatharraynoPath 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.
filePathstringnoOptional source file path, shown alongside the test. Independent of titlePath; not required for the FILE badge, which is derived from titlePath[0].
annotationsobjectnoFree-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.
retrynumbernoZero-based retry attempt index; must match the value sent to test/start.
startedAtstringnoISO 8601 timestamp.
endedAtstringnoISO 8601 timestamp.
statuspassed | failed | skipped | timedOut | timedout | interruptedyesFinal outcome of this attempt.
durationnumbernoMilliseconds. Computed from startedAt/endedAt if omitted.
errorobjectnoFailure details (message, stack, etc). Required in practice when status is failed/timedOut.
stepsarray of EmbeddedStepnoSteps not already sent via steps/stream, in EmbeddedStep shape.
attachmentsarray of EmbeddedAttachmentnoInline base64 attachments in EmbeddedAttachment shape; empty when files were uploaded separately via attachment/upload, attachment/stream, or presign+confirm.
tagsarraynoLabels 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.
caseIdintegernoInternal 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.
caseIdsarraynoInternal 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.

FieldTypeRequiredDescription
itemsarray of StepItemyesBatch 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

FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start.
statuspassed | failed | timedOut | timedout | interrupted | finished | completednoFinal outcome of the run as a whole.
endedAtstringnoISO 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.

multipart/form-data request: fields runId, testId plus up to 10 file parts (200 MB each).
FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start.
testIdstringyesClient-generated test UUID.
filestringyesOne 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.

multipart/form-data request: fields runId, testId plus up to 10 file parts (200 MB each).
FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start.
testIdstringyesClient-generated test UUID.
filestringyesOne 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.

FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start.
testIdstringyesClient-generated test UUID.
filenamestringyesName of the file. Stored as object metadata and used when the file is downloaded later.
contentTypestringyesMIME type of the file. Signed into the URL, so the PUT must send the same value.
sizeintegeryesExact 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.

FieldTypeRequiredDescription
runIdstringyesRun UUID returned by run/start. Must match the run the attachmentId was issued for.
attachmentIdstringyesThe attachmentId returned by attachment/presign. Only ids this server issued are accepted.
filenamestringyesName of the uploaded file.
contentTypestringnoMIME 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

FieldTypeRequiredDescription
errorstringnoShort error category, e.g. Unauthorized, Forbidden, Conflict.
messagestringnoHuman-readable detail; not machine-parseable, use code instead.
codeAPI_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_UNAVAILABLEnoMachine-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.

FieldTypeRequiredDescription
usedBytesnumbernoBytes already consumed by this run.
limitBytesnumbernoTotal bytes allowed for this run.
remainingBytesnumbernolimitBytes minus usedBytes.

AttachmentResult

One stored attachment, as returned by attachment/upload and attachment/stream.

FieldTypeRequiredDescription
idstringnoAttachment UUID.
namestringnoOriginal file name.
contentTypestringnoMIME type, as provided by the upload.
sizenumbernoFile size in bytes.

StepItem

One step event. Items without runId, testId and title are silently skipped by the server.

FieldTypeRequiredDescription
runIdstringyesRun UUID this step belongs to; ownership-checked per batch.
testIdstringyesClient-generated test UUID.
titlestringyesStep label shown as the row title.
actionstringno'begin' while running, 'end' when finished. Only 'end' (or a final status) is persisted.
statusstringno'running' while in progress, then 'passed' or 'failed'.
categorystringnoOptional grouping tag, e.g. 'pw:api' for framework-native steps.
durationnumbernoMilliseconds.
errorobjectnoFailure details for this step, if any.
nestingLevelnumbernoZero-based depth for nested steps.
stepIndexnumbernoStable 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.

FieldTypeRequiredDescription
titlestringnoStep label shown as the row title.
categorystringnoOptional grouping tag, e.g. 'pw:api' for framework-native steps.
statusstringno'passed', 'failed', 'skipped' or similar.
durationnumbernoMilliseconds.
errorobjectnoFailure details for this step, if any.
nestingLevelnumbernoZero-based depth for nested steps.
indexnumbernoZero-based step order within this test attempt. Preferred over stepIndex; falls back to array position if both are omitted.
stepIndexnumbernoAccepted 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.

FieldTypeRequiredDescription
idstringnoClient-generated attachment UUID. Generated server-side if omitted.
namestringyesFile name shown on the attachment.
contentTypestringnoDefaults to 'application/octet-stream' if omitted.
datastringyesBase64-encoded file content.
compressedbooleannoIf true, data is gzip-compressed before base64 encoding and is decompressed server-side.
sizenumbernoInformational only: the server recomputes size from the decoded buffer and ignores this value.
Last updated