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
}
measuredandthresholdare structured objects, never prose.messageis derived from them — read the objects, not the string.locationspinpoints offenders (mesh / primitive / material / node / image index and name), capped at 20 entries withlocations_truncated: truewhen there are more.- An
info-severity check can only bepass,skipped, orerror— it never warns or fails. skippedresults carry askip_reason.
Verdict rollup
- Any
error-severity check with statusfailgives verdictfail. - Otherwise any
warngives verdictwarn. - 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.