{"openapi":"3.1.0","info":{"title":"WizCut API","version":"1.0.0","summary":"Automatic multicam editing for video podcasts.","description":"WizCut edits multicam video podcasts automatically: upload the camera angles (and optionally separate audio), WizCut syncs them, detects who speaks when, works out who is on which camera, and cuts between cameras. A person can review the edit in a hosted editor before WizCut renders the final video.\n\nTypical flow:\n1. createJob with the sources, a `callbackUrl` and an `Idempotency-Key`.\n2. Upload each file to its presigned URL (multipart: requestUploadPartUrls, then completeMultipartUpload).\n3. startProcessing.\n4. Wait for the webhook (or poll getJob with `view=summary`). On \"mapping\", send a person to `reviewUrl` or call submitSpeakerMapping. On \"ready\", a person reviews, or you call startRender.\n5. On \"complete\", download `outputUrl`.\n\nAuthenticate every request with `Authorization: Bearer wc_live_…` (create a key at https://wizcut.com/settings). Presigned upload and download URLs need no auth header.\n\nErrors are `{ \"error\": string, \"code\"?: string }`. Codes:\n- `TRACKS_ON_CREATE` (400): POST /api/jobs got a speaker mapping. Leave `tracks` out and use `autoMap`, or set the mapping later with POST /api/jobs/{jobId}/tracks.\n- `FILE_TOO_LARGE` (400): A file is over the size limit, declared in `fileSize` or found once uploaded. `maxBytes` gives the limit.\n- `INVALID_FILE_SIZE` (400): `fileSize` isn't a whole number of bytes.\n- `FILE_SIZE_MISMATCH` (400): The uploaded file is much smaller or larger than its declared `fileSize`. Upload it again to the same source.\n- `UNKNOWN_SOURCE` (400): A track names a `sourceId` that isn't one of the job's video sources.\n- `EMPTY_MAPPING` (400): Speakers were detected but the submitted mapping puts nobody on any camera. Put each speaker on the camera that shows them.\n- `CUTS_REQUIRED` (400): Changing the mapping after cuts exist needs the updated `cuts` too.\n- `NOT_ENOUGH_VIDEOS` (400): The job needs at least two video sources to be processed.\n- `INVALID_IDEMPOTENCY_KEY` (400): The `Idempotency-Key` header isn't 1–255 printable ASCII characters.\n- `QUOTA_EXCEEDED` (403): You've used up your minutes.\n- `INVALID_STATUS` (409): The job isn't in a status that allows this — for example, its mapping was already submitted. The response's `status` field (when present) is the job's current status.\n- `SOURCES_PENDING` (409): Some sources are still uploading. `pendingSourceIds` lists them; finish those uploads, then start processing again.\n- `NO_CUTS` (409): There are no cuts to render.\n- `IDEMPOTENCY_IN_PROGRESS` (409): A request with the same `Idempotency-Key` is still being processed. Retry in a moment (see `Retry-After`).\n- `IDEMPOTENCY_KEY_REUSED` (409): The job this `Idempotency-Key` created has been deleted. Use a new key.\n- `IDEMPOTENCY_KEY_MISMATCH` (422): This `Idempotency-Key` was already used with a different request body. Use a new key for a different request.","contact":{"name":"WizCut support","email":"support@wizcut.com"}},"externalDocs":{"description":"Human-readable API guide","url":"https://wizcut.com/docs/api"},"servers":[{"url":"https://wizcut.com"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Jobs","description":"Create, find and delete editing jobs."},{"name":"Uploads","description":"Multipart upload of large source files."},{"name":"Processing","description":"Move a job through the pipeline: process, map, render."},{"name":"Editing","description":"Change the cuts before rendering."},{"name":"Webhooks","description":"Events WizCut POSTs to a job's callbackUrl."}],"paths":{"/api/jobs":{"post":{"operationId":"createJob","tags":["Jobs"],"summary":"Create a job and get upload URLs","description":"Start here. Creates a multicam editing job from a list of camera angles and audio files, and returns where to upload each file.\n\nNext steps: upload every file as its `uploads` entry says (a single presigned PUT, or multipart parts followed by completeMultipartUpload), then call startProcessing. Pass `fileSize` for every file so large files get multipart uploads.\n\nYou can't pass a speaker mapping (`tracks`) here: source IDs only exist once the job is created. Use `autoMap`, or submitSpeakerMapping later.\n\nSend an `Idempotency-Key` header to make retries safe: the same key and body return the same job (with fresh URLs and `Idempotent-Replayed: true`) instead of creating a second one.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional, recommended for agents. A value you pick for this job (a UUID works), 1–255 printable ASCII characters, remembered for 24 hours. Same key + same body replays the original response; same key + different body gets 422 IDEMPOTENCY_KEY_MISMATCH. Requests refused before a job is created don't use up the key.","schema":{"type":"string","minLength":1,"maxLength":255},"example":"5b0d7c1e-8a3f-4f8e-9c51-2f0e6d9b7a14"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobRequest"},"example":{"sources":[{"label":"Camera 1","kind":"video","ext":"mp4","fileSize":2147483648},{"label":"Camera 2","kind":"video","ext":"mp4","fileSize":1932735283},{"label":"Mic mix","kind":"audio","ext":"wav"}],"callbackUrl":"https://your-server.com/webhook","review":true,"autoMap":"confident","silence":{"mode":"tighten"}}}}},"responses":{"200":{"description":"Job created (or, with a reused Idempotency-Key, the original job replayed).","headers":{"Idempotent-Replayed":{"description":"`true` when this response replays a job created by an earlier request with the same Idempotency-Key.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","uploads":{"a1b2c3d4-0000-4000-8000-000000000001":{"method":"multipart","uploadId":"2~abcDEF","key":"sources/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/a1b2c3d4-0000-4000-8000-000000000001.mp4","partSize":52428800,"partCount":41,"urls":["https://presigned-part-1-url...","https://presigned-part-2-url..."]},"a1b2c3d4-0000-4000-8000-000000000003":{"method":"put","url":"https://presigned-upload-url..."}},"audioUploads":{}}}}},"400":{"description":"Invalid request: missing `sources`, more than 12 sources, invalid `autoMap` or `silence`, or codes TRACKS_ON_CREATE, FILE_TOO_LARGE, INVALID_FILE_SIZE, INVALID_IDEMPOTENCY_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"File is too large. The limit is 100 GB per file.","code":"FILE_TOO_LARGE","maxBytes":107374182400,"sourceIndex":0}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"QUOTA_EXCEEDED: the account's minutes are used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"You've used 90 of your 90 free minutes. Upgrade to continue.","code":"QUOTA_EXCEEDED"}}}},"409":{"description":"IDEMPOTENCY_IN_PROGRESS (the first request with this key is still running; retry after `Retry-After` seconds) or IDEMPOTENCY_KEY_REUSED (the job this key created was deleted; use a new key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"A request with this Idempotency-Key is still being processed. Retry in a moment.","code":"IDEMPOTENCY_IN_PROGRESS"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"string"}}}},"422":{"description":"IDEMPOTENCY_KEY_MISMATCH: this key was already used with a different body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This Idempotency-Key was already used with a different request body. Use a new key for a different request.","code":"IDEMPOTENCY_KEY_MISMATCH"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"operationId":"listJobs","tags":["Jobs"],"summary":"List recent jobs","description":"Returns the account's 50 most recent jobs, newest first, with their status. Use it to find a job ID; call getJob for details.","responses":{"200":{"description":"The jobs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListJobsResponse"},"example":{"jobs":[{"id":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","title":"Camera 1, Camera 2, Mic mix — Oct 10","status":"ready","created_at":"2026-10-10T09:12:03.000Z","updated_at":"2026-10-10T09:42:17.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}":{"get":{"operationId":"getJob","tags":["Jobs"],"summary":"Get a job's status, or the full job","description":"With `view=summary` (recommended for polling), returns a compact JobSummary: the status, render progress, and `nextAction` saying exactly what to do next. Without it, returns the full job including every speaker turn, cut, the camera-map proposal and freshly signed download URLs.\n\nSigned URLs in the response are fresh on every request; fetch the job again rather than storing them. Prefer webhooks over tight polling; when polling, wait at least 30 seconds between requests.","parameters":[{"$ref":"#/components/parameters/JobId"},{"name":"view","in":"query","required":false,"description":"`summary` returns a JobSummary instead of the full job. Any other value returns the full job.","schema":{"type":"string","enum":["summary"]}}],"responses":{"200":{"description":"A JobSummary when `view=summary`, otherwise a GetJobResponse with the full job.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/GetJobResponse"},{"$ref":"#/components/schemas/JobSummary"}]},"examples":{"summary":{"summary":"view=summary","value":{"id":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"mapping","progress":null,"nextAction":{"type":"map_speakers","url":"https://wizcut.com/jobs/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/edit?reviewToken=...","message":"Confirm who is on which camera at the review link, or submit the mapping with POST /api/jobs/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/tracks."},"outputUrl":null,"error":null,"failedAtStatus":null,"updatedAt":"2026-10-10T09:42:17.000Z"}},"full":{"summary":"Full job","value":{"job":{"id":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"complete","sources":[{"id":"a1b2c3d4-0000-4000-8000-000000000001","label":"Camera 1","kind":"video","ext":"mp4"}],"tracks":[{"sourceId":"a1b2c3d4-0000-4000-8000-000000000001","speakers":["SPEAKER_00"]}],"turns":[{"speaker":"SPEAKER_00","startMs":0,"endMs":4200}],"cuts":[{"startMs":0,"endMs":5000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000001"},{"startMs":5000,"endMs":12000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000002"}],"removed_ranges":[],"camera_map":{"version":1,"computedAt":"2026-10-01T09:42:17.000Z","speakers":["SPEAKER_00","SPEAKER_01"],"unassignedSpeakers":[],"droppedSpeakers":["SPEAKER_02"],"cameras":[{"sourceId":"a1b2c3d4-0000-4000-8000-000000000001","kind":"closeup","confidence":"high","speakers":["SPEAKER_00"],"z":{"SPEAKER_00":11.5,"SPEAKER_01":0.9},"lagErrorS":{"SPEAKER_00":0.1,"SPEAKER_01":41.6}},{"sourceId":"a1b2c3d4-0000-4000-8000-000000000002","kind":"closeup","confidence":"high","speakers":["SPEAKER_01"],"z":{"SPEAKER_01":12.4,"SPEAKER_00":1.3},"lagErrorS":{"SPEAKER_01":0.2,"SPEAKER_00":63}}]},"output_url":"https://presigned-download-url...","error_message":null},"sourceUrls":{"a1b2c3d4-0000-4000-8000-000000000001":"https://..."},"audioUrl":"https://..."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"operationId":"deleteJob","tags":["Jobs"],"summary":"Delete a job and its files","description":"Permanently deletes the job and every file it stored: uploads, proxies, speaker-detection audio and the render. Works in any status. Does not give back used minutes. Irreversible: confirm with the user first.","parameters":[{"$ref":"#/components/parameters/JobId"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"},"example":{"ok":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"Some files couldn't be deleted. Try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Couldn't delete all files. Please try again."}}}},"503":{"description":"Storage isn't available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Storage not configured"}}}}}}},"/api/jobs/{jobId}/sources/{sourceId}/parts":{"post":{"operationId":"requestUploadPartUrls","tags":["Uploads"],"summary":"Get presigned URLs for more multipart upload parts","description":"For sources with a `multipart` upload: createJob only presigns the first few parts. Call this for the next batch (up to 16 part numbers) shortly before uploading them, then PUT each part's bytes to its URL without an Authorization header. When every part is uploaded, call completeMultipartUpload.","parameters":[{"$ref":"#/components/parameters/JobId"},{"$ref":"#/components/parameters/SourceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartUrlsRequest"},"example":{"uploadId":"2~abcDEF","partNumbers":[5,6,7,8]}}}},"responses":{"200":{"description":"URLs by part number.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartUrlsResponse"},"example":{"urls":{"5":"https://...","6":"https://..."}}}}},"400":{"description":"Missing `uploadId`, invalid `partNumbers`, or a part number beyond the upload's `partCount`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This upload has 41 parts; part 42 doesn't exist"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The job or source doesn't exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Source not found"}}}},"500":{"$ref":"#/components/responses/ServerError"},"503":{"description":"Storage isn't available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Storage not configured"}}}}}}},"/api/uploads/complete":{"post":{"operationId":"completeMultipartUpload","tags":["Uploads"],"summary":"Finish a multipart upload","description":"Call once every part of a multipart upload has been PUT. Assembles the file and checks its size against the declared `fileSize`. After all files are uploaded, call startProcessing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultipartUploadRef"},"example":{"key":"sources/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/a1b2c3d4-0000-4000-8000-000000000001.mp4","uploadId":"2~abcDEF"}}}},"responses":{"200":{"description":"Upload complete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"},"example":{"ok":true}}}},"400":{"description":"Missing `key`/`uploadId`, or the assembled file failed its size check (FILE_TOO_LARGE, FILE_SIZE_MISMATCH). The file is then deleted; upload it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"The uploaded file isn't the size that was declared for it. Upload it again.","code":"FILE_SIZE_MISMATCH","sourceId":"a1b2c3d4-0000-4000-8000-000000000001"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key belongs to a job that isn't yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Forbidden"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/uploads/abort":{"post":{"operationId":"abortMultipartUpload","tags":["Uploads"],"summary":"Cancel a multipart upload","description":"Gives up on a multipart upload and discards the parts uploaded so far. Takes the same body as completeMultipartUpload.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultipartUploadRef"},"example":{"key":"sources/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/a1b2c3d4-0000-4000-8000-000000000001.mp4","uploadId":"2~abcDEF"}}}},"responses":{"200":{"description":"Upload aborted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"},"example":{"ok":true}}}},"400":{"description":"Missing `key` or `uploadId`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"key and uploadId are required"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key belongs to a job that isn't yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Forbidden"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}/process":{"post":{"operationId":"startProcessing","tags":["Processing"],"summary":"Start processing once every file is uploaded","description":"Kicks off the pipeline: audio sync, speaker detection, proxy rendering, camera mapping and cut generation. Returns immediately with status \"syncing\"; the work runs asynchronously and takes minutes.\n\nNext: wait for the webhook, or poll getJob with `view=summary`. With the default `autoMap` (\"confident\") the next webhook is either \"ready\" (WizCut mapped the cameras itself) or \"mapping\" (a person or submitSpeakerMapping must confirm who is on which camera).","parameters":[{"$ref":"#/components/parameters/JobId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessRequest"},"example":{"diarizeSourceIds":["a1b2c3d4-0000-4000-8000-000000000003"]}}}},"responses":{"200":{"description":"Processing started. `alreadyProcessed: true` means an earlier call already started it (the job stays \"uploading\" while videos finish).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/JobStatusResponse"}],"properties":{"alreadyProcessed":{"type":"boolean"}}},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"syncing"}}}},"400":{"description":"NOT_ENOUGH_VIDEOS (fewer than two video sources), the job has no sources, or an uploaded file failed its size check (FILE_TOO_LARGE, FILE_SIZE_MISMATCH).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"At least two camera files are required","code":"NOT_ENOUGH_VIDEOS"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS (the job was already started; it must be \"created\" or \"uploading\") or SOURCES_PENDING (some sources are still uploading).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Job cannot be started from status \"syncing\". Expected \"created\" or \"uploading\".","code":"INVALID_STATUS","status":"syncing"}}}},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"The audio sync service rejected the job. The job is marked failed; see retryJob.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Audio sync failed"}}}},"503":{"description":"Processing isn't available right now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Sync service not available. Contact support."}}}}}}},"/api/jobs/{jobId}/tracks":{"post":{"operationId":"submitSpeakerMapping","tags":["Processing"],"summary":"Tell WizCut which speaker is on which camera","description":"Call while the job is in \"mapping\": submit one track per video source naming the speakers it shows. Speaker labels come from the \"mapping\" webhook (`speakers`) or the job's `turns`; the job's `camera_map` is WizCut's proposal you can accept or correct. WizCut then generates the cuts, moves the job to \"ready\", sends the \"ready\" webhook and, if the job was created with `review: false`, starts the render. The response contains the new cuts.\n\nAdvanced: when the job is already \"ready\" or \"complete\", this corrects the mapping instead, but only together with the corrected `cuts` (else CUTS_REQUIRED). It then sends no webhook and starts no render; call startRender for a new output.","parameters":[{"$ref":"#/components/parameters/JobId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitTracksRequest"},"example":{"tracks":[{"sourceId":"a1b2c3d4-0000-4000-8000-000000000001","speakers":["SPEAKER_00"]},{"sourceId":"a1b2c3d4-0000-4000-8000-000000000002","speakers":["SPEAKER_01"]}]}}}},"responses":{"200":{"description":"Mapping saved and cuts generated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitTracksResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","cuts":[{"startMs":0,"endMs":5000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000001"},{"startMs":5000,"endMs":12000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000002"}],"success":true}}}},"400":{"description":"Invalid tracks: UNKNOWN_SOURCE (a sourceId isn't one of the job's video sources), EMPTY_MAPPING (nobody on any camera), CUTS_REQUIRED (correcting after cuts exist needs `cuts`), or a malformed body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unknown sourceId \"auto\": each track must name one of this job's video sources.","code":"UNKNOWN_SOURCE"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS: the job isn't in \"mapping\" (or \"ready\"/\"complete\" for a correction), or the mapping was already submitted, for example automatically.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This job's speakers were already assigned.","code":"INVALID_STATUS"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}/recut":{"post":{"operationId":"recutJob","tags":["Editing"],"summary":"Regenerate cuts with a different pace or silence setting","description":"Regenerates the cuts and removed ranges from the already-detected speakers, using the chosen pace preset and optionally new silence-removal settings. No reprocessing: returns immediately with the new cuts. Works while the job is \"ready\" or \"complete\". The render isn't updated; call startRender for a new output.","parameters":[{"$ref":"#/components/parameters/JobId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecutRequest"},"example":{"presetId":"balanced","silence":{"mode":"remove","minPauseSec":1}}}}},"responses":{"200":{"description":"New cuts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecutResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","presetId":"balanced","cuts":[{"startMs":0,"endMs":5000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000001"},{"startMs":5000,"endMs":12000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000002"}],"removedRanges":[{"startMs":61200,"endMs":63900}],"savedMs":2700,"silence":{"mode":"remove","minPauseSec":1,"keepPauseSec":0.7,"paddingSec":0.25},"success":true}}}},"400":{"description":"Invalid body, e.g. an unknown `presetId` or out-of-range silence settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"presetId must be one of: relaxed, balanced, dynamic"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS: the job isn't \"ready\" or \"complete\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Cuts cannot be regenerated in status \"mapping\".","code":"INVALID_STATUS","status":"mapping"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}/cuts":{"patch":{"operationId":"updateCuts","tags":["Editing"],"summary":"Replace the cut list or removed ranges","description":"For fully programmatic editing: replaces the job's cuts (which camera is shown when) and/or its silence-removal spans. Works while the job is \"ready\" or \"complete\". Call startRender afterwards to render the edit.","parameters":[{"$ref":"#/components/parameters/JobId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCutsRequest"},"example":{"cuts":[{"startMs":0,"endMs":5000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000001"},{"startMs":5000,"endMs":12000,"sourceId":"a1b2c3d4-0000-4000-8000-000000000002"}]}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","success":true}}}},"400":{"description":"Neither `cuts` nor `removedRanges` given, or one is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"each cut needs numeric startMs <= endMs and a sourceId"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS: the job isn't \"ready\" or \"complete\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Cuts cannot be edited in status \"rendering\".","code":"INVALID_STATUS","status":"rendering"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}/render":{"post":{"operationId":"startRender","tags":["Processing"],"summary":"Render the final video","description":"Starts rendering the job's current cuts. Works from \"ready\", or from \"complete\" to render again after editing. Returns immediately with status \"rendering\"; the \"complete\" webhook carries the download URL (or poll getJob with `view=summary`, whose `progress` goes from 0 to 1).","parameters":[{"$ref":"#/components/parameters/JobId"}],"responses":{"200":{"description":"Render started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"rendering"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS (the job isn't \"ready\" or \"complete\") or NO_CUTS (nothing to render).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This job has no cuts to render. If speaker detection found no speech, check that the source you picked for it isn't silent.","code":"NO_CUTS"}}}},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"The render couldn't be started. The job is marked failed; see retryJob.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Render failed to start"}}}}}}},"/api/jobs/{jobId}/approve":{"post":{"operationId":"approveJob","tags":["Processing"],"summary":"Mark a rendered job as approved","description":"Records that a person approved the rendered output. Only works once the job is \"complete\". Moves it to \"approved\" and sends the \"approved\" webhook. Optional: nothing else depends on it.","parameters":[{"$ref":"#/components/parameters/JobId"}],"responses":{"200":{"description":"Approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"approved"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"INVALID_STATUS: the job isn't \"complete\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Job cannot be approved from status \"rendering\". Expected \"complete\".","code":"INVALID_STATUS","status":"rendering"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/jobs/{jobId}/retry":{"post":{"operationId":"retryJob","tags":["Processing"],"summary":"Retry a failed job","description":"Restarts a \"failed\" job from the stage that failed (syncing, diarizing or rendering). Check getJob with `view=summary` first: when `nextAction.type` is `contact_support`, retrying won't help. When the sources picked for speaker detection had no speech, pass `diarizeSourceIds` naming a source with clear speech; that restarts from syncing.","parameters":[{"$ref":"#/components/parameters/JobId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryRequest"},"example":{"diarizeSourceIds":["a1b2c3d4-0000-4000-8000-000000000003"]}}}},"responses":{"200":{"description":"Retry started; `status` is the stage it restarted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"syncing"}}}},"400":{"description":"`diarizeSourceIds` is empty or names unknown sources.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"diarizeSourceIds must be a non-empty array"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/JobNotFound"},"409":{"description":"The job isn't \"failed\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Cannot retry from status \"ready\""}}}},"422":{"description":"The job failed at a stage that can't be retried. Start a new job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Cannot determine which stage to retry"}}}},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"The retry couldn't be started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Retry failed"}}}}}}}},"webhooks":{"jobMapping":{"post":{"operationId":"onJobMapping","tags":["Webhooks"],"summary":"Job needs its speaker mapping confirmed","description":"Speakers were detected and a person (or your agent) needs to confirm who is on which camera. Send a person to `reviewUrl`, or call submitSpeakerMapping. With `autoMap` \"confident\" or \"always\" this only arrives when WizCut couldn't decide on its own; `cameraMap` holds its proposal when there is one.\n\nSent as a JSON POST to the job's `callbackUrl`, signed per the Standard Webhooks spec (https://www.standardwebhooks.com) with your account's signing secret from the WizCut settings page. Verify the signature over the raw body. WizCut tries each delivery up to three times and waits up to 10 seconds per attempt; answer with any 2xx. Poll getJob as a fallback in case a delivery is lost.","parameters":[{"name":"webhook-id","in":"header","required":false,"description":"Unique per event (`msg_…`). Retries of the same event carry the same id: use it to skip duplicates.","schema":{"type":"string"},"example":"msg_2b6f0c4e9a7d4b1c8e3f5a6b7c8d9e0f"},{"name":"webhook-timestamp","in":"header","required":false,"description":"When this attempt was sent, in Unix seconds. Reject deliveries more than 5 minutes from your clock.","schema":{"type":"string"},"example":"1760089337"},{"name":"webhook-signature","in":"header","required":false,"description":"Space-separated `v1,<base64>` signatures (two during a secret rotation). Each is the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your `whsec_…` secret after the prefix. Accept if any matches (constant-time compare). The official `standardwebhooks` libraries verify this. Absent only if WizCut couldn't sign the delivery.","schema":{"type":"string"},"example":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MappingWebhook"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"mapping","reviewUrl":"https://wizcut.com/jobs/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/edit?reviewToken=...","speakers":["SPEAKER_00","SPEAKER_01"],"cameraMap":{"version":1,"computedAt":"2026-10-01T09:42:17.000Z","speakers":["SPEAKER_00","SPEAKER_01"],"unassignedSpeakers":[],"droppedSpeakers":["SPEAKER_02"],"cameras":[{"sourceId":"a1b2c3d4-0000-4000-8000-000000000001","kind":"closeup","confidence":"high","speakers":["SPEAKER_00"],"z":{"SPEAKER_00":11.5,"SPEAKER_01":0.9},"lagErrorS":{"SPEAKER_00":0.1,"SPEAKER_01":41.6}},{"sourceId":"a1b2c3d4-0000-4000-8000-000000000002","kind":"closeup","confidence":"high","speakers":["SPEAKER_01"],"z":{"SPEAKER_01":12.4,"SPEAKER_00":1.3},"lagErrorS":{"SPEAKER_01":0.2,"SPEAKER_00":63}}]}}}}},"responses":{"2XX":{"description":"Return any 2xx status to acknowledge. Anything else (or no answer within 10 seconds) is retried."}}}},"jobReady":{"post":{"operationId":"onJobReady","tags":["Webhooks"],"summary":"Cuts are ready","description":"The speaker mapping was submitted (by a person, your code or WizCut) and cuts were generated. With `review: true`, a person can review them at `reviewUrl`, or you can call startRender. With `review: false`, rendering has already started.\n\nSent as a JSON POST to the job's `callbackUrl`, signed per the Standard Webhooks spec (https://www.standardwebhooks.com) with your account's signing secret from the WizCut settings page. Verify the signature over the raw body. WizCut tries each delivery up to three times and waits up to 10 seconds per attempt; answer with any 2xx. Poll getJob as a fallback in case a delivery is lost.","parameters":[{"name":"webhook-id","in":"header","required":false,"description":"Unique per event (`msg_…`). Retries of the same event carry the same id: use it to skip duplicates.","schema":{"type":"string"},"example":"msg_2b6f0c4e9a7d4b1c8e3f5a6b7c8d9e0f"},{"name":"webhook-timestamp","in":"header","required":false,"description":"When this attempt was sent, in Unix seconds. Reject deliveries more than 5 minutes from your clock.","schema":{"type":"string"},"example":"1760089337"},{"name":"webhook-signature","in":"header","required":false,"description":"Space-separated `v1,<base64>` signatures (two during a secret rotation). Each is the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your `whsec_…` secret after the prefix. Accept if any matches (constant-time compare). The official `standardwebhooks` libraries verify this. Absent only if WizCut couldn't sign the delivery.","schema":{"type":"string"},"example":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyWebhook"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"ready","reviewUrl":"https://wizcut.com/jobs/3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10/edit?reviewToken=..."}}}},"responses":{"2XX":{"description":"Return any 2xx status to acknowledge. Anything else (or no answer within 10 seconds) is retried."}}}},"jobComplete":{"post":{"operationId":"onJobComplete","tags":["Webhooks"],"summary":"Render finished","description":"The final video is rendered. Download it from `outputUrl` (valid for seven days).\n\nSent as a JSON POST to the job's `callbackUrl`, signed per the Standard Webhooks spec (https://www.standardwebhooks.com) with your account's signing secret from the WizCut settings page. Verify the signature over the raw body. WizCut tries each delivery up to three times and waits up to 10 seconds per attempt; answer with any 2xx. Poll getJob as a fallback in case a delivery is lost.","parameters":[{"name":"webhook-id","in":"header","required":false,"description":"Unique per event (`msg_…`). Retries of the same event carry the same id: use it to skip duplicates.","schema":{"type":"string"},"example":"msg_2b6f0c4e9a7d4b1c8e3f5a6b7c8d9e0f"},{"name":"webhook-timestamp","in":"header","required":false,"description":"When this attempt was sent, in Unix seconds. Reject deliveries more than 5 minutes from your clock.","schema":{"type":"string"},"example":"1760089337"},{"name":"webhook-signature","in":"header","required":false,"description":"Space-separated `v1,<base64>` signatures (two during a secret rotation). Each is the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your `whsec_…` secret after the prefix. Accept if any matches (constant-time compare). The official `standardwebhooks` libraries verify this. Absent only if WizCut couldn't sign the delivery.","schema":{"type":"string"},"example":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteWebhook"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"complete","outputUrl":"https://presigned-download-url..."}}}},"responses":{"2XX":{"description":"Return any 2xx status to acknowledge. Anything else (or no answer within 10 seconds) is retried."}}}},"jobApproved":{"post":{"operationId":"onJobApproved","tags":["Webhooks"],"summary":"Output approved","description":"A person approved the rendered output (or approveJob was called).\n\nSent as a JSON POST to the job's `callbackUrl`, signed per the Standard Webhooks spec (https://www.standardwebhooks.com) with your account's signing secret from the WizCut settings page. Verify the signature over the raw body. WizCut tries each delivery up to three times and waits up to 10 seconds per attempt; answer with any 2xx. Poll getJob as a fallback in case a delivery is lost.","parameters":[{"name":"webhook-id","in":"header","required":false,"description":"Unique per event (`msg_…`). Retries of the same event carry the same id: use it to skip duplicates.","schema":{"type":"string"},"example":"msg_2b6f0c4e9a7d4b1c8e3f5a6b7c8d9e0f"},{"name":"webhook-timestamp","in":"header","required":false,"description":"When this attempt was sent, in Unix seconds. Reject deliveries more than 5 minutes from your clock.","schema":{"type":"string"},"example":"1760089337"},{"name":"webhook-signature","in":"header","required":false,"description":"Space-separated `v1,<base64>` signatures (two during a secret rotation). Each is the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your `whsec_…` secret after the prefix. Accept if any matches (constant-time compare). The official `standardwebhooks` libraries verify this. Absent only if WizCut couldn't sign the delivery.","schema":{"type":"string"},"example":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovedWebhook"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"approved","outputUrl":"https://presigned-download-url..."}}}},"responses":{"2XX":{"description":"Return any 2xx status to acknowledge. Anything else (or no answer within 10 seconds) is retried."}}}},"jobFailed":{"post":{"operationId":"onJobFailed","tags":["Webhooks"],"summary":"Job failed","description":"The job failed at any stage. For most sync, speaker-detection and render failures WizCut already retried once. Files it can't read (unsupported or damaged) fail straight away. Check getJob with `view=summary` for whether retryJob can help.\n\nSent as a JSON POST to the job's `callbackUrl`, signed per the Standard Webhooks spec (https://www.standardwebhooks.com) with your account's signing secret from the WizCut settings page. Verify the signature over the raw body. WizCut tries each delivery up to three times and waits up to 10 seconds per attempt; answer with any 2xx. Poll getJob as a fallback in case a delivery is lost.","parameters":[{"name":"webhook-id","in":"header","required":false,"description":"Unique per event (`msg_…`). Retries of the same event carry the same id: use it to skip duplicates.","schema":{"type":"string"},"example":"msg_2b6f0c4e9a7d4b1c8e3f5a6b7c8d9e0f"},{"name":"webhook-timestamp","in":"header","required":false,"description":"When this attempt was sent, in Unix seconds. Reject deliveries more than 5 minutes from your clock.","schema":{"type":"string"},"example":"1760089337"},{"name":"webhook-signature","in":"header","required":false,"description":"Space-separated `v1,<base64>` signatures (two during a secret rotation). Each is the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your `whsec_…` secret after the prefix. Accept if any matches (constant-time compare). The official `standardwebhooks` libraries verify this. Absent only if WizCut couldn't sign the delivery.","schema":{"type":"string"},"example":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedWebhook"},"example":{"jobId":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10","status":"failed","error":"description of what went wrong","failedAtStatus":"syncing"}}}},"responses":{"2XX":{"description":"Return any 2xx status to acknowledge. Anything else (or no answer within 10 seconds) is retried."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"wc_live_…","description":"A WizCut API key (`wc_live_…`), created and revoked on https://wizcut.com/settings."}},"schemas":{"Error":{"type":"object","description":"Every error response has this shape. Read `error` for a plain-English explanation you can show a person; branch on `code` when present. Some errors add fields such as `maxBytes`, `sourceId`, `status` or `pendingSourceIds`.","required":["error"],"properties":{"error":{"type":"string","description":"What went wrong, in plain English."},"code":{"$ref":"#/components/schemas/ErrorCode"},"maxBytes":{"type":"integer","description":"With `FILE_TOO_LARGE`: the per-file limit in bytes."},"sourceId":{"type":"string","description":"With file size errors: the source the file belongs to."},"sourceIndex":{"type":"integer","description":"With file size errors on POST /api/jobs: the index in `sources` of the offending file."},"status":{"type":"string","description":"With `INVALID_STATUS`: the job's current status."},"pendingSourceIds":{"type":"array","items":{"type":"string"},"description":"With `SOURCES_PENDING`: sources still uploading."}},"additionalProperties":true},"ErrorCode":{"type":"string","description":"Machine-readable error code. Not every error has one; treat a code you don't recognise like an error without a code.\n\n- `TRACKS_ON_CREATE` (400): POST /api/jobs got a speaker mapping. Leave `tracks` out and use `autoMap`, or set the mapping later with POST /api/jobs/{jobId}/tracks.\n- `FILE_TOO_LARGE` (400): A file is over the size limit, declared in `fileSize` or found once uploaded. `maxBytes` gives the limit.\n- `INVALID_FILE_SIZE` (400): `fileSize` isn't a whole number of bytes.\n- `FILE_SIZE_MISMATCH` (400): The uploaded file is much smaller or larger than its declared `fileSize`. Upload it again to the same source.\n- `UNKNOWN_SOURCE` (400): A track names a `sourceId` that isn't one of the job's video sources.\n- `EMPTY_MAPPING` (400): Speakers were detected but the submitted mapping puts nobody on any camera. Put each speaker on the camera that shows them.\n- `CUTS_REQUIRED` (400): Changing the mapping after cuts exist needs the updated `cuts` too.\n- `NOT_ENOUGH_VIDEOS` (400): The job needs at least two video sources to be processed.\n- `INVALID_IDEMPOTENCY_KEY` (400): The `Idempotency-Key` header isn't 1–255 printable ASCII characters.\n- `QUOTA_EXCEEDED` (403): You've used up your minutes.\n- `INVALID_STATUS` (409): The job isn't in a status that allows this — for example, its mapping was already submitted. The response's `status` field (when present) is the job's current status.\n- `SOURCES_PENDING` (409): Some sources are still uploading. `pendingSourceIds` lists them; finish those uploads, then start processing again.\n- `NO_CUTS` (409): There are no cuts to render.\n- `IDEMPOTENCY_IN_PROGRESS` (409): A request with the same `Idempotency-Key` is still being processed. Retry in a moment (see `Retry-After`).\n- `IDEMPOTENCY_KEY_REUSED` (409): The job this `Idempotency-Key` created has been deleted. Use a new key.\n- `IDEMPOTENCY_KEY_MISMATCH` (422): This `Idempotency-Key` was already used with a different request body. Use a new key for a different request.","enum":["TRACKS_ON_CREATE","FILE_TOO_LARGE","INVALID_FILE_SIZE","FILE_SIZE_MISMATCH","UNKNOWN_SOURCE","EMPTY_MAPPING","CUTS_REQUIRED","NOT_ENOUGH_VIDEOS","INVALID_IDEMPOTENCY_KEY","QUOTA_EXCEEDED","INVALID_STATUS","SOURCES_PENDING","NO_CUTS","IDEMPOTENCY_IN_PROGRESS","IDEMPOTENCY_KEY_REUSED","IDEMPOTENCY_KEY_MISMATCH"]},"JobStatus":{"type":"string","enum":["created","uploading","syncing","diarizing","mapping","ready","rendering","complete","approved","failed"],"description":"Where the job is in its lifecycle:\n- `created`: job created, waiting for uploads.\n- `uploading`: waiting for files to finish uploading.\n- `syncing`: aligning audio across sources.\n- `diarizing`: detecting speakers.\n- `mapping`: speakers detected; WizCut is working out who's on which camera, or a person (or your agent) needs to confirm it.\n- `ready`: cuts generated, ready for review or rendering.\n- `rendering`: final render in progress.\n- `complete`: render done, `output_url` available.\n- `approved`: a person approved the output.\n- `failed`: something went wrong, see `error_message`."},"AutoMapMode":{"type":"string","enum":["off","confident","always"],"default":"confident","description":"Whether WizCut may decide on its own who is on which camera. `off`: never; a person or your agent submits the mapping. `confident` (default): maps the cameras itself when it is sure it found a close-up of every speaker, otherwise sends the \"mapping\" webhook. `always`: never waits for a person, uses its best guess."},"SilenceSettings":{"type":"object","description":"Automatic pause removal, applied before cuts are generated. Omit for off.","required":["mode"],"properties":{"mode":{"type":"string","enum":["off","tighten","remove"],"description":"`off`: keep all pauses. `tighten` (recommended): shorten long pauses to a natural beat. `remove`: cut long pauses out almost entirely."},"minPauseSec":{"type":"number","minimum":0.3,"maximum":10,"default":1.5,"description":"Only pauses longer than this are touched."},"keepPauseSec":{"type":"number","minimum":0,"maximum":3,"default":0.7,"description":"The beat left in place of a tightened pause (`tighten` only)."},"paddingSec":{"type":"number","minimum":0,"maximum":1,"default":0.25,"description":"Safety margin kept around speech at every removed range."}},"example":{"mode":"tighten"}},"SourceInput":{"type":"object","required":["label"],"properties":{"label":{"type":"string","description":"Human-readable name, e.g. \"Camera 1\" or \"Mic mix\". Used in the job title and in error messages."},"kind":{"type":"string","enum":["video","audio"],"default":"video","description":"`video` for a camera angle, `audio` for a separate microphone recording."},"ext":{"type":"string","enum":["mp4","mov","webm","mkv","wav","mp3","m4a","aac"],"description":"File extension. Defaults to `mp4` for video and `wav` for audio; an unknown value falls back to the default."},"fileSize":{"type":"integer","minimum":0,"maximum":107374182400,"description":"The file's exact size in bytes, up to 107374182400 (100 GB). Strongly recommended: files over 100 MB then get a multipart upload (parallel parts, per-part retries, and the only way past 5 GB). The size is signed into the upload URLs, so it must be exact. Without it the file gets a single PUT, limited to 5 GB."}}},"CreateJobRequest":{"type":"object","required":["sources"],"properties":{"sources":{"type":"array","maxItems":12,"items":{"$ref":"#/components/schemas/SourceInput"},"description":"Camera angles and audio files, up to 12. Processing needs at least two video sources."},"title":{"type":"string","minLength":1,"maxLength":200,"description":"Optional name for the job, e.g. the episode title. Defaults to the source labels and the date."},"callbackUrl":{"type":"string","format":"uri","description":"Optional. WizCut POSTs a signed JSON webhook here when the job reaches mapping, ready, complete, approved or failed. See the `webhooks` section."},"review":{"type":"boolean","default":true,"description":"When true, the job pauses at \"ready\" so a person can review cuts before rendering. When false, rendering starts automatically as soon as the speaker mapping is submitted."},"autoMap":{"$ref":"#/components/schemas/AutoMapMode"},"silence":{"$ref":"#/components/schemas/SilenceSettings"}}},"PutUpload":{"type":"object","required":["method","url"],"description":"Upload the whole file with one HTTP PUT to `url` within an hour. No Authorization header: the URL is presigned.","properties":{"method":{"type":"string","const":"put"},"url":{"type":"string","format":"uri"}}},"MultipartUpload":{"type":"object","required":["method","uploadId","key","partSize","partCount","urls"],"description":"Split the file into `partSize`-byte chunks (the last one is shorter) and PUT chunk n to the URL for part n — no Authorization header. `urls[0]` is part 1. Get URLs for the remaining parts with POST /api/jobs/{jobId}/sources/{sourceId}/parts, then finish with POST /api/uploads/complete.","properties":{"method":{"type":"string","const":"multipart"},"uploadId":{"type":"string"},"key":{"type":"string","description":"Storage key; pass it with `uploadId` to /api/uploads/complete or /api/uploads/abort."},"partSize":{"type":"integer","example":52428800},"partCount":{"type":"integer","description":"Total number of parts to upload."},"urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Presigned URLs for the first parts, valid for two hours. Request the rest as you go."}}},"UploadInfo":{"oneOf":[{"$ref":"#/components/schemas/PutUpload"},{"$ref":"#/components/schemas/MultipartUpload"}],"discriminator":{"propertyName":"method","mapping":{"put":"#/components/schemas/PutUpload","multipart":"#/components/schemas/MultipartUpload"}}},"CreateJobResponse":{"type":"object","required":["jobId","uploads"],"properties":{"jobId":{"type":"string","format":"uuid"},"uploads":{"type":"object","description":"Keyed by the source IDs WizCut assigned, in the same order as the request's `sources`. Upload each file as its entry describes.","additionalProperties":{"$ref":"#/components/schemas/UploadInfo"}},"audioUploads":{"type":"object","description":"Used by the WizCut apps to upload an extracted audio track ahead of the video. API clients can ignore it.","additionalProperties":{"$ref":"#/components/schemas/PutUpload"}}}},"JobListItem":{"type":"object","required":["id","status","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"],"description":"Generated from the source labels and date."},"status":{"$ref":"#/components/schemas/JobStatus"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ListJobsResponse":{"type":"object","required":["jobs"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/JobListItem"},"description":"Your 50 most recent jobs, newest first."}}},"JobSource":{"type":"object","required":["id","label","kind","ext"],"properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"kind":{"type":"string","enum":["video","audio"]},"ext":{"type":"string"},"durationMs":{"type":"number"},"fileSize":{"type":"integer"}},"additionalProperties":true},"Track":{"type":"object","required":["sourceId","speakers"],"description":"Which speakers a camera shows.","properties":{"sourceId":{"type":"string","description":"One of the job's video source IDs."},"speakers":{"type":"array","items":{"type":"string"},"description":"Speaker labels as detected, e.g. \"SPEAKER_00\". A camera can show more than one speaker."}}},"Turn":{"type":"object","required":["speaker","startMs","endMs"],"description":"One stretch of speech by one speaker.","properties":{"speaker":{"type":"string"},"startMs":{"type":"number"},"endMs":{"type":"number"}}},"Cut":{"type":"object","required":["startMs","endMs","sourceId"],"description":"Show `sourceId` from `startMs` to `endMs`, in source time (milliseconds).","properties":{"startMs":{"type":"number"},"endMs":{"type":"number"},"sourceId":{"type":"string"}}},"TimeRange":{"type":"object","required":["startMs","endMs"],"properties":{"startMs":{"type":"number"},"endMs":{"type":"number"}}},"CameraMapCamera":{"type":"object","required":["sourceId","kind","confidence","speakers"],"properties":{"sourceId":{"type":"string","description":"One of the job's video sources."},"kind":{"type":"string","enum":["closeup","twoshot","wide","multi","unknown"],"description":"`closeup`: one person. `twoshot`: two side by side. `wide`: three people. `multi`: several people one after another (operated or switched camera)."},"confidence":{"type":"string","enum":["high","low","none"],"description":"`high`: WizCut is sure. `low`: a likely answer. `none`: no answer."},"speakers":{"type":"array","items":{"type":"string"},"description":"Who the camera shows, left to right for two-shots and wides. Empty when confidence is `none`."},"positions":{"type":"object","description":"Two-shots and wides only: who is where in the frame.","properties":{"left":{"type":"string"},"middle":{"type":"string"},"right":{"type":"string"}}},"reason":{"type":"string","description":"`no_motion` when the camera couldn't be analysed."},"z":{"type":"object","additionalProperties":{"type":"number"},"description":"Diagnostic only; don't build on it."},"lagErrorS":{"type":"object","additionalProperties":{"type":"number"},"description":"Diagnostic only; don't build on it."}}},"CameraMap":{"type":"object","description":"WizCut's proposal for who is on which camera. To accept it, turn each camera you trust into a Track (its `sourceId` and `speakers`) and submit it with POST /api/jobs/{jobId}/tracks.","required":["version","speakers","unassignedSpeakers","droppedSpeakers","cameras"],"properties":{"version":{"type":"integer","const":1},"computedAt":{"type":"string","format":"date-time"},"speakers":{"type":"array","items":{"type":"string"},"description":"Speakers WizCut judged: everyone with at least a minute of speech."},"unassignedSpeakers":{"type":"array","items":{"type":"string"},"description":"Judged speakers who aren't on any high-confidence camera. Non-empty means a person should confirm the mapping."},"droppedSpeakers":{"type":"array","items":{"type":"string"},"description":"Speakers with too little speech to judge."},"cameras":{"type":"array","items":{"$ref":"#/components/schemas/CameraMapCamera"}}},"example":{"version":1,"computedAt":"2026-10-01T09:42:17.000Z","speakers":["SPEAKER_00","SPEAKER_01"],"unassignedSpeakers":[],"droppedSpeakers":["SPEAKER_02"],"cameras":[{"sourceId":"a1b2c3d4-0000-4000-8000-000000000001","kind":"closeup","confidence":"high","speakers":["SPEAKER_00"],"z":{"SPEAKER_00":11.5,"SPEAKER_01":0.9},"lagErrorS":{"SPEAKER_00":0.1,"SPEAKER_01":41.6}},{"sourceId":"a1b2c3d4-0000-4000-8000-000000000002","kind":"closeup","confidence":"high","speakers":["SPEAKER_01"],"z":{"SPEAKER_01":12.4,"SPEAKER_00":1.3},"lagErrorS":{"SPEAKER_01":0.2,"SPEAKER_00":63}}]}},"Job":{"type":"object","description":"The full job. Includes every speaker turn and cut, so it can be large; poll with `?view=summary` instead when you only need the status. Other fields may appear; ignore the ones you don't know.","required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"],"description":"Generated from the source labels and date."},"status":{"$ref":"#/components/schemas/JobStatus"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/JobSource"}},"tracks":{"type":"array","items":{"$ref":"#/components/schemas/Track"},"description":"The submitted speaker-to-camera mapping."},"turns":{"type":"array","items":{"$ref":"#/components/schemas/Turn"},"description":"Detected speech, once speaker detection is done."},"cuts":{"type":"array","items":{"$ref":"#/components/schemas/Cut"}},"removed_ranges":{"type":["array","null"],"items":{"$ref":"#/components/schemas/TimeRange"},"description":"Spans removed by silence removal, in source time. The render skips them."},"camera_map":{"oneOf":[{"$ref":"#/components/schemas/CameraMap"},{"type":"null"}],"description":"WizCut's camera proposal; null until computed, a few minutes after the proxies are built."},"output_url":{"type":["string","null"],"description":"Freshly signed download URL of the render, once complete. Re-fetch the job rather than storing it."},"error_message":{"type":["string","null"],"description":"Why the job failed, in plain English. Null unless failed."},"failed_at_status":{"type":["string","null"],"description":"The stage that failed, such as syncing, diarizing or rendering."},"review":{"type":"boolean"},"auto_map":{"$ref":"#/components/schemas/AutoMapMode"},"callback_url":{"type":["string","null"],"description":"Webhook URL given at creation."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":true},"GetJobResponse":{"type":"object","required":["job","sourceUrls"],"properties":{"job":{"$ref":"#/components/schemas/Job"},"sourceUrls":{"type":"object","additionalProperties":{"type":"string","format":"uri"},"description":"Download URLs of the uploaded files by source ID, signed fresh on every request (valid for an hour)."},"audioUrl":{"type":["string","null"],"description":"Download URL of the mixed audio used for speaker detection."}}},"NextAction":{"type":"object","required":["type","url","message"],"properties":{"type":{"type":"string","enum":["upload_files","wait","map_speakers","review_or_render","download","retry","contact_support"],"description":"What to do next. New types may be added; treat an unknown one like `wait`.\n- `upload_files` (created, uploading): upload the files, then call startProcessing.\n- `wait` (syncing, diarizing, rendering): nothing; check again later or wait for the next webhook.\n- `map_speakers` (mapping): confirm who's on which camera at `url`, or call submitSpeakerMapping.\n- `review_or_render` (ready): review the cuts at `url`, or call startRender.\n- `download` (complete, approved): download the video from `url`.\n- `retry` (failed): call retryJob; the message says if a new `diarizeSourceIds` is needed.\n- `contact_support` (failed): retrying won't help; start a new job or contact support."},"url":{"type":["string","null"],"description":"Review link (map_speakers, review_or_render), freshly signed render URL (download), mailto: link (contact_support), otherwise null."},"message":{"type":"string","description":"Plain-English hint you can pass on to a person or an agent. Build logic on `type`, not on this text."}}},"JobSummary":{"type":"object","required":["id","status","progress","nextAction","outputUrl","error","failedAtStatus","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/JobStatus"},"progress":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Render progress from 0 to 1 while rendering, else null."},"nextAction":{"$ref":"#/components/schemas/NextAction"},"outputUrl":{"type":["string","null"],"description":"Freshly signed render URL when complete or approved, else null."},"error":{"type":["string","null"],"description":"Same as the job's `error_message`."},"failedAtStatus":{"type":["string","null"],"description":"The stage that failed."},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ProcessRequest":{"type":"object","properties":{"diarizeSourceIds":{"type":"array","items":{"type":"string"},"description":"Which sources WizCut listens to for speaker detection. Defaults to your `audio` sources, or the first source if there are none. Pick sources with clean audio of everyone talking; a muted camera leads to a job with no cuts."}}},"JobStatusResponse":{"type":"object","required":["jobId","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/JobStatus"}}},"PartUrlsRequest":{"type":"object","required":["uploadId","partNumbers"],"properties":{"uploadId":{"type":"string","description":"The `uploadId` from the job's upload info."},"partNumbers":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"integer","minimum":1,"maximum":10000},"description":"Up to 16 part numbers (1-based), each no higher than the upload's `partCount`."}}},"PartUrlsResponse":{"type":"object","required":["urls"],"properties":{"urls":{"type":"object","description":"Presigned PUT URL per part number. Valid for two hours, so fetch them shortly before use.","additionalProperties":{"type":"string","format":"uri"}}}},"MultipartUploadRef":{"type":"object","required":["key","uploadId"],"properties":{"key":{"type":"string","description":"The `key` from the multipart upload info."},"uploadId":{"type":"string"}}},"OkResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}},"SubmitTracksRequest":{"type":"object","required":["tracks"],"properties":{"tracks":{"type":"array","items":{"$ref":"#/components/schemas/Track"},"description":"One entry per video source you want used. Use speaker labels from the mapping webhook or the job's `turns`."},"cuts":{"type":"array","items":{"$ref":"#/components/schemas/Cut"},"description":"Only when the job is already \"ready\" or \"complete\": the corrected cuts to save with the new mapping. Required then."},"removedRanges":{"type":"array","items":{"$ref":"#/components/schemas/TimeRange"},"description":"Optional, with `cuts`: corrected silence-removal spans."}}},"SubmitTracksResponse":{"type":"object","required":["jobId","cuts","success"],"properties":{"jobId":{"type":"string","format":"uuid"},"cuts":{"type":"array","items":{"$ref":"#/components/schemas/Cut"},"description":"The generated (or saved) cuts."},"success":{"type":"boolean","const":true}}},"CutPresetId":{"type":"string","enum":["relaxed","balanced","dynamic"],"description":"Editing pace: `relaxed` holds shots longer, `dynamic` cuts more often, `balanced` sits in between."},"RecutRequest":{"type":"object","properties":{"presetId":{"allOf":[{"$ref":"#/components/schemas/CutPresetId"}],"description":"The pace to cut with. Leave it out to keep the job's current pace and its styled sections, e.g. when only changing `silence`."},"scopes":{"type":"array","description":"Optional sections that use a different preset, in source time.","items":{"type":"object","required":["startMs","endMs","presetId"],"properties":{"startMs":{"type":"number","minimum":0},"endMs":{"type":"number"},"presetId":{"$ref":"#/components/schemas/CutPresetId"}}}},"silence":{"allOf":[{"$ref":"#/components/schemas/SilenceSettings"}],"description":"New silence-removal settings. `{ \"mode\": \"off\" }` turns it off; omit to keep the job's stored setting."}}},"RecutResponse":{"type":"object","required":["jobId","presetId","cuts","removedRanges","savedMs"],"properties":{"jobId":{"type":"string","format":"uuid"},"presetId":{"$ref":"#/components/schemas/CutPresetId"},"cuts":{"type":"array","items":{"$ref":"#/components/schemas/Cut"}},"removedRanges":{"type":"array","items":{"$ref":"#/components/schemas/TimeRange"}},"savedMs":{"type":"number","description":"How much shorter the output gets from silence removal."},"silence":{"$ref":"#/components/schemas/SilenceSettings"},"metrics":{"type":"object","description":"Pace statistics of the new cuts.","additionalProperties":true},"success":{"type":"boolean","const":true}}},"UpdateCutsRequest":{"type":"object","description":"Send `cuts`, `removedRanges`, or both.","properties":{"cuts":{"type":"array","items":{"$ref":"#/components/schemas/Cut"},"description":"Replaces the job's cuts."},"removedRanges":{"type":"array","items":{"$ref":"#/components/schemas/TimeRange"},"description":"Replaces the silence-removal spans, e.g. to restore a pause the automatic pass removed."}}},"SuccessResponse":{"type":"object","required":["jobId","success"],"properties":{"jobId":{"type":"string","format":"uuid"},"success":{"type":"boolean","const":true}}},"RetryRequest":{"type":"object","properties":{"diarizeSourceIds":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Optional new pick of sources for speaker detection. Changing it restarts the job from syncing. Use it when the failure says the picked sources had no speech."}}},"MappingWebhook":{"type":"object","required":["jobId","status","speakers"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"mapping"},"reviewUrl":{"type":"string","format":"uri","description":"Signed link to the WizCut editor, valid for 72 hours. Send a person here to confirm the cameras and review cuts."},"speakers":{"type":"array","items":{"type":"string"},"description":"Detected speakers, most talkative first."},"cameraMap":{"$ref":"#/components/schemas/CameraMap"}}},"ReadyWebhook":{"type":"object","required":["jobId","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"ready"},"reviewUrl":{"type":"string","format":"uri","description":"Signed editor link, valid for 72 hours."}}},"CompleteWebhook":{"type":"object","required":["jobId","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"complete"},"outputUrl":{"type":"string","format":"uri","description":"Download URL of the render, valid for seven days. GET /api/jobs/{jobId} always returns a fresh one."}}},"ApprovedWebhook":{"type":"object","required":["jobId","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"approved"},"outputUrl":{"type":"string","format":"uri","description":"Download URL of the render (seven-day link)."}}},"FailedWebhook":{"type":"object","required":["jobId","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"failed"},"error":{"type":"string","description":"Same message as the job's `error_message`. A file WizCut can't read is named by its label."},"failedAtStatus":{"type":"string","description":"The stage that failed, such as syncing, diarizing or rendering."}}}},"responses":{"Unauthorized":{"description":"Missing, invalid or revoked API key. Send `Authorization: Bearer wc_live_…`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unauthorized"}}}},"JobNotFound":{"description":"The job doesn't exist or doesn't belong to this API key's account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Job not found"}}}},"ServerError":{"description":"Unexpected server error. Safe to retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unexpected server error"}}}}},"parameters":{"JobId":{"name":"jobId","in":"path","required":true,"description":"The job ID returned by createJob.","schema":{"type":"string","format":"uuid"},"example":"3f2b8c1e-5d7a-4e9b-a1c2-0d4e6f8a9b10"},"SourceId":{"name":"sourceId","in":"path","required":true,"description":"A source ID: a key of `uploads` in the createJob response.","schema":{"type":"string","format":"uuid"},"example":"a1b2c3d4-0000-4000-8000-000000000001"}}}}