Build your own integration
Every M00N Report reporter speaks the same small HTTP API: the ingest v2 protocol. If we do not ship an official reporter for your test runner yet, you can build one in an afternoon - in any language that can send HTTP requests.
The contract
The full OpenAPI 3.0 specification is public: /api/ingest/v2/openapi.json. A browsable version lives on the API reference page.
Any server implementing this contract must also serve two liveness probes - GET /healthz and GET /api/ingest/health - both requiring no authentication and returning { ok: true }. The official reporters probe /api/ingest/health then /healthz and treat the result as advisory - it words the failure message, it does not stop reporting. Answer both with a JSON body carrying ok: true; a bare 200 is what an edge returns for an unrouted path. Serve both: /healthz is what direct ingest URLs, self-hosted deployments and local development expose, while /api/ingest/health is the path that survives an edge or proxy where only specific path prefixes (such as /api/*) reach the backend - which is the case on the m00nreport.com cloud today, where /healthz is not among the reachable prefixes and hits the frontend instead of ingest.
# Cloud-routable path (works against https://m00nreport.com today):
curl https://m00nreport.com/api/ingest/health
# -> { "ok": true, "service": "ingest" }
# Self-hosted / local / direct ingest URLs also serve the same check at /healthz:
curl http://localhost:4001/healthz
# -> { "ok": true, "service": "ingest" }X-API-Key header. The project API key (format m00n_...) identifies both your organization and project - never send a project id.Protocol walkthrough
One rule to remember: the server generates the runId (returned by run/start), your reporter generates each testId (UUID v4) so steps can stream before the test finishes. On a retry, reuse the titlePath, generate a new testId, and increment retry (0-based).
# 1. Start a run (server returns the runId)
curl -X POST https://m00nreport.com/api/ingest/v2/run/start \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"launch": "My first integration", "startedAt": "2026-07-17T21:00:00Z"}'
# -> { "ok": true, "runId": "3f0f1e6a-..." }
# 2. Register a test attempt (client generates the testId UUID, fire-and-forget)
curl -X POST https://m00nreport.com/api/ingest/v2/test/start \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"runId": "3f0f1e6a-...", "testId": "9a1b2c3d-...", "titlePath": ["suite", "my test"], "retry": 0}'
# 3. (Optional, recommended) Stream live steps in batches of up to 1000
curl -X POST https://m00nreport.com/api/ingest/v2/steps/stream \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"items": [{"runId": "3f0f1e6a-...", "testId": "9a1b2c3d-...", "title": "click login", "action": "end", "status": "passed", "stepIndex": 0}]}'
# 4. Complete the test
curl -X POST https://m00nreport.com/api/ingest/v2/test/complete \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"runId": "3f0f1e6a-...", "testId": "9a1b2c3d-...", "titlePath": ["suite", "my test"], "status": "passed", "retry": 0}'
# 5. End the run (always last, exactly once)
curl -X POST https://m00nreport.com/api/ingest/v2/run/end \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"runId": "3f0f1e6a-...", "status": "finished"}'Naming tests: titlePath
titlePath is the single field the dashboard uses to derive a test's title, its suite grouping, its FILE badge, and its browser badge. The shape is: [file-or-module, ...suite/describe/class names, test title].
| Part of titlePath | Rendered as |
|---|---|
| Last element | The test's title (row label) - always. |
| Element(s) before the title | Suite grouping - the element immediately before the title is shown as the test's closest enclosing suite in the dashboard's suite tree and filters. |
| First element | FILE badge - only when it looks like a file (has a path separator like / or \, or a trailing extension like .spec.ts). A bare namespace or module segment (e.g. NUnit's root namespace) does not trigger it. |
Trailing [chromium] / [firefox] / [webkit] on the title | Browser badge - a suffix on the title string itself, not a separate array element. |
On a retry, reuse the SAME titlePath, send a NEW testId, and increment retry by 1 per attempt (0-based) - the dashboard groups every attempt sharing a titlePath into one flaky-test history.
Per-ecosystem examples
These are the exact shapes M00N Report's own reporters and rendering logic recognize:
| Ecosystem | Example titlePath |
|---|---|
Playwright JS (raw test.titlePath()) | ['', 'chromium', 'auth.spec.ts', 'Login', 'valid credentials'] |
| pytest | ['tests/test_login.py', 'test_valid_credentials[chromium]'] |
| NUnit | ['MyApp', 'Tests', 'LoginTests', 'ValidCredentials'] |
Do / don't
// DO: file/module first, then suite names, then the test title last
titlePath: ['tests/checkout.spec.ts', 'Checkout', 'pays with card']
// -> FILE badge: tests/checkout.spec.ts | suite: Checkout | title: pays with card
// DON'T: a single element loses suite AND file grouping
titlePath: ['pays with card']
// -> no FILE badge, no suite - the test lands in "Other Tests"
// DON'T: put the run/launch name inside titlePath
titlePath: ['Nightly Regression', 'pays with card']
// -> "Nightly Regression" is read as the suite name, not the run title;
// set the run name via run/start's `launch` field insteadAttachments
# Small files: buffered multipart
curl -X POST https://m00nreport.com/api/ingest/v2/attachment/upload \
-H "X-API-Key: $M00N_API_KEY" \
-F "runId=3f0f1e6a-..." -F "testId=9a1b2c3d-..." -F "file=@screenshot.png"
# Large files: ask for a URL, upload straight to storage, then confirm.
# The URL is signed over the size, type and name you declare here, so
# the PUT must send exactly those. A chunked request is refused.
curl -X POST https://m00nreport.com/api/ingest/v2/attachment/presign \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"runId": "3f0f1e6a-...", "testId": "9a1b2c3d-...", "filename": "trace.zip",
"contentType": "application/zip", "size": 41943040}'
# -> {"url": "https://...", "attachmentId": "7c8d9e0f-...", expiresIn: 600}
curl -X PUT "$PRESIGNED_URL" \
-H "Content-Length: 41943040" -H "Content-Type: application/zip" \
--data-binary @trace.zip # no API key: the signature carries it
curl -X POST https://m00nreport.com/api/ingest/v2/attachment/confirm \
-H "X-API-Key: $M00N_API_KEY" -H "Content-Type: application/json" \
-d '{"runId": "3f0f1e6a-...", "attachmentId": "7c8d9e0f-...",
"filename": "trace.zip", "contentType": "application/zip"}'
# /attachment/stream takes the same shape as upload, and is the fallback
# when direct upload is unavailable or the PUT fails.Use attachment/upload for small files: one request instead of three. For large ones the attachment/presign + PUT + attachment/confirmtrio sends the bytes straight to storage, which is what the official Playwright reporter does above 10MB. run/start reports whether this deployment supports it (directUpload) and from what size (directUploadMinBytes); when it does not, or when the PUT fails, fall back to attachment/stream.
Two details decide whether your client works. The presigned URL is signed over the exact size, content type and filename you declared, so the PUT has to send precisely those and must carry no API key. And once the PUT has succeeded the object exists: retry confirm if its response is lost, but never fall back to attachment/stream at that point, or the file is stored and charged twice.
The iron rule and limits
| Limit | Value |
|---|---|
| Steps per test/complete | 5000 |
| Attachments per test/complete | 100 |
| Items per steps/stream batch | 1000 |
| File size (multipart) | 200 MB |
| Files per multipart request | 10 |
| Attachment bytes per run | by pricing tier (413 RUN_ATTACHMENT_LIMIT_EXCEEDED) |
Prove it with the conformance suite
A conformance suite lives in the M00N Report repository today (reporter-conformance/). It runs a recording mock server, drives your reporter against it, and asserts the traffic matches the contract: every payload validates against the OpenAPI schemas, the lifecycle ordering rules hold (run/start before any test, run/end exactly once and last), and fault injection confirms your retry logic never lets a transient 5xx fail the test run. It will be published as an installable npm package soon; until then, the OpenAPI document above is the authoritative, always-current contract.
# Planned package interface (not yet published to npm):
npx m00nreport-conformance run --spec ingest-v2.json -- <your test command>
# Your reporter reads M00N_SERVER_URL and M00N_API_KEY from the environment.
# Add --faults flaky500 to verify retry-with-backoff and the iron rule:
# a reporter must NEVER fail the test run.Build a reporter with AI
A complete, self-contained prompt for building a M00N Report integration for any language or test runner with an AI coding assistant. It covers the contract, the lifecycle and payload fields for all 6 core endpoints, testId/runId rules, retry semantics, titlePath naming rules, step streaming, attachments, the resilience requirements as acceptance criteria, and the 15 conformance invariants as the definition of done. Paste it into your AI assistant and answer its setup questions.