{
  "info": {
    "name": "Mocai API v1",
    "description": "Markerless motion capture as an API.\n\n## Getting started\n1. Create an API key at **App → Developers** and set the `apiKey` collection variable.\n2. `baseUrl` defaults to production (`https://mocai.ai`) — point it at `http://localhost:3000` for local development.\n3. Upload in two steps: **Create upload session** → **Upload file to session URL** (pick your video as the binary body) → **Create take from upload**. (For clips under ~30 MB, the one-shot **Upload video (inline, small files)** also works.) Poll **Get take** until `status` is `ready`, or better: configure a webhook (see the Webhook folder) and we'll POST you `take.ready` / `take.failed` events.\n\n## Notes\n- API access is included on all paid plans — Free-plan requests return `403 plan_required`.\n- Rate limits are per account (Basic 20 req/min, Pro 60; all keys share the budget). Responses carry `RateLimit-Limit/-Remaining/-Reset`; exceeding returns `429 rate_limited` with `Retry-After`.\n- Uploads count toward your plan's monthly footage allowance (30fps-equivalent minutes) and appear in your library alongside portal uploads. Uploads past the allowance are refused; takes past your plan's retention window become status `expired` (`expiresAt` warns 7 days ahead).\n- All errors use `{\"error\": {\"code\", \"message\"}}`.\n\n## Verifying webhook signatures\nEach delivery is signed over the raw body:\n`X-Mocai-Signature: sha256=HEX(HMAC_SHA256(secret, body))`\n\nNode example:\n```js\nconst crypto = require(\"crypto\");\nconst expected = \"sha256=\" + crypto.createHmac(\"sha256\", process.env.MOCAI_WEBHOOK_SECRET).update(rawBody).digest(\"hex\");\nconst ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers[\"x-mocai-signature\"]));\n```",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://mocai.ai",
      "type": "string"
    },
    {
      "key": "apiKey",
      "value": "mk_live_REPLACE_ME",
      "type": "string"
    },
    {
      "key": "takeId",
      "value": "",
      "type": "string"
    },
    {
      "key": "uploadId",
      "value": "",
      "type": "string"
    },
    {
      "key": "uploadUrl",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Takes",
      "item": [
        {
          "name": "Create upload session",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  const s = pm.response.json();",
                  "  pm.collectionVariables.set('uploadId', s.uploadId);",
                  "  pm.collectionVariables.set('uploadUrl', s.parts[0].url);",
                  "  if (s.parts.length > 1) console.log(`NOTE: ${s.parts.length} parts — Postman uploads part 0 only; use parallel PUTs of each byte range for large files`);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/uploads",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "uploads"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"contentType\": \"video/mp4\",\n  \"sizeBytes\": 48211234\n}"
            },
            "description": "Step 1 of the two-step upload (files up to 2 GB): returns `uploadId` plus `parts[]` — byte ranges each with their own direct-to-storage URL. Send parts concurrently for ~2x throughput; files ≤8 MB get a single part, so the next request (Upload file to session URL) just works. Then run Create take from upload."
          }
        },
        {
          "name": "Upload file to session URL",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "PUT",
            "url": {
              "raw": "{{uploadUrl}}",
              "host": [
                "{{uploadUrl}}"
              ]
            },
            "body": {
              "mode": "file",
              "file": {
                "src": ""
              }
            },
            "description": "Step 2: select your video as the binary body and send. Uses No Auth deliberately — the session URL is its own credential; never send your API key to storage."
          }
        },
        {
          "name": "Create take from upload",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  pm.collectionVariables.set('takeId', pm.response.json().id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"uploadId\": \"{{uploadId}}\",\n  \"title\": \"My take\",\n  \"hands\": true,\n  \"maxPersons\": 1,\n  \"smoothing\": \"standard\"\n}"
            },
            "description": "Step 3: adopt the finished upload as a take. Returns 201 with the take resource (status `queued`); processing is asynchronous — use webhooks or poll Get take. Optional `selection`: [{\"bbox\":[x0,y0,x1,y1]}] normalized first-frame boxes.\n\nCounts toward your monthly footage allowance; 403 `usage_limit_reached` when spent."
          }
        },
        {
          "name": "Upload video (inline, small files)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  pm.collectionVariables.set('takeId', pm.response.json().id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes"
              ]
            },
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": [],
                  "description": "The video to process (mp4/mov). Inline uploads suit small clips (~30 MB deployed); use the two-step flow for anything larger"
                },
                {
                  "key": "title",
                  "value": "My take",
                  "type": "text",
                  "description": "Optional — defaults to the filename"
                },
                {
                  "key": "hands",
                  "value": "true",
                  "type": "text",
                  "description": "Articulated finger tracking (default true)"
                },
                {
                  "key": "maxPersons",
                  "value": "1",
                  "type": "text",
                  "description": "Performers to reconstruct, capped by plan (Free 1, Basic 3, Pro 10)"
                },
                {
                  "key": "smoothing",
                  "value": "standard",
                  "type": "text",
                  "description": "Temporal smoothing of exports & playback: off | standard | strong (default standard; keypoint exports stay raw)"
                },
                {
                  "key": "selection",
                  "value": "",
                  "type": "text",
                  "description": "Optional JSON: [{\"bbox\":[x0,y0,x1,y1]}] normalized first-frame boxes of the people to track",
                  "disabled": true
                }
              ]
            },
            "description": "One-shot alternative for small clips: video and options in a single multipart request. Deployed request bodies cap around 30 MB — prefer the two-step flow (Create upload session) for anything larger. Returns 201 with the take resource (status `queued`).\n\nCounts toward your monthly footage allowance; 403 `usage_limit_reached` when spent."
          }
        },
        {
          "name": "List takes",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "1–100, default 20"
                },
                {
                  "key": "status",
                  "value": "ready",
                  "description": "queued | processing | baking | ready | failed",
                  "disabled": true
                },
                {
                  "key": "before",
                  "value": "",
                  "description": "Pagination cursor — the nextBefore value from the previous page",
                  "disabled": true
                }
              ]
            },
            "description": "Newest first. Response: {takes: [...], nextBefore: <ms|null>}."
          }
        },
        {
          "name": "Get take",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes/{{takeId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes",
                "{{takeId}}"
              ]
            },
            "description": "Fetch one take. When `status` is `ready`, `exports[]` lists downloadable assets (FBX, GLB, BVH, USD, USD mesh, keypoints JSON/CSV)."
          }
        },
        {
          "name": "Download export",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes/{{takeId}}/exports/fbx",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes",
                "{{takeId}}",
                "exports",
                "fbx"
              ]
            },
            "description": "Answers with a 302 redirect to a short-lived signed download URL (large exports exceed proxy response limits, so storage serves the bytes directly). Follow the redirect; do NOT send the Authorization header to the redirect target (Postman, curl -L, and python-requests handle this automatically). Kinds: fbx, glb, bvh, usd, usd-mesh, keypoints-json, keypoints-csv (see the take's exports[] for what's available)."
          }
        },
        {
          "name": "Delete take",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/api/v1/takes/{{takeId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "takes",
                "{{takeId}}"
              ]
            },
            "description": "Deletes the take, its source video, and every processed asset. Irreversible."
          }
        }
      ]
    },
    {
      "name": "Webhook",
      "item": [
        {
          "name": "Get webhook config",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhook",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhook"
              ]
            }
          }
        },
        {
          "name": "Set webhook",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhook",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhook"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://yourapp.com/hooks/mocai\"\n}"
            },
            "description": "We POST take.ready / take.failed events here. The response includes your signing secret (stable across URL changes) — verify X-Mocai-Signature with it; see the collection description."
          }
        },
        {
          "name": "Remove webhook",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhook",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhook"
              ]
            }
          }
        }
      ]
    }
  ]
}