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 may probe before streaming and treat a failed or non-200 response as "server unreachable" for the whole run. 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 (videos, traces): streaming multipart via /attachment/stream,
# or direct-to-storage via /attachment/presign + PUT + /attachment/confirm.The attachment/presign + PUT + attachment/confirm trio is an optional direct-to-storage upload path; the official reporters use attachment/upload (<=10MB) and attachment/stream (>10MB) instead and never need it.
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 12 conformance invariants as the definition of done. Paste it into your AI assistant and answer its setup questions.