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-data with a file field — a .glb, or a .zip bundle for .gltf plus 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/uploads returns { blob_id, upload_url }; PUT the bytes to the presigned URL, then validate by blob_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 for full.
  • Larger models (up to 250 MB) run async — a 202 with a job envelope you poll at GET /v1/jobs/{id}. Over 250 MB returns 413 FILE_TOO_LARGE.
  • checks_only skips the renderer; the RND-* checks report skipped.
  • overrides replaces 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.txt is 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.