API reference
Endpoints, request options, credit costs, and the enumerated error codes.
Base URL: https://api.meshcheck.dev/v1. The API is path-versioned; breaking changes ship under /v2
with at least six months of /v1 overlap.
Input shapes
Every analysis endpoint accepts a model three ways:
multipart/form-datawith afilefield — a.glb, or a.zipbundle for.gltfplus external resources. Accepted up to the platform request-body cap (~4.5 MB).application/json{ "url": "https://..." }— the server fetches it (25 MB cap, 10 s timeout, http(s) only, no redirects into private ranges).application/json{ "blob_id": "..." }— for files above the multipart cap.POST /v1/uploadsreturns{ blob_id, upload_url };PUTthe bytes to the presigned URL, then validate byblob_id.
Upload blobs are deleted after processing on every path.
POST /v1/validate
The core product. Options (JSON fields or multipart fields):
{
"profile": "web",
"overrides": { "max_tris": 15000 },
"mode": "full | checks_only",
"screenshots": true,
"webhook_url": "https://... (async only, optional)"
}
- Models ≤ 20 MB validate synchronously and return the report directly. Target p95: under 3 s for
checks_only, under 8 s forfull. - Larger models (up to 250 MB) run async — a
202with a job envelope you poll atGET /v1/jobs/{id}. Over 250 MB returns413 FILE_TOO_LARGE. checks_onlyskips the renderer; theRND-*checks reportskipped.overridesreplaces individual profile parameters. Profile defaults are opinions; overrides are the escape hatch.
POST /v1/render
Same input shapes. Renders stills or a turntable with the fixed studio rig:
{
"angles": [{ "rv": 0, "rh": 0 }],
"size": 1024,
"turntable": { "frames": 36, "format": "gif", "size": 512 },
"background": "transparent | #RRGGBB"
}
Maximum 12 stills per call. Turntables are always async and gif only in v1 (mp4 returns
400 BAD_REQUEST). The response is the report envelope with a screenshots array and stats, no
checks.
POST /v1/inspect
The one non-deterministic endpoint. Renders standard angles internally, asks a vision model, and returns a schema-constrained answer. One of:
{ "check": "VIS-001", "prompt": "rusty metal barrel, low poly" }
{ "check": "VIS-002" }
{ "check": "VIS-003", "question": "Is the lid a separate mesh?" }
Every answer carries "deterministic": false and the model version. Confidence is categorical
(low | medium | high), never a number.
Other endpoints
GET /v1/jobs/{id}— poll an async job. A webhook fires once on completion if you provided one.GET /v1/reports/{id}— re-fetch any report within its 30-day retention. Free.GET /v1/account— your plan, credit balance, and usage this period.GET /v1/openapi.json,GET /v1/schema/{name}.json,GET /llms.txt— public, no auth.llms.txtis the one-fetch agent orientation.
Credits
| Action | Credits |
|---|---|
validate (checks_only) |
1 |
validate (full — renderer runs) |
2 |
| render (up to 6 stills) | 2 |
| render (turntable) | 4 |
| inspect | 8 |
| report re-fetch, account, schemas | 0 |
Insufficient balance returns 402 INSUFFICIENT_CREDITS with the balance and the amount needed. The
renderer is charged whether or not screenshots are stored.
Error codes
Errors share one shape across every endpoint:
{ "error": { "code": "FILE_TOO_LARGE", "message": "...", "detail": { "bytes": 31457280, "max": 20971520 } } }
The code strings are stable and enumerated:
MISSING_API_KEY, INVALID_API_KEY, INSUFFICIENT_CREDITS, RATE_LIMITED, BAD_REQUEST,
FILE_TOO_LARGE, FETCH_FAILED, UNSUPPORTED_FORMAT, MALFORMED_GLTF, BUNDLE_INCOMPLETE,
NOT_FOUND, JOB_NOT_FOUND, REPORT_EXPIRED, RENDER_FAILED, INSPECT_UNAVAILABLE, INTERNAL.
Client-side errors are detected before credits are charged wherever the input alone determines them.
INTERNAL is never used for a client mistake. Rate-limited responses (429 RATE_LIMITED) carry a
Retry-After header.