Report schema

The versioned JSON contract your code builds against, field by field.

The report is the contract. It is versioned and additive-stable: new fields and new checks bump the minor version; removing or renaming anything bumps major. Pin against the major version and treat the checks array as open-ended — key off the IDs you know and ignore the rest.

The canonical JSON Schema is published at /v1/schema/report.json and is emitted from the Rust core, so it never drifts from what the API returns.

Top-level shape

{
  "schema_version": "1.1",
  "report_id": "rpt_01J9XK...",
  "created_at": "2026-07-13T18:04:11Z",
  "expires_at": "2026-08-12T18:04:11Z",
  "input": { "filename": "barrel.glb", "bytes": 4183920, "sha256": "9f2c...", "format": "glb", "gltf_version": "2.0" },
  "profile": { "name": "web", "overrides": { "max_tris": 15000 }, "resolved": { "max_tris": 15000 } },
  "verdict": "fail",
  "summary": { "errors": 2, "warnings": 3, "info": 6, "skipped": 1 },
  "stats": { "triangles": 48231, "vertices": 30122, "materials": 2, "textures": 4, "...": "..." },
  "checks": [ /* CheckResult[], fixed ID order */ ],
  "screenshots": [ /* Screenshot[], empty if checks_only */ ],
  "timing_ms": { "total": 1840, "parse": 120, "checks": 610, "render": 1050 },
  "meshcheck_version": "0.3.1"
}

stats collects the measured totals — triangles, vertices, meshes, primitives, materials, textures, texture_memory_bytes, draw_call_estimate, nodes, the AABB and extent, animations, and skins.

CheckResult

{
  "id": "PERF-001",
  "name": "Triangle count",
  "category": "performance",
  "severity": "error",
  "status": "fail",
  "measured": { "triangles": 48231 },
  "threshold": { "max_tris": 15000 },
  "message": "48,231 triangles exceeds profile budget of 15,000",
  "locations": [
    { "kind": "mesh", "index": 1, "name": "Barrel_Body", "detail": { "triangles": 41200 } }
  ],
  "deterministic": true
}
  • measured and threshold are structured objects, never prose. message is derived from them — read the objects, not the string.
  • locations pinpoints offenders (mesh / primitive / material / node / image index and name), capped at 20 entries with locations_truncated: true when there are more.
  • An info-severity check can only be pass, skipped, or error — it never warns or fails.
  • skipped results carry a skip_reason.

Verdict rollup

  • Any error-severity check with status fail gives verdict fail.
  • Otherwise any warn gives verdict warn.
  • Otherwise pass.

If any check reports status error (the check crashed), the report still ships and adds "verdict_confidence": "partial". There is no numeric score anywhere.

Screenshot

{
  "id": "shot_front",
  "angle": { "rv": 0, "rh": 0 },
  "kind": "still",
  "width": 1024,
  "height": 1024,
  "url": "https://meshcheck.dev/s/rpt_01J9XK/front.png?sig=...",
  "expires_at": "2026-08-12T18:04:11Z",
  "backend": "cpu-swiftshader",
  "render_hash": "sha256 of the pixel buffer"
}

URLs are signed and expire with the report. render_hash is the SHA-256 of the pixel buffer, which makes determinism auditable and lets you cache or diff renders across regenerations. The standard still set is front (0,0), back (0,180), left (0,90), right (0,-90), top (-89,0), and three-quarter (30,45).

Async job envelope

{ "job_id": "job_...", "status": "queued|processing|done|failed", "poll": "/v1/jobs/job_...", "result": null }

result carries the full report when status is done; a failed job carries a structured error. Jobs and their results follow the same 30-day retention as reports.

Error shape

Every endpoint returns errors in one shape, with a stable code:

{ "error": { "code": "FILE_TOO_LARGE", "message": "Max sync size is 20MB; use url upload for async.", "detail": { "bytes": 31457280, "max": 20971520 } } }

The full list of codes is in the API reference.