Mocai API v1

Capture, programmatically.

The Mocai API turns a video into a 3D take (mesh, skeleton, and exports) with a few HTTP calls. Uploads count toward your plan's monthly footage allowance and appear in your library next to portal uploads. Grab the Postman collection to start in minutes, or skip the HTTP entirely with the Blender add-on.

Authentication

Send your API key as a bearer token on every request. Keys start with mk_live_ and are shown once at creation. Create and revoke them under App → Developers. API access is included on all paid plans; on the Free plan requests return 403 plan_required. Requests are rate-limited per account (Basic 20/min, Pro 60/min; all your keys share the budget); every response carries RateLimit-Limit / -Remaining / -Reset headers, and going over returns 429 rate_limited with Retry-After.

curl {BASE}/api/v1/takes \
  -H "Authorization: Bearer mk_live_…"

Upload a take

Uploads are two steps so video bytes go straight to storage: POST /api/v1/uploads returns a list of parts (byte ranges with their own upload URLs; send them concurrently for roughly double the throughput), you PUT each range, then create the take from the uploadId; the parts are stitched together server-side. Options: hands toggles finger tracking, maxPersons is capped by your plan, an optional selection of normalized first-frame boxes pins exactly which people to reconstruct, and smoothing (off | standard | strong, default standard) applies zero-lag temporal smoothing to the skeleton and mesh exports and portal playback; keypoint JSON/CSV always stay raw for analytics. For clips under ~30 MB you can still POST multipart/form-data with a file field directly to /api/v1/takes in one call.

POST /api/v1/uploads
POST /api/v1/takes
# 1) Create an upload session
curl -X POST {BASE}/api/v1/uploads \
  -H "Authorization: Bearer mk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "contentType": "video/mp4", "sizeBytes": 48211234 }'
# → 201  { "uploadId": "9dK1…", "maxBytes": 2147483648, "parts": [
#           { "url": "https://storage.googleapis.com/…", "offset": 0,        "size": 16070412 },
#           { "url": "https://storage.googleapis.com/…", "offset": 16070412, "size": 16070411 },
#           { "url": "https://storage.googleapis.com/…", "offset": 32140823, "size": 16070411 } ] }

# 2) PUT each part's byte range to its URL, in parallel for ~2× throughput
#    (no auth header; each URL is its own credential; ≤8 MB files get one part)
curl -X PUT --data-binary @part0.bin "https://storage.googleapis.com/…" &
curl -X PUT --data-binary @part1.bin "https://storage.googleapis.com/…" &
curl -X PUT --data-binary @part2.bin "https://storage.googleapis.com/…" &
wait

# 3) Create the take from the finished upload
curl -X POST {BASE}/api/v1/takes \
  -H "Authorization: Bearer mk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "uploadId": "9dK1…", "title": "Golf swing", "hands": true, "maxPersons": 1, "smoothing": "standard" }'

# → 201
{
  "id": "8Fk2…",
  "status": "queued",
  "via": "api",
  "options": { "hands": true, "maxPersons": 1, "smoothing": "standard" },
  "exports": []
}

Status & exports

Status moves through queued → processing → baking → ready. When ready, exports[] lists downloadable assets: FBX, GLB, BVH, USD (skeleton or full mesh animation), and raw keypoints as JSON/CSV. The export endpoint answers with a 302 to a short-lived download URL — follow the redirect (standard clients like curl -L handle this, and correctly drop the Authorization header on the way). Takes older than your plan's retention window become expired (files removed; expiresAt on the resource warns 7 days ahead). A clip is refused up front if your monthly allowance is already spent; the take that crosses the cap still completes.

GET /api/v1/takes
GET /api/v1/takes/{id}
GET /api/v1/takes/{id}/exports/{kind}
DELETE /api/v1/takes/{id}
curl {BASE}/api/v1/takes/8Fk2… \
  -H "Authorization: Bearer mk_live_…"

{
  "id": "8Fk2…",
  "status": "ready",
  "frames": 240,
  "persons": 1,
  "exports": [
    { "kind": "fbx",  "url": "/api/v1/takes/8Fk2…/exports/fbx" },
    { "kind": "glb",  "url": "/api/v1/takes/8Fk2…/exports/glb" },
    { "kind": "bvh",  "url": "/api/v1/takes/8Fk2…/exports/bvh" },
    { "kind": "usd-mesh", "url": "…" }
  ]
}

Webhooks

Register an endpoint and we POST a signed event when a take finishes. No polling. Verify X-Mocai-Signature by computing HMAC-SHA256 of the raw request body with your signing secret.

# register once
curl -X PUT {BASE}/api/v1/webhook \
  -H "Authorization: Bearer mk_live_…" \
  -d '{ "url": "https://your-app.com/hooks/mocai" }'

# then we POST you signed events, no polling
POST https://your-app.com/hooks/mocai
X-Mocai-Event: take.ready
X-Mocai-Signature: sha256=5f2b…   # HMAC-SHA256(secret, raw body)

{
  "event": "take.ready",
  "data": { "take": { "id": "8Fk2…", "status": "ready", "exports": [ … ] } }
}

Errors & limits

Errors use standard HTTP status codes with a typed body: { "error": { "code", "message" } }. Two-step uploads take files up to 2 GB (inline multipart stays under ~30 MB), and a 403 usage_limit_reached means the monthly footage allowance is spent. List endpoints paginate with limit and the nextBefore cursor.