{"openapi":"3.1.0","info":{"title":"NexoClip API","version":"0.1.0"},"paths":{"/healthz":{"get":{"summary":"Healthz","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/readyz":{"get":{"summary":"Readyz","operationId":"readyz_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Readyz Readyz Get"}}}}}}},"/streams":{"get":{"tags":["streams"],"summary":"List Streams","operationId":"list_streams_streams_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/StreamResponse"},"type":"array","title":"Response List Streams Streams Get"}}}}}},"post":{"tags":["streams"],"summary":"Create Stream","description":"Ingest a VOD and schedule the rest of the pipeline in the background.\n\nThe ingest step (yt-dlp metadata + audio/video download) runs inline so\nthe response can carry the resulting Stream id. Transcription, detection,\ncutting, and variant generation fire as a BackgroundTask after the\nresponse goes out.","operationId":"create_stream_streams_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamCreateRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/streams/upload":{"post":{"tags":["streams"],"summary":"Upload Stream","description":"Ingest an uploaded video file (no yt-dlp). Streams the upload to a\ntempfile chunk-by-chunk so a 1 GB VOD doesn't get pinned in RAM.\n\nSame downstream contract as `POST /streams`: the response carries the\nnew Stream id and the rest of the pipeline (transcribe, detect, cut,\nvariants) runs as a BackgroundTask.","operationId":"upload_stream_streams_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_stream_streams_upload_post"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/streams/{stream_id}":{"get":{"tags":["streams"],"summary":"Get Stream","operationId":"get_stream_streams__stream_id__get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/streams/{stream_id}/candidates":{"get":{"tags":["streams"],"summary":"List Candidates","operationId":"list_candidates_streams__stream_id__candidates_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CandidateResponse"},"title":"Response List Candidates Streams  Stream Id  Candidates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/streams/{stream_id}/clips":{"get":{"tags":["streams"],"summary":"List Clips","operationId":"list_clips_streams__stream_id__clips_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClipResponse"},"title":"Response List Clips Streams  Stream Id  Clips Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/clips/{clip_id}":{"get":{"tags":["clips"],"summary":"Get Clip","operationId":"get_clip_clips__clip_id__get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["clips"],"summary":"Update Clip","operationId":"update_clip_clips__clip_id__patch","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/clips/{clip_id}/publish":{"post":{"tags":["clips"],"summary":"Publish Clip","description":"Enqueue one publish_job per connected account for this clip + variant.","operationId":"publish_clip_clips__clip_id__publish_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublishJobResponse"},"title":"Response Publish Clip Clips  Clip Id  Publish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/personas":{"get":{"tags":["personas"],"summary":"List Personas","operationId":"list_personas_personas_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PersonaResponse"},"type":"array","title":"Response List Personas Personas Get"}}}}}},"post":{"tags":["personas"],"summary":"Create Persona","operationId":"create_persona_personas_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonaCreateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonaResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/personas/{persona_id}":{"patch":{"tags":["personas"],"summary":"Update Persona","operationId":"update_persona_personas__persona_id__patch","parameters":[{"name":"persona_id","in":"path","required":true,"schema":{"type":"string","title":"Persona Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonaUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonaResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/llm-calls":{"get":{"tags":["llm-calls"],"summary":"List Llm Calls","operationId":"list_llm_calls_llm_calls_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LLMCallResponse"},"title":"Response List Llm Calls Llm Calls Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/logout":{"post":{"tags":["dashboard"],"summary":"Logout","description":"Clear our session cookie + bounce to nexo-ai. No in-house login\npage exists anymore for the redirect to land on.","operationId":"logout_dashboard_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/streams":{"get":{"tags":["dashboard"],"summary":"Streams List","operationId":"streams_list_dashboard_streams_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Streams Create","operationId":"streams_create_dashboard_streams_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_streams_create_dashboard_streams_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/upload":{"post":{"tags":["dashboard"],"summary":"Streams Upload","description":"Ingest an operator-uploaded video file. The simpler path that sidesteps\nyt-dlp / Kick auth entirely — the operator drops a recorded VOD on the\npage and the pipeline runs against it.","operationId":"streams_upload_dashboard_streams_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_streams_upload_dashboard_streams_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/progress":{"get":{"tags":["dashboard"],"summary":"Stream Progress","description":"HTMX poll target — renders the live pipeline-progress card.\n\nReads `pipeline.step.{start,done,failed}` events for this stream and\nsurfaces what's running right now plus what's already finished. The\nparent stream-detail page polls this every 3s while the pipeline is\nin flight.","operationId":"stream_progress_dashboard_streams__stream_id__progress_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}":{"get":{"tags":["dashboard"],"summary":"Stream Detail","operationId":"stream_detail_dashboard_streams__stream_id__get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish":{"get":{"tags":["dashboard"],"summary":"Publish View","description":"Slice O.8 — global Publish page.\n\nAggregates every approved (or published) clip across every stream\nfor the current tenant into one matrix. Replaces the legacy\nAccounts nav tab; account management still happens inline via the\nchip strip + modal at the top.","operationId":"publish_view_dashboard_publish_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Publish Submit","description":"Slice O.8 — accept the global publish form.\n\nSame form shape as the per-stream POST: `clip_<id>_<platform>=1`\nticks + `meta_<id>_<platform>_<field>` overrides. We just look up\neach clip's stream on demand instead of getting it from the URL.","operationId":"publish_submit_dashboard_publish_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/status.json":{"get":{"tags":["dashboard"],"summary":"Publish Status Json","description":"Slice O.8 — tenant-wide publish status feed for the global page.\n\nSame payload shape as the per-stream endpoint so the JS poller\ncan be shared verbatim — same `cells` dict keyed `<clip>__<platform>`,\nsame `failures` list, same `summary` counts.","operationId":"publish_status_json_dashboard_publish_status_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/streams/{stream_id}/publish":{"get":{"tags":["dashboard"],"summary":"Stream Publish View","description":"Slice O.2 — publishing page.\n\nThe page after Approve. Shows every approved/published clip on\nthis stream alongside the tenant's connected social accounts,\nand lets the operator pick which clips ship to which platforms.\n\nLists pre-existing publish_jobs per clip so the operator sees\nwhat's already queued / sent / failed — `Publish to TikTok`\nbecomes greyed out for clips that already have a TikTok job.","operationId":"stream_publish_view_dashboard_streams__stream_id__publish_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["dashboard"],"summary":"Stream Publish Submit","description":"Slice O.2 — accept the publish form.\n\nThe form posts a flat list of `clip_<clip_id>_<platform>=1` flags\n(one per checkbox the operator ticked). For each tick we create\na `publish_jobs` row in `pending` state; the existing publish\nworker drains them on its next pass.","operationId":"stream_publish_submit_dashboard_streams__stream_id__publish_post","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/publish-status.json":{"get":{"tags":["dashboard"],"summary":"Stream Publish Status Json","description":"Slice O.5 — live publish-status polling for the matrix view.\n\nReturns a flat dict keyed `<clip_id>__<platform>` → job status\nstring (\"pending\" / \"running\" / \"sent\" / \"failed\"). The publish\npage polls this every few seconds and rewrites the matrix cells\nin place so the operator sees progress without a hard refresh.\n\nAlso returns `summary` (counts per status) so the page header\ncan show \"3 sent · 1 failed · 2 pending\" rolling totals.","operationId":"stream_publish_status_json_dashboard_streams__stream_id__publish_status_json_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/rerun":{"post":{"tags":["dashboard"],"summary":"Streams Rerun","description":"Re-trigger the pipeline for an already-ingested stream.\n\nUseful when (a) the original background task died with a server restart,\n(b) the user uploaded before the step-event tracking landed, or (c) they\njust want to rebuild clips after editing config. Each pipeline step is\nidempotent on its own output, so this is safe to call repeatedly.","operationId":"streams_rerun_dashboard_streams__stream_id__rerun_post","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_streams_rerun_dashboard_streams__stream_id__rerun_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}":{"get":{"tags":["dashboard"],"summary":"Clip Detail","operationId":"clip_detail_dashboard_clips__clip_id__get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/apply-ai-fixes":{"post":{"tags":["dashboard"],"summary":"Clip Apply Ai Fixes","description":"Slice I.3 + N.1 — apply non-destructive AI-recommended fixes.\n\nRe-runs `apply_ai_fixes` against the clip's current overlay_config,\nwrites the updated dict back, and (N.1) recomputes publishability\nbefore + after so the dashboard can animate the retention score\nmoving up. Also auto-promotes a clip from `cut` to\n`ready_for_review` when the post-fix score crosses the\npublish-ready threshold (75) — operator's \"Auto fix & optimize\"\nbutton should produce a tangible status change, not just a\nsilent toast.","operationId":"clip_apply_ai_fixes_dashboard_clips__clip_id__apply_ai_fixes_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/me/brand-kit-prefs":{"post":{"tags":["dashboard"],"summary":"Me Brand Kit Prefs","description":"Slice H.1 — auto-save endpoint for the editor's right panel.\n\nThe clip-editor JS debounce-calls this on every form change so the\noperator's setup (URL, banner toggle, caption position / font size\n/ animation / lead-time, etc.) lands in the tenant's default\nbrand_kit immediately. The brand_kit becomes the canonical source\nof truth for user-level setup; every new clip's editor reads from\nit on render.\n\nBody is a single-field JSON patch:\n\n    { \"field\": \"banner_url\",  \"value\": \"aldovillanueva\" }\n    { \"field\": \"banner_enabled\", \"value\": true }\n    { \"field\": \"captions_lead_ms\", \"value\": 150 }\n\nReturns `{\"ok\": true}` on success. Failures return a 400 so the JS\ncan surface the error inline without breaking the page.\n\nSINGLE source of truth: this endpoint and the existing\n`_persist_branding_to_brand_kit` (which fires on Save Draft /\nFinalize) both write to the same brand_kit columns — no risk of\ndrift because the brand_kit row is the only writer destination.","operationId":"me_brand_kit_prefs_dashboard_me_brand_kit_prefs_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/clips/{clip_id}/overlay":{"post":{"tags":["dashboard"],"summary":"Clip Overlay Save","description":"Save (only) the per-clip overlay config. Idempotent — re-POSTing\noverwrites. Used by the \"Save draft\" button on the clip editor.","operationId":"clip_overlay_save_dashboard_clips__clip_id__overlay_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_clip_overlay_save_dashboard_clips__clip_id__overlay_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/finalize":{"post":{"tags":["dashboard"],"summary":"Clip Overlay Finalize","description":"The 'Complete' button — saves the overlay config AND transitions\nthe clip from `cut` / `ready_for_review` → `approved` (the existing\npre-publish standby state).\n\nRejects when the current status doesn't allow the transition (e.g.\na `published` clip). The dashboard hides the button in those cases\nso the 409 only fires on a stale page.","operationId":"clip_overlay_finalize_dashboard_clips__clip_id__finalize_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_clip_overlay_finalize_dashboard_clips__clip_id__finalize_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/reject":{"post":{"tags":["dashboard"],"summary":"Clip Reject","description":"Slice F.7-H — one-click reject + close editor.\n\nForces the clip into `rejected` (any valid in-bound transition is\naccepted; we don't fight the operator's intent), emits the\n`clip.rejected` event for the audit trail, and redirects the\nbrowser back to the stream page so the editor closes immediately.\nThe stream-page clip card shows the REJECTED stamp via its\n`data-status=\"rejected\"` overlay.","operationId":"clip_reject_dashboard_clips__clip_id__reject_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/generate-hooks":{"post":{"tags":["dashboard"],"summary":"Clip Generate Hooks","description":"Generate `n` viral-hook title candidates for a clip and return\nthem as JSON for the editor's hook-picker UI to render inline.\n\nDriven by a tone preset (default / aggressive / gen_z / corporate /\ncurious) so the operator can sweep approaches without re-typing\nthe prompt. Each call is a single Anthropic request — cost-tracked\nby the LLMRouter under purpose='hook_generation'.\n\nReturns:\n    JSON: {\"hooks\": [{\"text\": \"...\"}, ...], \"tone\": \"...\", \"n\": N}\n    on success.\n    502 on provider failure with the LLM error inline.","operationId":"clip_generate_hooks_dashboard_clips__clip_id__generate_hooks_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_clip_generate_hooks_dashboard_clips__clip_id__generate_hooks_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/download-approved":{"get":{"tags":["dashboard"],"summary":"Stream Download Approved","description":"Slice O.1 — bulk extract: zip every approved/published clip\non this stream into a single download. Operator's \"Download all\"\nflow at the top of the Clips section.\n\nBuilds the zip in-memory (clips are short, the entire bundle\nrarely exceeds a few hundred MB). For a stream with hundreds of\nfinalized clips this would warrant streaming-zip-on-disk, but\nthat's a J.2 follow-up — short-form clip pipelines max out\naround 20-40 clips per stream.","operationId":"stream_download_approved_dashboard_streams__stream_id__download_approved_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/download":{"get":{"tags":["dashboard"],"summary":"Clip Download","description":"Slice O.21 — download the clip's headless-Chrome rendered MP4.\n\nReplaces the previous overlay_burn.py pipeline. The exported MP4 is\nnow produced by Playwright recording the `/clips/<id>/render` page\nat 1080×1920 native, then muxing the source clip's audio track\nlossless. By construction the output is pixel-identical to the\neditor preview — same browser engine renders both.\n\n`overlay_burn.py` is intentionally kept in the repo (slice O.21\ndecision) but no longer called from any endpoint. We retain it as\na fallback option in case Playwright fails at runtime — see the\nexcept branch below.\n\nCache: the recorder writes `clip_render.mp4` next to the source\nclip; subsequent downloads serve the cached file unless the\noperator saved new overlay settings (which deletes the cache via\n`clip_overlay_save`).","operationId":"clip_download_dashboard_clips__clip_id__download_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/render":{"get":{"tags":["dashboard"],"summary":"Clip Render View","description":"Slice O.19 — minimal render-mode page for headless capture.\n\nReturns a stripped-down HTML page rendering ONLY the .nc-preview\nframe at 1080×1920 native. No editor chrome (nav, sidebar, tabs,\ncontrols). Playwright (slice O.20) opens this URL at viewport\n1080×1920 and screen-records the playback. What you see here IS\nwhat gets exported — by construction, no separate burn pipeline.\n\nTenant-scoped: the underlying ClipsRepo.get filters by current\ntenant context, so cross-tenant clip-id guessing 404s.","operationId":"clip_render_view_dashboard_clips__clip_id__render_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/media":{"get":{"tags":["dashboard"],"summary":"Clip Media","description":"Stream the cut MP4 for inline <video> playback on the clip detail page.\n\nSlice M.6 — defaults to the ORIGINAL `clip.mp4` (no burns).\nOperator-reported bug: the editor was serving `clip_final.mp4`\n(which has captions BAKED INTO PIXELS) while the live preview\nALSO renders styled overlay captions on top — operators saw\nevery caption line twice: the burned plain-white one underneath\n+ the karaoke-styled overlay on top.\n\nThe editor's job is to compose; the burn is what we SHIP. To see\nthe burned-final pixels, pass `?source=final` (used only by the\n\"Final\" preview-mode tab, if/when wired). Everywhere else gets\nthe source so the styled overlays don't conflict with anything.\n\nReturns 404 if the clip row is missing or the on-disk file disappeared\n(e.g., out/ was nuked between runs). Tenant-bound so one tenant can't\nfetch another's clip even by guessing the id.","operationId":"clip_media_dashboard_clips__clip_id__media_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"original","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/thumbnail":{"get":{"tags":["dashboard"],"summary":"Clip Thumbnail","description":"Serve the per-clip thumbnail JPEG for the inbox clip cards.\n\nReturns 404 when the clip row has no `thumbnail_frame_path` (the\npipeline skipped the picker step) or when the file is gone from disk.","operationId":"clip_thumbnail_dashboard_clips__clip_id__thumbnail_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/intelligence.json":{"get":{"tags":["dashboard"],"summary":"Clip Intelligence","description":"Per-clip intelligence markers (audio peaks / scene cuts /\nlaughter reactions / chat-heat spikes / face-emotion changes).\n\nAggregated read-only across the existing transcripts +\nvisual_signals + chat_replay surfaces. Returns a flat\n`{markers: [{kind, ts, score, label}, ...]}` JSON shape — no\ncaching needed (the underlying tables are already indexed by\nstream_id and the aggregation is cheap).","operationId":"clip_intelligence_dashboard_clips__clip_id__intelligence_json_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/captions.json":{"get":{"tags":["dashboard"],"summary":"Clip Captions","description":"Word-level captions sliced to the clip window — slice F.7-F.\n\nReturns the same caption lines the renderer's ASS pass burns into\nthe MP4, so the live editor preview and the published clip\nmatch exactly. Each line carries:\n\n  - ts / end_ts (clip-relative seconds)\n  - text (joined line)\n  - words: [{ts, end_ts, text, emphasis}]\n  - emphasis: strongest emphasis tag on the line\n\nEmpty {lines: []} when the transcript has no word-level data\noverlapping the clip window — the preview gracefully renders the\nfallback placeholder copy in that case.","operationId":"clip_captions_dashboard_clips__clip_id__captions_json_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/clips/{clip_id}/waveform.json":{"get":{"tags":["dashboard"],"summary":"Clip Waveform","description":"Serve the per-clip audio waveform as a list of normalized peaks.\n\nComputed on first request via ffmpeg PCM extraction, then cached\nnext to the clip MP4 as `waveform.json` for instant subsequent\nloads. Returns `[]` (200, not 404) when extraction fails so the\neditor's scrubber gracefully degrades to a flat baseline rather\nthan spamming the console with 404s on every clip without audio.","operationId":"clip_waveform_dashboard_clips__clip_id__waveform_json_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/source":{"get":{"tags":["dashboard"],"summary":"Stream Source","description":"Serve the original uploaded/downloaded source video for preview.","operationId":"stream_source_dashboard_streams__stream_id__source_get","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/streams/{stream_id}/delete":{"post":{"tags":["dashboard"],"summary":"Stream Delete","description":"Slice O.11 — hard-delete a stream + cascade everything beneath it.\n\nTwo-phase: DB cascade first (FKs handle clips/candidates/variants/\npublish_jobs/etc), then best-effort filesystem cleanup of the\nper-stream output directory derived from `source_video_path`.\n\nBlocks deletion while the pipeline is running so we don't kneecap\na worker mid-flight (the cascade would yank rows it's still\nwriting to). Ingested / done / failed streams are fair game.","operationId":"stream_delete_dashboard_streams__stream_id__delete_post","parameters":[{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","title":"Stream Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/calibration":{"get":{"tags":["dashboard"],"summary":"Calibration View","description":"Per-platform rescore-vs-views calibration table.\n\nPearson r over the paired (rescore_score, views) values for each\nplatform's last 30 days. Surfaces the data the operator needs to\ndecide whether the scoring system has earned the right to auto-\npublish (see PHASE_3.md hard rules).","operationId":"calibration_view_dashboard_calibration_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/clips/{clip_id}/publish":{"post":{"tags":["dashboard"],"summary":"Clip Publish","operationId":"clip_publish_dashboard_clips__clip_id__publish_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_clip_publish_dashboard_clips__clip_id__publish_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish-jobs/{job_id}/cancel":{"post":{"tags":["dashboard"],"summary":"Publish Jobs Cancel","description":"Undo a pending auto-publish job (slice E.2).\n\nReturns 404 when the job doesn't exist OR belongs to another tenant\nOR is already past 'pending' (sent / failed). The dashboard hides\nthe Undo button in those cases so the 404 only fires on a stale page\nor a racing request — both safe to fail loudly on.","operationId":"publish_jobs_cancel_dashboard_publish_jobs__job_id__cancel_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_publish_jobs_cancel_dashboard_publish_jobs__job_id__cancel_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/inbox":{"get":{"tags":["dashboard"],"summary":"Inbox","description":"Operator inbox: clips grouped by VOD → speaker, plus a strip of\nin-window auto-publish jobs that can still be undone.\n\nDesigned to be the one page an operator opens each morning. The strip\nat the top is time-sensitive (auto-publish about to fire); the lists\nbelow are the regular review queue.","operationId":"inbox_dashboard_inbox_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/personas":{"get":{"tags":["dashboard"],"summary":"Personas List","operationId":"personas_list_dashboard_personas_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Personas Create","operationId":"personas_create_dashboard_personas_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_personas_create_dashboard_personas_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/personas/{persona_id}/edit":{"get":{"tags":["dashboard"],"summary":"Persona Edit Form","operationId":"persona_edit_form_dashboard_personas__persona_id__edit_get","parameters":[{"name":"persona_id","in":"path","required":true,"schema":{"type":"string","title":"Persona Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/personas/{persona_id}":{"post":{"tags":["dashboard"],"summary":"Persona Edit Submit","operationId":"persona_edit_submit_dashboard_personas__persona_id__post","parameters":[{"name":"persona_id","in":"path","required":true,"schema":{"type":"string","title":"Persona Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_persona_edit_submit_dashboard_personas__persona_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/connected-accounts":{"get":{"tags":["dashboard"],"summary":"Accounts List","operationId":"accounts_list_dashboard_connected_accounts_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Accounts Create","description":"Slice O.3 — accept a `redirect_to` form field so the inline\n\"Connect TikTok\" modal on the stream publish page can drop the\noperator right back on the matrix view after they finish the\nconnection (instead of bouncing them to the standalone\n/connected-accounts page). Defaults to the legacy redirect for\nrequests that don't pass it.","operationId":"accounts_create_dashboard_connected_accounts_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_accounts_create_dashboard_connected_accounts_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/connected-accounts/{account_id}/update":{"post":{"tags":["dashboard"],"summary":"Accounts Update","description":"Slice O.7 — edit an existing connected account.\n\nCalled from the \"Edit connection\" modal on the publish page when\nthe operator clicks an already-connected platform chip. Partial\nupdate: blank `access_token` keeps the existing one (so the\noperator can rename without re-pasting a long-lived secret).","operationId":"accounts_update_dashboard_connected_accounts__account_id__update_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"string","title":"Account Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_accounts_update_dashboard_connected_accounts__account_id__update_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/connected-accounts/{account_id}/disconnect":{"post":{"tags":["dashboard"],"summary":"Accounts Disconnect","description":"Slice O.7 — mark a connection as `disabled`.\n\nWe don't hard-delete because there may be publish_jobs that point\nat it; the schema declares ON DELETE RESTRICT for that exact reason.\nDisabling hides it from the publish UI without orphaning anything.","operationId":"accounts_disconnect_dashboard_connected_accounts__account_id__disconnect_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"string","title":"Account Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_accounts_disconnect_dashboard_connected_accounts__account_id__disconnect_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish-jobs/{job_id}/retry":{"post":{"tags":["dashboard"],"summary":"Publish Job Retry","description":"Slice O.7 — flip a failed job back to `pending`.\n\nSurfaced from the publish-page failures panel (\"Retry\" button next\nto each error). Doesn't reset the attempts counter — we want the\nhistory visible so chronic failures are spottable.","operationId":"publish_job_retry_dashboard_publish_jobs__job_id__retry_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_publish_job_retry_dashboard_publish_jobs__job_id__retry_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/llm-calls":{"get":{"tags":["dashboard"],"summary":"Llm Calls View","operationId":"llm_calls_view_dashboard_llm_calls_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/settings/llm":{"get":{"tags":["dashboard"],"summary":"Llm Settings View","operationId":"llm_settings_view_dashboard_settings_llm_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/brand-kits":{"get":{"tags":["dashboard"],"summary":"Brand Kits List","operationId":"brand_kits_list_dashboard_brand_kits_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Brand Kits Create","description":"Create a brand kit. Phrases come in as newline-separated textareas;\ncolor pickers and checkboxes ride the standard HTML form contract.","operationId":"brand_kits_create_dashboard_brand_kits_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_brand_kits_create_dashboard_brand_kits_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/new":{"get":{"tags":["dashboard"],"summary":"Brand Kits New Form","operationId":"brand_kits_new_form_dashboard_brand_kits_new_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/brand-kits/{kit_id}":{"get":{"tags":["dashboard"],"summary":"Brand Kits Edit Form","operationId":"brand_kits_edit_form_dashboard_brand_kits__kit_id__get","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["dashboard"],"summary":"Brand Kits Edit Submit","operationId":"brand_kits_edit_submit_dashboard_brand_kits__kit_id__post","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_brand_kits_edit_submit_dashboard_brand_kits__kit_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/{kit_id}/default":{"post":{"tags":["dashboard"],"summary":"Brand Kits Set Default","operationId":"brand_kits_set_default_dashboard_brand_kits__kit_id__default_post","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/{kit_id}/delete":{"post":{"tags":["dashboard"],"summary":"Brand Kits Delete","operationId":"brand_kits_delete_dashboard_brand_kits__kit_id__delete_post","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/{kit_id}/generate-logo":{"post":{"tags":["dashboard"],"summary":"Brand Kits Generate Logo","description":"Call Claude → sanitized SVG → save under brand_kits/<kit_id>/ →\npersist the relative URL + ai_* metadata on the brand kit row.\n\nErrors bubble up as a 500 with the provider message; the dashboard's\nnext render shows the previously-generated logo unchanged (best-effort\nsemantics — the kit isn't mutated until both the LLM call AND the disk\nwrite succeed).","operationId":"brand_kits_generate_logo_dashboard_brand_kits__kit_id__generate_logo_post","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_brand_kits_generate_logo_dashboard_brand_kits__kit_id__generate_logo_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/{kit_id}/logo.svg":{"get":{"tags":["dashboard"],"summary":"Brand Kits Logo Svg","description":"Serve the saved SVG for the dashboard preview + downstream renderer.","operationId":"brand_kits_logo_svg_dashboard_brand_kits__kit_id__logo_svg_get","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/brand-kits/{kit_id}/logo.png":{"get":{"tags":["dashboard"],"summary":"Brand Kits Logo Png","description":"Serve the rasterized PNG (when cairosvg was installed at generate-time).\n\nFalls through to 404 when the SVG was generated but rasterization was\nskipped — the dashboard's `<img>` fallback covers that case.","operationId":"brand_kits_logo_png_dashboard_brand_kits__kit_id__logo_png_get","parameters":[{"name":"kit_id","in":"path","required":true,"schema":{"type":"string","title":"Kit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks":{"get":{"tags":["webhooks"],"summary":"List Webhooks","operationId":"list_webhooks_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WebhookResponse"},"type":"array","title":"Response List Webhooks Webhooks Get"}}}}}},"post":{"tags":["webhooks"],"summary":"Create Webhook","description":"Register a subscriber URL. Secret is minted now and shown once.","operationId":"create_webhook_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/{sub_id}":{"delete":{"tags":["webhooks"],"summary":"Delete Webhook","description":"Idempotent delete: 204 if the row was there, 404 if it wasn't.","operationId":"delete_webhook_webhooks__sub_id__delete","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/{sub_id}/rotate-secret":{"post":{"tags":["webhooks"],"summary":"Rotate Secret","description":"Rotate the subscription's HMAC secret.\n\nThe prior secret stays honored for `grace_s` (default 24h) so\nsubscribers can update without missing deliveries. The new secret is\nreturned ONCE; subsequent reads only show its existence, not the value.","operationId":"rotate_secret_webhooks__sub_id__rotate_secret_post","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateSecretRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateSecretResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/{sub_id}/secrets":{"get":{"tags":["webhooks"],"summary":"List Active Secrets","description":"Past secrets still inside the rotation grace window.\n\nThe *current* secret is on the subscription itself and is shown only\nat create / rotate time; this endpoint specifically lists rotated-out\nsecrets that subscribers may still see in the wild during the window.","operationId":"list_active_secrets_webhooks__sub_id__secrets_get","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ActiveSecretResponse"},"title":"Response List Active Secrets Webhooks  Sub Id  Secrets Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/overlay/kick":{"get":{"tags":["overlay"],"summary":"Overlay Kick","description":"Render the Kick repost-page overlay as a standalone HTML page.","operationId":"overlay_kick_overlay_kick_get","parameters":[{"name":"channelId","in":"query","required":false,"schema":{"type":"string","maxLength":64,"description":"User-configured Kick channel id. Drives the KICK.COM/<channelId> text in the URL bar.","default":"yourhandle","title":"Channelid"},"description":"User-configured Kick channel id. Drives the KICK.COM/<channelId> text in the URL bar."},{"name":"handle","in":"query","required":false,"schema":{"type":"string","maxLength":64,"description":"Follow-card @handle. Defaults to NexoClip's own Kick handle so the card always reads correctly even when the operator hasn't set their own.","default":"reelonkick","title":"Handle"},"description":"Follow-card @handle. Defaults to NexoClip's own Kick handle so the card always reads correctly even when the operator hasn't set their own."},{"name":"followLabel","in":"query","required":false,"schema":{"type":"string","maxLength":24,"description":"Text on the green Follow pill.","default":"Follow","title":"Followlabel"},"description":"Text on the green Follow pill."},{"name":"scale","in":"query","required":false,"schema":{"type":"number","maximum":2.0,"minimum":0.5,"description":"Overall scale factor — multiplies every dimension via the --kb-scale CSS variable. Useful for OBS scenes that want the banner at non-1080p resolutions.","default":1.0,"title":"Scale"},"description":"Overall scale factor — multiplies every dimension via the --kb-scale CSS variable. Useful for OBS scenes that want the banner at non-1080p resolutions."},{"name":"starfield","in":"query","required":false,"schema":{"type":"integer","maximum":1,"minimum":0,"description":"1 to render the preview starfield background; OMIT for production/OBS use where the body must stay transparent.","default":0,"title":"Starfield"},"description":"1 to render the preview starfield background; OMIT for production/OBS use where the body must stay transparent."}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/tenants":{"post":{"tags":["nexo_ai"],"summary":"Provision Tenant","description":"Idempotent tenant provisioning called by Nexo AI.","operationId":"provision_tenant_api_admin_tenants_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ProvisionRequest"}}}},"responses":{"200":{"description":"Tenant already existed (duplicate)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ProvisionResponse"}}}},"201":{"description":"Tenant created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ProvisionResponse"}}}},"401":{"description":"Missing admin bearer"},"403":{"description":"Invalid admin bearer"},"503":{"description":"Nexo AI integration not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/tenants/{tenant_id}/status":{"post":{"tags":["nexo_ai"],"summary":"Set Tenant Status","description":"Pause / resume a tenant remotely. Called by Nexo AI when a PRO\nsubscriber swaps their live engine (the OLD engine — this one — is told\nto pause so the user can't run two engines on a single-slot plan).","operationId":"set_tenant_status_api_admin_tenants__tenant_id__status_post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_StatusRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"204":{"description":"Status updated (or already at requested value)"},"401":{"description":"Missing admin bearer"},"403":{"description":"Invalid admin bearer"},"404":{"description":"Tenant not found"},"503":{"description":"Nexo AI integration not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sso":{"get":{"tags":["nexo_ai"],"summary":"Sso Finalize","description":"Verify a Nexo AI-signed token (or trust it unsigned), set the\nsession cookie, redirect home.\n\nSlice O.22 — when `NEXO_AI_SSO_SECRET` is unset on this NexoClip\ninstance, we treat that as \"no wall\" mode: decode the payload\nwithout HMAC verification and trust the tenant_id it claims.\nThis is the explicit-opt-in lax mode — operator's call. With the\nsecret set, strict HMAC verify resumes (production hardening).\n\nExpiry check is also relaxed in lax mode (7-day leeway) so a token\nminted by nexo-ai a few minutes ago still works if the user opens\nNexoClip later from the same tab.","operationId":"sso_finalize_auth_sso_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"303":{"description":"Token valid; cookie set; redirected to dashboard"},"400":{"description":"Missing or malformed token"},"401":{"description":"Token signature failed or expired"},"503":{"description":"Nexo AI SSO not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ActiveSecretResponse":{"properties":{"id":{"type":"string","title":"Id"},"expires_at":{"type":"string","title":"Expires At"},"created_at":{"type":"string","title":"Created At"},"secret":{"type":"string","title":"Secret"}},"type":"object","required":["id","expires_at","created_at","secret"],"title":"ActiveSecretResponse","description":"One past secret still inside its grace window."},"Body_accounts_create_dashboard_connected_accounts_post":{"properties":{"platform":{"type":"string","title":"Platform"},"external_id":{"type":"string","title":"External Id"},"display_name":{"type":"string","title":"Display Name","default":""},"access_token":{"type":"string","title":"Access Token"},"redirect_to":{"type":"string","title":"Redirect To","default":""}},"type":"object","required":["platform","external_id","access_token"],"title":"Body_accounts_create_dashboard_connected_accounts_post"},"Body_accounts_disconnect_dashboard_connected_accounts__account_id__disconnect_post":{"properties":{"redirect_to":{"type":"string","title":"Redirect To","default":""}},"type":"object","title":"Body_accounts_disconnect_dashboard_connected_accounts__account_id__disconnect_post"},"Body_accounts_update_dashboard_connected_accounts__account_id__update_post":{"properties":{"external_id":{"type":"string","title":"External Id","default":""},"display_name":{"type":"string","title":"Display Name","default":""},"access_token":{"type":"string","title":"Access Token","default":""},"redirect_to":{"type":"string","title":"Redirect To","default":""}},"type":"object","title":"Body_accounts_update_dashboard_connected_accounts__account_id__update_post"},"Body_brand_kits_create_dashboard_brand_kits_post":{"properties":{"name":{"type":"string","title":"Name"},"primary_color":{"type":"string","title":"Primary Color"},"accent_color":{"type":"string","title":"Accent Color"},"text_color":{"type":"string","title":"Text Color","default":"#FFFFFF"},"font_family":{"type":"string","title":"Font Family","default":"Inter"},"font_weight":{"type":"integer","title":"Font Weight","default":800},"default_layout":{"type":"string","title":"Default Layout","default":"pip"},"is_default":{"type":"string","title":"Is Default","default":""},"handle_tiktok":{"type":"string","title":"Handle Tiktok","default":""},"handle_youtube":{"type":"string","title":"Handle Youtube","default":""},"handle_instagram":{"type":"string","title":"Handle Instagram","default":""},"handle_kick":{"type":"string","title":"Handle Kick","default":""},"caption_preset":{"type":"string","title":"Caption Preset","default":"karaoke_pop"},"auto_publish_enabled":{"type":"string","title":"Auto Publish Enabled","default":""},"auto_publish_platforms":{"type":"string","title":"Auto Publish Platforms","default":""},"auto_publish_delay_min":{"type":"integer","title":"Auto Publish Delay Min","default":60},"forward_phrases":{"type":"string","title":"Forward Phrases","default":""},"retroactive_phrases":{"type":"string","title":"Retroactive Phrases","default":""}},"type":"object","required":["name","primary_color","accent_color"],"title":"Body_brand_kits_create_dashboard_brand_kits_post"},"Body_brand_kits_edit_submit_dashboard_brand_kits__kit_id__post":{"properties":{"name":{"type":"string","title":"Name"},"primary_color":{"type":"string","title":"Primary Color"},"accent_color":{"type":"string","title":"Accent Color"},"text_color":{"type":"string","title":"Text Color","default":"#FFFFFF"},"font_family":{"type":"string","title":"Font Family","default":"Inter"},"font_weight":{"type":"integer","title":"Font Weight","default":800},"default_layout":{"type":"string","title":"Default Layout","default":"pip"},"caption_preset":{"type":"string","title":"Caption Preset","default":"karaoke_pop"},"handle_tiktok":{"type":"string","title":"Handle Tiktok","default":""},"handle_youtube":{"type":"string","title":"Handle Youtube","default":""},"handle_instagram":{"type":"string","title":"Handle Instagram","default":""},"handle_kick":{"type":"string","title":"Handle Kick","default":""},"auto_publish_enabled":{"type":"string","title":"Auto Publish Enabled","default":""},"auto_publish_platforms":{"type":"string","title":"Auto Publish Platforms","default":""},"auto_publish_delay_min":{"type":"integer","title":"Auto Publish Delay Min","default":60},"forward_phrases":{"type":"string","title":"Forward Phrases","default":""},"retroactive_phrases":{"type":"string","title":"Retroactive Phrases","default":""}},"type":"object","required":["name","primary_color","accent_color"],"title":"Body_brand_kits_edit_submit_dashboard_brand_kits__kit_id__post"},"Body_brand_kits_generate_logo_dashboard_brand_kits__kit_id__generate_logo_post":{"properties":{"style_hint":{"type":"string","title":"Style Hint","default":"Minimal mark / monogram"}},"type":"object","title":"Body_brand_kits_generate_logo_dashboard_brand_kits__kit_id__generate_logo_post"},"Body_clip_generate_hooks_dashboard_clips__clip_id__generate_hooks_post":{"properties":{"tone":{"type":"string","title":"Tone","default":"default"},"n":{"type":"integer","title":"N","default":5},"persona_id":{"type":"string","title":"Persona Id","default":""}},"type":"object","title":"Body_clip_generate_hooks_dashboard_clips__clip_id__generate_hooks_post"},"Body_clip_overlay_finalize_dashboard_clips__clip_id__finalize_post":{"properties":{"title_text":{"type":"string","title":"Title Text","default":""},"banner_enabled":{"type":"string","title":"Banner Enabled","default":""},"banner_platform":{"type":"string","title":"Banner Platform","default":"kick"},"banner_url":{"type":"string","title":"Banner Url","default":""},"banner_color":{"type":"string","title":"Banner Color","default":""},"banner_show_context":{"type":"string","title":"Banner Show Context","default":""},"banner_show_safezones":{"type":"string","title":"Banner Show Safezones","default":""},"captions_enabled":{"type":"string","title":"Captions Enabled","default":""},"captions_preset":{"type":"string","title":"Captions Preset","default":""},"captions_highlight_color":{"type":"string","title":"Captions Highlight Color","default":""},"captions_position":{"type":"string","title":"Captions Position","default":""},"captions_font_size":{"type":"string","title":"Captions Font Size","default":""},"captions_animation":{"type":"string","title":"Captions Animation","default":""},"captions_lead_ms":{"type":"integer","title":"Captions Lead Ms","default":120},"clip_style":{"type":"string","title":"Clip Style","default":""},"banner_variant":{"type":"string","title":"Banner Variant","default":""},"banner_live_badge":{"type":"string","title":"Banner Live Badge","default":""},"top_hook_enabled":{"type":"string","title":"Top Hook Enabled","default":""},"top_hook_text":{"type":"string","title":"Top Hook Text","default":""},"top_hook_style":{"type":"string","title":"Top Hook Style","default":""},"platform_overlay_preview":{"type":"string","title":"Platform Overlay Preview","default":""},"safe_zone_platform":{"type":"string","title":"Safe Zone Platform","default":""},"preview_mode":{"type":"string","title":"Preview Mode","default":""},"target_platform":{"type":"string","title":"Target Platform","default":""},"comments_show":{"type":"string","title":"Comments Show","default":""},"comments_fake_likes":{"type":"integer","title":"Comments Fake Likes","default":0}},"type":"object","title":"Body_clip_overlay_finalize_dashboard_clips__clip_id__finalize_post"},"Body_clip_overlay_save_dashboard_clips__clip_id__overlay_post":{"properties":{"title_text":{"type":"string","title":"Title Text","default":""},"banner_enabled":{"type":"string","title":"Banner Enabled","default":""},"banner_platform":{"type":"string","title":"Banner Platform","default":"kick"},"banner_url":{"type":"string","title":"Banner Url","default":""},"banner_color":{"type":"string","title":"Banner Color","default":""},"banner_show_context":{"type":"string","title":"Banner Show Context","default":""},"banner_show_safezones":{"type":"string","title":"Banner Show Safezones","default":""},"captions_enabled":{"type":"string","title":"Captions Enabled","default":""},"captions_preset":{"type":"string","title":"Captions Preset","default":""},"captions_highlight_color":{"type":"string","title":"Captions Highlight Color","default":""},"captions_position":{"type":"string","title":"Captions Position","default":""},"captions_font_size":{"type":"string","title":"Captions Font Size","default":""},"captions_animation":{"type":"string","title":"Captions Animation","default":""},"captions_lead_ms":{"type":"integer","title":"Captions Lead Ms","default":120},"clip_style":{"type":"string","title":"Clip Style","default":""},"banner_variant":{"type":"string","title":"Banner Variant","default":""},"banner_live_badge":{"type":"string","title":"Banner Live Badge","default":""},"top_hook_enabled":{"type":"string","title":"Top Hook Enabled","default":""},"top_hook_text":{"type":"string","title":"Top Hook Text","default":""},"top_hook_style":{"type":"string","title":"Top Hook Style","default":""},"platform_overlay_preview":{"type":"string","title":"Platform Overlay Preview","default":""},"safe_zone_platform":{"type":"string","title":"Safe Zone Platform","default":""},"preview_mode":{"type":"string","title":"Preview Mode","default":""},"target_platform":{"type":"string","title":"Target Platform","default":""},"comments_show":{"type":"string","title":"Comments Show","default":""},"comments_fake_likes":{"type":"integer","title":"Comments Fake Likes","default":0}},"type":"object","title":"Body_clip_overlay_save_dashboard_clips__clip_id__overlay_post"},"Body_clip_publish_dashboard_clips__clip_id__publish_post":{"properties":{"variant_id":{"type":"string","title":"Variant Id"}},"type":"object","required":["variant_id"],"title":"Body_clip_publish_dashboard_clips__clip_id__publish_post"},"Body_persona_edit_submit_dashboard_personas__persona_id__post":{"properties":{"name":{"type":"string","title":"Name"},"primary_language":{"type":"string","title":"Primary Language"},"voice_prompt":{"type":"string","title":"Voice Prompt"},"target_languages":{"type":"string","title":"Target Languages","default":""},"routing_tags":{"type":"string","title":"Routing Tags","default":""}},"type":"object","required":["name","primary_language","voice_prompt"],"title":"Body_persona_edit_submit_dashboard_personas__persona_id__post"},"Body_personas_create_dashboard_personas_post":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"primary_language":{"type":"string","title":"Primary Language"},"voice_prompt":{"type":"string","title":"Voice Prompt"},"target_languages":{"type":"string","title":"Target Languages","default":""},"routing_tags":{"type":"string","title":"Routing Tags","default":""}},"type":"object","required":["id","name","primary_language","voice_prompt"],"title":"Body_personas_create_dashboard_personas_post"},"Body_publish_job_retry_dashboard_publish_jobs__job_id__retry_post":{"properties":{"redirect_to":{"type":"string","title":"Redirect To","default":""}},"type":"object","title":"Body_publish_job_retry_dashboard_publish_jobs__job_id__retry_post"},"Body_publish_jobs_cancel_dashboard_publish_jobs__job_id__cancel_post":{"properties":{"redirect_to":{"type":"string","title":"Redirect To","default":"/dashboard"}},"type":"object","title":"Body_publish_jobs_cancel_dashboard_publish_jobs__job_id__cancel_post"},"Body_streams_create_dashboard_streams_post":{"properties":{"vod_url":{"type":"string","title":"Vod Url"},"persona_id":{"type":"string","title":"Persona Id"}},"type":"object","required":["vod_url","persona_id"],"title":"Body_streams_create_dashboard_streams_post"},"Body_streams_rerun_dashboard_streams__stream_id__rerun_post":{"properties":{"persona_id":{"type":"string","title":"Persona Id"}},"type":"object","required":["persona_id"],"title":"Body_streams_rerun_dashboard_streams__stream_id__rerun_post"},"Body_streams_upload_dashboard_streams_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"persona_id":{"type":"string","title":"Persona Id"}},"type":"object","required":["file","persona_id"],"title":"Body_streams_upload_dashboard_streams_upload_post"},"Body_upload_stream_streams_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"persona_id":{"type":"string","title":"Persona Id"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"}},"type":"object","required":["file","persona_id"],"title":"Body_upload_stream_streams_upload_post"},"CandidateResponse":{"properties":{"id":{"type":"string","title":"Id"},"stream_id":{"type":"string","title":"Stream Id"},"ts":{"type":"number","title":"Ts"},"score":{"type":"number","title":"Score"},"reason":{"type":"string","title":"Reason"},"evidence":{"additionalProperties":true,"type":"object","title":"Evidence"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","stream_id","ts","score","reason","created_at"],"title":"CandidateResponse"},"ClipResponse":{"properties":{"id":{"type":"string","title":"Id"},"stream_id":{"type":"string","title":"Stream Id"},"candidate_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Candidate Id"},"start_s":{"type":"number","title":"Start S"},"end_s":{"type":"number","title":"End S"},"duration_s":{"type":"number","title":"Duration S"},"width":{"type":"integer","title":"Width"},"height":{"type":"integer","title":"Height"},"path":{"type":"string","title":"Path"},"smart_crop_box":{"anyOf":[{"additionalProperties":{"type":"number"},"type":"object"},{"type":"null"}],"title":"Smart Crop Box"},"thumbnail_frame_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Frame Path"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","stream_id","start_s","end_s","duration_s","width","height","path","status","created_at"],"title":"ClipResponse"},"ClipUpdateRequest":{"properties":{"status":{"anyOf":[{"type":"string","enum":["cut","ready_for_review","approved","rejected","published"]},{"type":"null"}],"title":"Status"}},"additionalProperties":false,"type":"object","title":"ClipUpdateRequest","description":"Body for `PATCH /clips/{id}`. Only `status` is editable in Phase 1."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LLMCallResponse":{"properties":{"id":{"type":"string","title":"Id"},"purpose":{"type":"string","title":"Purpose"},"provider":{"type":"string","title":"Provider"},"model":{"type":"string","title":"Model"},"quality":{"type":"string","title":"Quality"},"input_tokens":{"type":"integer","title":"Input Tokens"},"output_tokens":{"type":"integer","title":"Output Tokens"},"cost_usd_micros":{"type":"integer","title":"Cost Usd Micros"},"status":{"type":"string","title":"Status"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"attempts":{"type":"integer","title":"Attempts"},"ts":{"type":"string","title":"Ts"}},"type":"object","required":["id","purpose","provider","model","quality","input_tokens","output_tokens","cost_usd_micros","status","attempts","ts"],"title":"LLMCallResponse"},"PersonaCreateRequest":{"properties":{"id":{"type":"string","minLength":1,"title":"Id"},"name":{"type":"string","minLength":1,"title":"Name"},"primary_language":{"type":"string","minLength":2,"title":"Primary Language"},"target_languages":{"items":{"type":"string"},"type":"array","title":"Target Languages"},"voice_prompt":{"type":"string","minLength":1,"title":"Voice Prompt"},"routing_tags":{"items":{"type":"string"},"type":"array","title":"Routing Tags"}},"additionalProperties":false,"type":"object","required":["id","name","primary_language","voice_prompt"],"title":"PersonaCreateRequest"},"PersonaResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"primary_language":{"type":"string","title":"Primary Language"},"target_languages":{"items":{"type":"string"},"type":"array","title":"Target Languages"},"voice_prompt":{"type":"string","title":"Voice Prompt"},"routing_tags":{"items":{"type":"string"},"type":"array","title":"Routing Tags"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","name","primary_language","target_languages","voice_prompt","routing_tags","created_at"],"title":"PersonaResponse"},"PersonaUpdateRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"primary_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Language"},"target_languages":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Target Languages"},"voice_prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Prompt"},"routing_tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Routing Tags"}},"additionalProperties":false,"type":"object","title":"PersonaUpdateRequest","description":"Body for `PATCH /personas/{id}`. Only set fields are updated."},"PublishJobResponse":{"properties":{"id":{"type":"string","title":"Id"},"clip_id":{"type":"string","title":"Clip Id"},"variant_id":{"type":"string","title":"Variant Id"},"account_id":{"type":"string","title":"Account Id"},"platform":{"type":"string","title":"Platform"},"status":{"type":"string","title":"Status"},"attempts":{"type":"integer","title":"Attempts"},"scheduled_for":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled For"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","clip_id","variant_id","account_id","platform","status","attempts","created_at"],"title":"PublishJobResponse"},"PublishRequest":{"properties":{"variant_id":{"type":"string","title":"Variant Id"},"account_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Account Ids"}},"additionalProperties":false,"type":"object","required":["variant_id"],"title":"PublishRequest","description":"Body for `POST /clips/{id}/publish`.\n\n`account_ids` defaults to \"every connected account on this tenant\" when\nomitted - the common case in Phase 1's single-tenant dashboard. The\npublisher's idempotency lives in Task 11."},"RotateSecretRequest":{"properties":{"grace_s":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Grace S","description":"Seconds the prior secret stays valid. Default: 24h."}},"additionalProperties":false,"type":"object","title":"RotateSecretRequest","description":"Optional override of the rotation grace window."},"RotateSecretResponse":{"properties":{"id":{"type":"string","title":"Id"},"secret":{"type":"string","title":"Secret"},"prior_secret_expires_at":{"type":"string","title":"Prior Secret Expires At"}},"type":"object","required":["id","secret","prior_secret_expires_at"],"title":"RotateSecretResponse","description":"Response from `POST /webhooks/{id}/rotate-secret`.\n\n`secret` is the new value (returned ONCE; mirrors api_tokens / create).\n`prior_secret_expires_at` is the deadline subscribers have to update\ntheir config before the previous secret stops being honored."},"StreamCreateRequest":{"properties":{"vod_url":{"type":"string","minLength":1,"title":"Vod Url"},"persona_id":{"type":"string","minLength":1,"title":"Persona Id"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"}},"additionalProperties":false,"type":"object","required":["vod_url","persona_id"],"title":"StreamCreateRequest","description":"Body for `POST /streams`."},"StreamResponse":{"properties":{"id":{"type":"string","title":"Id"},"vod_url":{"type":"string","title":"Vod Url"},"platform":{"type":"string","title":"Platform"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"duration_s":{"type":"number","title":"Duration S"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","vod_url","platform","duration_s","status","created_at"],"title":"StreamResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreateRequest":{"properties":{"url":{"type":"string","minLength":1,"title":"Url"},"types":{"items":{"type":"string"},"type":"array","title":"Types"}},"additionalProperties":false,"type":"object","required":["url"],"title":"WebhookCreateRequest"},"WebhookCreateResponse":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"types":{"items":{"type":"string"},"type":"array","title":"Types"},"secret":{"type":"string","title":"Secret"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","url","types","secret","status","created_at"],"title":"WebhookCreateResponse","description":"Response on create — includes the secret. Other reads omit it."},"WebhookResponse":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"types":{"items":{"type":"string"},"type":"array","title":"Types"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","title":"Created At"},"last_dispatch_ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Dispatch Ts"},"failure_count":{"type":"integer","title":"Failure Count","default":0}},"type":"object","required":["id","url","types","status","created_at"],"title":"WebhookResponse"},"_ProvisionRequest":{"properties":{"external_user_id":{"type":"string","maxLength":255,"minLength":1,"title":"External User Id"},"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"},"display_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Display Name"},"tier":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Tier"}},"type":"object","required":["external_user_id","email"],"title":"_ProvisionRequest","description":"Body shape from Nexo AI. See docs/nexo_ai_integration.md."},"_ProvisionResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"api_token":{"type":"string","title":"Api Token"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["tenant_id","api_token"],"title":"_ProvisionResponse","description":"Success body (200/201) and duplicate body (409) share this shape.\nThe `error` field is present ONLY on 409 to disambiguate."},"_StatusRequest":{"properties":{"status":{"type":"string","pattern":"^(active|paused|cancelled)$","title":"Status"}},"type":"object","required":["status"],"title":"_StatusRequest","description":"Body for the pause/resume endpoint. Slice NX.2 only uses 'active' and\n'paused' — 'cancelled' is reserved for future hard-stop (delete data)."}}}}