{"openapi":"3.1.0","info":{"title":"ChalyClip 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"}}}}}}},"/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 chalyb. 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 — mirrors the URL-job flow.\n\nPreviously this endpoint blocked the request on:\n  1. Receiving the upload bytes (unavoidable; HTTP request body)\n  2. ingest_uploaded() — file move + audio extract + ffprobe\n     (~30-60 s for a 1-hour video on Railway CPU)\n  3. Stream row upsert + event emit\n  4. Pipeline dispatch\n…then redirected. For a 500 MB upload the operator stared at a\nspinner for minutes before they could see the live progress page.\n\nNow (matches /dashboard/url-jobs):\n  1. Receive bytes (unavoidable)\n  2. Mint stream_id, insert placeholder StreamRow (status=\"pending\",\n     duration_s=0, canonical paths the runner will write to)\n  3. Emit stream.created\n  4. Schedule `upload_pipeline_runner` as a background task —\n     audio extract + transcribe + detect + cut all happen there\n  5. 303 to /dashboard/streams/{id} immediately\n\nThe live progress page reads the same event surface every other\nsource uses (stream.download.* + stream.audio_extracted +\npipeline.step.* + clip.cut.substep).","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/start":{"get":{"tags":["dashboard"],"summary":"Start Page","description":"Unified entry-point page — surface every ingest method (paste URL,\nupload, Drive watch, live RTMP) as cards on one screen so operators\naren't hunting through nav menus to start a job.\n\nPersona is auto-resolved to the tenant's first persona; per-run\noverride stays on the legacy /dashboard/streams advanced form for\noperators who need it.","operationId":"start_page_dashboard_start_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/url-jobs/new":{"get":{"tags":["dashboard"],"summary":"Url Job Form","description":"Backward-compat — bookmarked /dashboard/url-jobs/new redirects to\nthe unified /dashboard/start page (Task UX 9). The old single-field\ntemplate has been deleted.","operationId":"url_job_form_dashboard_url_jobs_new_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/url-jobs":{"post":{"tags":["dashboard"],"summary":"Url Job Create","description":"Create a URL ingest job (Task 3a).\n\nThe full pipeline (ingest → transcribe → detect → cut) runs as a\nbackground task. The response redirects to the existing stream detail\npage which polls progress + lists clips as they finish.","operationId":"url_job_create_dashboard_url_jobs_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_url_job_create_dashboard_url_jobs_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/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/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}/revert-trim":{"post":{"tags":["dashboard"],"summary":"Clip Revert Trim","description":"Slice G.4b — restore the clip to its original bounds.\n\nNo-op when the clip has never been trimmed. Otherwise re-cuts\nthe source video at the originals.","operationId":"clip_revert_trim_dashboard_clips__clip_id__revert_trim_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}/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}/clips/approve-all":{"post":{"tags":["dashboard"],"summary":"Stream Approve All Clips","description":"Bulk-approve every clip on a stream still awaiting review\n(cut / ready_for_review → approved), so the operator doesn't click\nthrough 40 clips one at a time.\n\nStatus-flip ONLY — it deliberately does NOT publish or render. Firing a\nrender per clip here would spawn dozens of Chromium/ffmpeg processes at\nonce and exhaust the box. After this, the Publish Center's\n\"Auto-programar\" schedules the now-approved clips (sequentially, capped).","operationId":"stream_approve_all_clips_dashboard_streams__stream_id__clips_approve_all_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}/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}/render-status":{"get":{"tags":["dashboard"],"summary":"Clip Render Status","description":"Render Migration T1 — JSON status feed for the polling UI.\n\nThe Download button hits this every ~1 s and renders a progress\nbar / \"Download MP4\" link / \"Failed — retry\" affordance based on\nthe response shape:\n\n  {state: \"idle\",     progress_pct: 0,   error: null}  → operator hasn't clicked yet\n  {state: \"rendering\",progress_pct: 42,  error: null}  → background task in flight\n  {state: \"ready\",    progress_pct: 100, error: null,  → cache file exists; UI swaps\n   download_url: \"...\"}                                 to a direct download link\n  {state: \"failed\",   progress_pct: ...,error: \"...\"}  → UI shows retry button\n\nFalls through to \"ready\" when the cache file is on disk but the\nDB column never got promoted (e.g. legacy clips rendered pre-T1\nor sweeper hasn't run). That keeps backward compat with any\nalready-rendered MP4 the operator's deploy might have.","operationId":"clip_render_status_dashboard_clips__clip_id__render_status_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}},{"name":"quality","in":"query","required":false,"schema":{"type":"string","default":"1080","title":"Quality"}}],"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-retry":{"post":{"tags":["dashboard"],"summary":"Clip Render Retry","description":"Render Migration T1 — UI \"Retry\" button on a failed render.\n\nResets clip.render_state='idle' AND unlinks any cached render at\nthe requested resolution so the next Download click forces a\nfresh background render. We don't enqueue the render here; the\ndownload endpoint is the single place that schedules\nBackgroundTasks. Operator clicks Retry → cache wiped + state\ngoes idle → they click Download again → background render\nstarts with the latest pipeline.\n\nRender Migration R17 — added the cache unlink. Without it, the\ncache-hit branch in clip_download fires before the state check\nand the operator gets the stale rendered file every time,\nregardless of how many Retry clicks land. R16's libass pipeline\n(and any future render-pipeline change) only ships if the cache\nactually misses.","operationId":"clip_render_retry_dashboard_clips__clip_id__render_retry_post","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}},{"name":"quality","in":"query","required":false,"schema":{"type":"string","default":"1080","title":"Quality"}}],"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.\nSlice J.2a — `quality` query param picks the export resolution\n(1080 / 2k / 4k). Free + pro tiers are capped at 1080; all_access\ncan pick higher and pays the extra GPU time via the outbound\nusage report.\n\nRender Migration T1 — was inline; now backgrounded.\n  - Cache hit (clip_render_<res>.mp4 on disk) → 200 FileResponse,\n    immediate download.\n  - Cache miss + state='idle' → flip state to 'rendering',\n    schedule background runner, return 202 JSON status.\n  - Cache miss + state='rendering' → 202 JSON status (the UI\n    polls /render-status every ~1 s for progress).\n  - Cache miss + state='failed' → 409 JSON status with error\n    message + retry hint (operator hits POST /render-retry to\n    reset to 'idle' then re-clicks Download).\n\nThe exported MP4 is produced by Playwright recording the\n`/clips/<id>/render` page at the chosen viewport, then muxing the\nsource clip's audio track lossless. By construction the output is\npixel-identical to the editor preview — same browser engine\nrenders both, just sized to the picked resolution.\n\nCache key includes the resolution so 1080 and 4K versions of the\nsame clip live as separate files — switching resolutions doesn't\ninvalidate the other's cache.","operationId":"clip_download_dashboard_clips__clip_id__download_get","parameters":[{"name":"clip_id","in":"path","required":true,"schema":{"type":"string","title":"Clip Id"}},{"name":"quality","in":"query","required":false,"schema":{"type":"string","default":"1080","title":"Quality"}}],"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\nDrift fix Step 2 — uses the shared _resolve_overlay_context helper\nso face_zone / zone_profile / banner_url / brand_kit threading\nmatches the editor's clip_detail handler bit-for-bit. The\n`_overlay_styles.html` and `_overlay_markup.html` partials in\nthis template's render carry the unified overlay CSS + markup\nthat previously had drifted between the two surfaces.\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/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/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/sources":{"get":{"tags":["dashboard"],"summary":"Sources List","description":"Connected channels — auto-ingest sources (YouTube/Twitch/Kick).","operationId":"sources_list_dashboard_sources_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["dashboard"],"summary":"Sources Add","operationId":"sources_add_dashboard_sources_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_sources_add_dashboard_sources_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/sources/{watch_id}/toggle":{"post":{"tags":["dashboard"],"summary":"Sources Toggle","operationId":"sources_toggle_dashboard_sources__watch_id__toggle_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"string","title":"Watch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/sources/{watch_id}/delete":{"post":{"tags":["dashboard"],"summary":"Sources Delete","operationId":"sources_delete_dashboard_sources__watch_id__delete_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"string","title":"Watch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/sources/{watch_id}/schedule":{"post":{"tags":["dashboard"],"summary":"Sources Schedule","description":"Change a channel's poll cadence (times/day). Clamped to 1-96.","operationId":"sources_schedule_dashboard_sources__watch_id__schedule_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"string","title":"Watch Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_sources_schedule_dashboard_sources__watch_id__schedule_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/sources/{watch_id}/poll":{"post":{"tags":["dashboard"],"summary":"Sources Poll Now","description":"Scan one watched channel for new VODs right now, ignoring its cadence.\n\nThe listing + per-VOD download can take minutes, so the poll runs as a\nbackground task and the page redirects immediately with a 'scanning'\nbanner; new VODs appear on the streams page as they finish. Uses the\nshared app DB + dispatcher (the request-scoped ones don't outlive the\nresponse) and `poll_channel_watches`' own per-tenant binding, scoped to\nthis single watch via `watch_id` + `respect_schedule=False`.","operationId":"sources_poll_now_dashboard_sources__watch_id__poll_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"string","title":"Watch 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":{"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"}}}}}}},"/dashboard/publish/zernio":{"get":{"tags":["zernio"],"summary":"Zernio Dashboard","description":"Publish Center — single-page surface with two tabs:\n  - Single Publish: thumbnail grid + selected-clip publish panel\n  - Bulk Publish: per-clip rows with inline platform chip toggles\n\nSections (top to bottom):\n  1. Compact \"Connected accounts\" row (one Connect button per platform)\n  2. Tab switcher\n  3. Active tab content\n  4. History feed at the bottom","operationId":"zernio_dashboard_dashboard_publish_zernio_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/publish/zernio/profile":{"post":{"tags":["zernio"],"summary":"Zernio Create Profile","description":"Create the tenant's Zernio profile (inline, AJAX from the\ndashboard). JSON body: {\"name\": \"...\", \"description\": \"...\"?}.\n\nCalls Zernio's POST /profiles, persists the returned profile id +\nname on the tenant row, and returns JSON so the dashboard can update\nin place without a navigation. Connecting social accounts to the new\nprofile is the next step (the per-platform Connect buttons).","operationId":"zernio_create_profile_dashboard_publish_zernio_profile_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/connect":{"post":{"tags":["zernio"],"summary":"Zernio Connect","description":"Mint a hosted-OAuth authUrl for ONE platform and return it as\nJSON so the dashboard opens it in a NEW TAB.\n\nWhy a new tab instead of a 303: Zernio's connect flow has no\nredirect-back parameter — after OAuth it lands the browser on\nZernio's own dashboard. Navigating our whole tab away would strand\nthe operator on Zernio. Opening Zernio in a separate tab keeps the\nChalyClip tab alive; when the operator returns to it, the dashboard\nrefreshes and shows the newly connected account.\n\nJSON body: {\"platform\": \"tiktok\"}. Requires the tenant's Zernio\nprofile to already exist (created via POST /profile); accounts\nattach to that profile.","operationId":"zernio_connect_dashboard_publish_zernio_connect_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/connected":{"get":{"tags":["zernio"],"summary":"Zernio Connected Landing","description":"Post-OAuth landing page — where Zernio's `redirect_url` sends the\npopup after the account connects.\n\nSame-origin, so `window.close()` works on the script-opened popup:\nnotify the opener (main ChalyClip tab) via postMessage (with the\nplatform, read client-side from the query string), then close. If\nthe page was somehow opened as a full navigation (no opener), fall\nback to the dashboard. The operator never sees Zernio's UI.\n\nHeadless branch: when the redirect carries selection state\n(`tempToken` without an `accountId`) for a platform we connect in\nheadless mode, the account is NOT created yet — render our own\npicker (Facebook page selection) instead of closing.","operationId":"zernio_connected_landing_dashboard_publish_zernio_connected_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/publish/zernio/fb-pages":{"post":{"tags":["zernio"],"summary":"Zernio Facebook Pages","description":"List the Facebook Pages the just-OAuth'd user can manage.\n\nPOST (not GET) so the short-lived `temp_token` travels in the body,\nnever in a URL that access logs would capture. The popup shares the\ndashboard's session cookie, so the normal tenant gates apply and\nthe profileId is resolved server-side — the client never picks it.","operationId":"zernio_facebook_pages_dashboard_publish_zernio_fb_pages_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/fb-page/select":{"post":{"tags":["zernio"],"summary":"Zernio Facebook Select Page","description":"Finish the headless Facebook connect: save the picked page.\n\nBody: {page_id, temp_token, user_profile_raw}. Same body-not-URL\nrule as /fb-pages — the temp token is an OAuth credential.","operationId":"zernio_facebook_select_page_dashboard_publish_zernio_fb_page_select_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/accounts-panel":{"get":{"tags":["zernio"],"summary":"Zernio Accounts Panel","description":"Server-rendered connected-accounts row (the same partial the\ndashboard includes). The connect popup's postMessage triggers a\nfetch of this and swaps it in — chips update without a reload,\nkeeping the limit/disconnect logic in ONE Jinja template.","operationId":"zernio_accounts_panel_dashboard_publish_zernio_accounts_panel_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/publish/zernio/schedule.json":{"get":{"tags":["zernio"],"summary":"Zernio Schedule Json","description":"Programación payload: recurring queue slots + the upcoming\n(scheduled/queued) posts list. Read-only; tenant-scoped via the\nprofile. Empty + ok when no profile/key so the panel renders a\nclean empty state instead of erroring.","operationId":"zernio_schedule_json_dashboard_publish_zernio_schedule_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/schedule/slots":{"post":{"tags":["zernio"],"summary":"Zernio Save Slots","description":"Replace the default queue's recurring slots.\n\nJSON body: {\"timezone\": \"America/Mexico_City\", \"slots\": [\n  {\"dayOfWeek\": 1, \"time\": \"09:00\"}, ...]}. dayOfWeek is\n0=Sunday..6=Saturday, time \"HH:mm\". Validated before the Zernio\ncall so a bad weekday/time is a clean 400, not a Zernio 400.","operationId":"zernio_save_slots_dashboard_publish_zernio_schedule_slots_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/schedule/queue/{queue_id}/delete":{"post":{"tags":["zernio"],"summary":"Zernio Delete Queue","description":"Delete one recurring queue.","operationId":"zernio_delete_queue_dashboard_publish_zernio_schedule_queue__queue_id__delete_post","parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","title":"Queue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/ads/campaigns.json":{"get":{"tags":["zernio"],"summary":"Zernio Ads Campaigns Json","description":"Read-only ad campaigns (404 when FEATURE_ADS is off). Shown next\nto organic metrics in Rendimiento.","operationId":"zernio_ads_campaigns_json_dashboard_publish_zernio_ads_campaigns_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/ads/boost":{"post":{"tags":["zernio"],"summary":"Zernio Ads Boost","description":"Boost a published clip into a paid ad (404 when FEATURE_ADS off).\nBehind a confirmation. Body: {account_id, ad_account_id, name, goal,\nbudget_amount, budget_type, post_id?/platform_post_id?, confirm}.","operationId":"zernio_ads_boost_dashboard_publish_zernio_ads_boost_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/whatsapp/status.json":{"get":{"tags":["zernio"],"summary":"Zernio Whatsapp Status Json","description":"WhatsApp number provisioning status (404 when FEATURE_WHATSAPP\noff). Fed by whatsapp.number.* webhooks.","operationId":"zernio_whatsapp_status_json_dashboard_publish_zernio_whatsapp_status_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/community/settings.json":{"get":{"tags":["zernio"],"summary":"Zernio Community Settings Json","description":"Current community-notification settings + the connected\nDiscord/Telegram accounts the operator can pick as the channel.","operationId":"zernio_community_settings_json_dashboard_publish_zernio_community_settings_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/community/settings":{"post":{"tags":["zernio"],"summary":"Zernio Community Save Settings","description":"Save the \"Avisar a mi comunidad\" toggle + channel + brand\nidentity + weekly-digest toggle. Body: {enabled, discord_account_id?,\ntelegram_account_id?, brand_name?, brand_avatar_url?, weekly_digest?}.","operationId":"zernio_community_save_settings_dashboard_publish_zernio_community_settings_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/autopublish.json":{"get":{"tags":["zernio"],"summary":"Zernio Autopublish Json","description":"Auto-publish settings + the connected platforms the operator can\ntarget + today's post count (for the daily-cap meter).","operationId":"zernio_autopublish_json_dashboard_publish_zernio_autopublish_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/autopublish/save":{"post":{"tags":["zernio"],"summary":"Zernio Autopublish Save","description":"Save auto-publish settings. Body: {enabled, mode, targets:[...],\npost_mode, daily_cap}. `hands_free` is accepted but does not fire yet\n(phase 2 — the pipeline hook isn't wired).","operationId":"zernio_autopublish_save_dashboard_publish_zernio_autopublish_save_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/autopublish/rules.json":{"get":{"tags":["zernio"],"summary":"Zernio Pacing Rules Json","description":"The effective per-platform rulebook (defaults overlaid with the tenant's\nsaved overrides). `overridden` flags which platforms the tenant has tuned.","operationId":"zernio_pacing_rules_json_dashboard_publish_zernio_autopublish_rules_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/autopublish/rules/save":{"post":{"tags":["zernio"],"summary":"Zernio Pacing Rules Save","description":"Upsert one platform's rule override, or reset it to the shipped default.\n\nBody: {platform, max_per_day, min_gap_minutes, caption_max_chars,\nhashtag_max, jitter_minutes, enabled} to save, or {platform, reset: true}\nto drop the override.","operationId":"zernio_pacing_rules_save_dashboard_publish_zernio_autopublish_rules_save_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/clips/{clip_id}/growth.json":{"get":{"tags":["zernio"],"summary":"Zernio Clip Growth Json","description":"The cached Growth Score card for one clip (the pre-publish panel).\n404 when the clip hasn't been scored yet.","operationId":"zernio_clip_growth_json_dashboard_publish_zernio_clips__clip_id__growth_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/publish/zernio/growth/contacts.json":{"get":{"tags":["zernio"],"summary":"Zernio Growth Contacts Json","description":"Contacts seeded from comments/DMs (phase 9), with optional tag\nfilter + free-text search over name/username.","operationId":"zernio_growth_contacts_json_dashboard_publish_zernio_growth_contacts_json_get","parameters":[{"name":"tag","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tag"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/growth/automations.json":{"get":{"tags":["zernio"],"summary":"Zernio Growth Automations Json","description":"List the tenant's comment-to-DM automations with stats.","operationId":"zernio_growth_automations_json_dashboard_publish_zernio_growth_automations_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/growth/automations":{"post":{"tags":["zernio"],"summary":"Zernio Growth Create Automation","description":"Create a comment-to-DM automation. IG/FB only (enforced). Body:\n{account_id, name, dm_message, keywords?, platform_post_id?,\npost_id?, comment_reply?}.","operationId":"zernio_growth_create_automation_dashboard_publish_zernio_growth_automations_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/growth/automations/{automation_id}/toggle":{"post":{"tags":["zernio"],"summary":"Zernio Growth Toggle Automation","description":"Activate/pause an automation. Body: {active: bool}.","operationId":"zernio_growth_toggle_automation_dashboard_publish_zernio_growth_automations__automation_id__toggle_post","parameters":[{"name":"automation_id","in":"path","required":true,"schema":{"type":"string","title":"Automation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/growth/sequences.json":{"get":{"tags":["zernio"],"summary":"Zernio Growth Sequences Json","description":"List the tenant's drip sequences.","operationId":"zernio_growth_sequences_json_dashboard_publish_zernio_growth_sequences_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/growth/sequences":{"post":{"tags":["zernio"],"summary":"Zernio Growth Create Sequence","description":"Create a drip sequence. Body: {account_id, name, steps:[...],\ndescription?, template?}. template='bienvenida' uses the built-in\n\"Bienvenida Chalyb\" 3-step template.","operationId":"zernio_growth_create_sequence_dashboard_publish_zernio_growth_sequences_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/growth/sequences/{sequence_id}/toggle":{"post":{"tags":["zernio"],"summary":"Zernio Growth Toggle Sequence","operationId":"zernio_growth_toggle_sequence_dashboard_publish_zernio_growth_sequences__sequence_id__toggle_post","parameters":[{"name":"sequence_id","in":"path","required":true,"schema":{"type":"string","title":"Sequence Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/growth/sequences/{sequence_id}/enroll":{"post":{"tags":["zernio"],"summary":"Zernio Growth Enroll Sequence","description":"Enroll contacts. Body: {contact_ids:[...]} OR {tag: \"...\"} to\nenroll all contacts carrying that tag.","operationId":"zernio_growth_enroll_sequence_dashboard_publish_zernio_growth_sequences__sequence_id__enroll_post","parameters":[{"name":"sequence_id","in":"path","required":true,"schema":{"type":"string","title":"Sequence Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/growth/broadcasts/send":{"post":{"tags":["zernio"],"summary":"Zernio Growth Send Broadcast","description":"Create + populate + SEND a broadcast in one call (the UI's\nconfirm modal gates the click). Body: {account_id, name, message,\ncontact_ids?, tag?, confirm: true}.\n\nGuardrail: the per-tenant daily cap is checked BEFORE the send —\na broadcast is an irreversible mass DM. `confirm` must be true.","operationId":"zernio_growth_send_broadcast_dashboard_publish_zernio_growth_broadcasts_send_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/inbox/comments.json":{"get":{"tags":["zernio"],"summary":"Zernio Inbox Comments Json","description":"Comments on the tenant's clips — unified feed, or one post when\n`platform_post_id` is set. Reads the local webhook store.","operationId":"zernio_inbox_comments_json_dashboard_publish_zernio_inbox_comments_json_get","parameters":[{"name":"platform_post_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/inbox/comments/reply":{"post":{"tags":["zernio"],"summary":"Zernio Inbox Comment Reply","description":"Reply to a comment. Body: {account_id, post_id, comment_id?,\nmessage}. account_id must be one the tenant owns (checked).","operationId":"zernio_inbox_comment_reply_dashboard_publish_zernio_inbox_comments_reply_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/inbox/comments/{comment_action}":{"post":{"tags":["zernio"],"summary":"Zernio Inbox Comment Action","description":"Like or hide a comment. comment_action ∈ {like, hide}. Body:\n{account_id, post_id, comment_id, platform?}. Hide is gated to the\nplatforms that support it.","operationId":"zernio_inbox_comment_action_dashboard_publish_zernio_inbox_comments__comment_action__post","parameters":[{"name":"comment_action","in":"path","required":true,"schema":{"type":"string","title":"Comment Action"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/inbox/conversations.json":{"get":{"tags":["zernio"],"summary":"Zernio Inbox Conversations Json","description":"DM conversations across the tenant's accounts (local store).","operationId":"zernio_inbox_conversations_json_dashboard_publish_zernio_inbox_conversations_json_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","default":"","title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/inbox/messages.json":{"get":{"tags":["zernio"],"summary":"Zernio Inbox Messages Json","description":"Messages in one conversation (local store).","operationId":"zernio_inbox_messages_json_dashboard_publish_zernio_inbox_messages_json_get","parameters":[{"name":"conversation_id","in":"query","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/inbox/conversations/reply":{"post":{"tags":["zernio"],"summary":"Zernio Inbox Send Message","description":"Send a DM reply. Body: {account_id, conversation_id, message,\nattachment_url?}. Attachments gated per platform capability.","operationId":"zernio_inbox_send_message_dashboard_publish_zernio_inbox_conversations_reply_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/inbox/conversations/archive":{"post":{"tags":["zernio"],"summary":"Zernio Inbox Archive","description":"Archive (or re-activate) a conversation. Body: {account_id,\nconversation_id, status?}. status defaults to 'archived'.","operationId":"zernio_inbox_archive_dashboard_publish_zernio_inbox_conversations_archive_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/calendar.json":{"get":{"tags":["zernio"],"summary":"Zernio Calendar Json","description":"Unified content calendar: entries from THREE sources merged and\nlabeled —\n  - `hub`: clips published through ChalyClip (zernio_publishes)\n  - `scheduled`: hub posts with a future scheduled_for\n  - `external`: posts the streamer authored natively, detected via\n    post.external.* webhooks (zernio_calendar), resolved to this\n    tenant by matching the social account id against their\n    connected accounts\n\nEach entry: {date, source, platform(s), content, status, url,\npost_id, clip_id?}. Deleted external posts are included with\nstatus='deleted' so the UI can grey them out. Date bounds are\nISO-8601 (YYYY-MM-DD); omitted = no bound.","operationId":"zernio_calendar_json_dashboard_publish_zernio_calendar_json_get","parameters":[{"name":"date_from","in":"query","required":false,"schema":{"type":"string","default":"","title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"type":"string","default":"","title":"Date To"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/rendimiento.json":{"get":{"tags":["zernio"],"summary":"Zernio Performance Json","description":"Rendimiento tab data: recent clips with per-platform metrics +\na totals header, over the last `days` (7 or 30). Live from Zernio,\nnormalized (no fake zeros — absent metrics render as \"—\").","operationId":"zernio_performance_json_dashboard_publish_zernio_rendimiento_json_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/failed.json":{"get":{"tags":["zernio"],"summary":"Zernio Failed Json","description":"Failed posts for this tenant, each with its per-platform error +\nSpanish hint. Powers the clickable FAILED counter list.","operationId":"zernio_failed_json_dashboard_publish_zernio_failed_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/retry/{post_id}":{"post":{"tags":["zernio"],"summary":"Zernio Retry One","description":"Reintentar one failed post (POST /posts/{id}/retry).","operationId":"zernio_retry_one_dashboard_publish_zernio_retry__post_id__post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/retry-all":{"post":{"tags":["zernio"],"summary":"Zernio Retry All","description":"Reintentar todos los posts fallidos del tenant. Per-post failures\ndon't abort the batch; each reports ok/error.","operationId":"zernio_retry_all_dashboard_publish_zernio_retry_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/reprocess-failed":{"post":{"tags":["zernio"],"summary":"Zernio Reprocess Failed","description":"Reprocesar todos los posts fallidos del tenant a través del drip.\n\nRe-renderiza cada clip, mintea una URL firmada nueva y lo re-agenda\ncon el nuevo gate (uno cada 30/60 min según la estrategia). Esto SÍ\narregla los fallos por URL expirada — a diferencia de \"Reintentar\ntodos\", que re-dispara la misma URL.","operationId":"zernio_reprocess_failed_dashboard_publish_zernio_reprocess_failed_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/reprocess/{post_id}":{"post":{"tags":["zernio"],"summary":"Zernio Reprocess One","description":"Reprocesar un post fallido (re-render + URL nueva + re-agendar).","operationId":"zernio_reprocess_one_dashboard_publish_zernio_reprocess__post_id__post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/best-time.json":{"get":{"tags":["zernio"],"summary":"Zernio Best Time Json","description":"Best-time slots for the \"Hora óptima\" panel + the \"Usar hora\nóptima\" prefill on Programar. 403 (Analytics add-on) or no data →\nempty list, not an error — the panel shows a \"sin datos aún\" state.","operationId":"zernio_best_time_json_dashboard_publish_zernio_best_time_json_get","parameters":[{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/schedule/next-best.json":{"get":{"tags":["zernio"],"summary":"Zernio Next Best Json","description":"The single next best posting datetime (UTC ISO) — backs the\n\"Sugerir mejor hora\" button on Programar. Uses the tenant's best-time\nanalytics when available, else the sane fallback spread.","operationId":"zernio_next_best_json_dashboard_publish_zernio_schedule_next_best_json_get","parameters":[{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/compose/{clip_id}":{"post":{"tags":["zernio"],"summary":"Zernio Compose Clip","description":"Generate the title + caption auto-publish would post for this clip —\nbacks the Single Publish \"Auto-rellenar\" button. Stitches the viral\nhook + caption + AI hashtags + the tenant's fixed handle/hashtag\nsuffix; generates a fresh hook when the variant doesn't carry one.","operationId":"zernio_compose_clip_dashboard_publish_zernio_compose__clip_id__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/publish/zernio/schedule/auto":{"post":{"tags":["zernio"],"summary":"Zernio Schedule Auto","description":"Auto-program the approved backlog under the per-platform rulebook.\n\nRuns the rulebook-aware planner (`plan_backlog_schedule`): each connected\nplatform gets its publishable clips paced by its own `max_per_day` +\n`min_gap`, rolling overflow across days, best clips first — so a big YouTube\nbacklog spreads over weeks at a few per day instead of dozens in one day at\n30-min intervals. Idempotent: a cross-worker DB lock blocks concurrent runs\nand clips already posted to Zernio are skipped. Targets the autopublish\ntarget platforms ∩ connected (∩ the per-tier account cap), else all\nconnected. The slow scoring/render/publish loop runs in the background.","operationId":"zernio_schedule_auto_dashboard_publish_zernio_schedule_auto_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/schedule/auto/progress":{"get":{"tags":["zernio"],"summary":"Zernio Schedule Auto Progress","description":"Live progress for the tenant's auto-program run, polled by the UI.\n\nReturns {state: idle|running|done|error, total, done, scheduled, failed,\nresults:[{clip_id, ok, post_id|error, ...}]}.","operationId":"zernio_schedule_auto_progress_dashboard_publish_zernio_schedule_auto_progress_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/schedule/cancel/{post_id}":{"post":{"tags":["zernio"],"summary":"Zernio Cancel Scheduled","description":"Cancel a scheduled/queued post (DELETE /posts/{id} — Zernio only\nallows it for non-published posts). Tombstones the matching local\nrow so it leaves our history immediately, and puts the clip BACK in\nthe approved pool — cancelling means \"re-schedule this later\", not\n\"bury this clip forever\" (the tombstone used to leave the clip in\n'published' with no surface able to touch it again).","operationId":"zernio_cancel_scheduled_dashboard_publish_zernio_schedule_cancel__post_id__post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/accounts.json":{"get":{"tags":["zernio"],"summary":"Zernio Accounts Json","description":"Connected accounts for this tenant's profile, as JSON.\n\nUsed by the connect popup poller: after the operator authorizes on\nZernio (in a separate tab), the dashboard polls this until the new\nplatform appears, then closes the popup + refreshes — so the\noperator never has to deal with Zernio's own dashboard.","operationId":"zernio_accounts_json_dashboard_publish_zernio_accounts_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/accounts/{account_id}/disconnect":{"post":{"tags":["zernio"],"summary":"Zernio Disconnect Account","description":"Disconnect ONE connected account on Zernio (DELETE /accounts/{id}),\nso the operator manages connections without leaving ChalyClip.","operationId":"zernio_disconnect_account_dashboard_publish_zernio_accounts__account_id__disconnect_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"string","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/accounts/claim":{"post":{"tags":["zernio"],"summary":"Zernio Claim Existing","description":"Bind an EXISTING Zernio profileId to this tenant.\n\nUse case: an operator who already created connections under a\nspecific profileId on Zernio wants ChalyClip to reuse it instead of\nderiving a fresh `ten_<ulid>` one. We validate the profileId has at\nleast one connected account (so a typo surfaces immediately), then\npersist it on the tenant row.","operationId":"zernio_claim_existing_dashboard_publish_zernio_accounts_claim_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_zernio_claim_existing_dashboard_publish_zernio_accounts_claim_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/publish/zernio/unlink":{"post":{"tags":["zernio"],"summary":"Zernio Unlink","description":"Clear the tenant's Zernio profileId binding.\n\nDoes NOT disconnect accounts on Zernio's side — it just forgets the\nlocal mapping so the next Create/claim can use a different profile\nwithout touching existing connections.","operationId":"zernio_unlink_dashboard_publish_zernio_unlink_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/clip/{clip_id}/mark-published":{"post":{"tags":["zernio"],"summary":"Zernio Mark Published","description":"Manually flip an approved clip to 'published' so it leaves the\nready-to-publish grid.\n\nFor clips that already went out — either published before ChalyClip\nstarted recording publishes locally (pre-migration-030), or posted\nmanually outside ChalyClip. Local state only; nothing is sent to\nZernio. Records a synthetic `manual_<clip_id>` history row so the\nclip shows on the Published tab with the time it was marked.","operationId":"zernio_mark_published_dashboard_publish_zernio_clip__clip_id__mark_published_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/publish/zernio/clip/{clip_id}/reopen":{"post":{"tags":["zernio"],"summary":"Zernio Reopen Clip","description":"Reopen a published clip — flip it back to `approved` so it returns\nto the Publicar uno / Publicar varios grid and can be re-published.\n\nFor clips an auto-publish (or a manual publish) marked `published`\neven though the post failed to land. Local state only; nothing is\nsent to or removed from Zernio.","operationId":"zernio_reopen_clip_dashboard_publish_zernio_clip__clip_id__reopen_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/publish/zernio/reopen-all":{"post":{"tags":["zernio"],"summary":"Zernio Reopen All","description":"Reopen every published clip for the tenant in one shot — bulk\nrecovery after an auto-publish run marked a batch `published` that\nnever actually went out. Each moves `published` -> `approved`.","operationId":"zernio_reopen_all_dashboard_publish_zernio_reopen_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/post/{clip_id}":{"post":{"tags":["zernio"],"summary":"Zernio Post Clip","description":"Publish a rendered clip to one or more platforms via Zernio.\n\nInputs come from the dashboard form: clip_id (URL), platforms\n(comma-separated checkbox group), and optional title/description.\nThe caption (`content`) is the description, falling back to the\ntitle. We mint a signed URL pointing at /api/internal/clip/{clip_id}\n(1h TTL); Zernio downloads from there, re-hosts, and publishes to\neach requested account. The page's feed polls\n/status/{post_id}.json for completion.\n\nPhase-4 fields: `mode` (now|draft|schedule + `scheduled_for`),\n`first_comment` (only sent to platforms that support it),\n`tiktok_privacy`, and collapsed per-platform caption overrides as\n`caption_{platform}` form keys (+ `yt_title` for YouTube's\nrequired title).","operationId":"zernio_post_clip_dashboard_publish_zernio_post__clip_id__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_zernio_post_clip_dashboard_publish_zernio_post__clip_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/draft/{post_id}/publish":{"post":{"tags":["zernio"],"summary":"Zernio Draft Publish","description":"Publicar ahora / Programar a saved draft.\n\nZernio has no publish-a-draft endpoint (POST /posts/{id}/edit is\nX-only post-publication editing), so the hub re-creates the post\nfrom the LOCAL snapshot — fresh signed media URL, same captions /\nfirst comment / privacy — then deletes the Zernio-side draft.\n`scheduled_for` empty = ahora; set = programar.","operationId":"zernio_draft_publish_dashboard_publish_zernio_draft__post_id__publish_post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_zernio_draft_publish_dashboard_publish_zernio_draft__post_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/zernio/draft/{post_id}/delete":{"post":{"tags":["zernio"],"summary":"Zernio Draft Delete","description":"Delete a draft on Zernio + mark the local row deleted.","operationId":"zernio_draft_delete_dashboard_publish_zernio_draft__post_id__delete_post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/bulk-post":{"post":{"tags":["zernio"],"summary":"Zernio Bulk","description":"Bulk-publish entry point for the Bulk tab on the dashboard.\n\nJSON body shape (HTMX wire format from the bulk submit JS):\n\n  {\n    \"clips\": [\n      {\"clip_id\": \"clp_...\", \"platforms\": [\"tiktok\", \"instagram\"]},\n      {\"clip_id\": \"clp_...\", \"platforms\": [\"youtube\"]}\n    ],\n    \"title\": \"optional\",\n    \"description\": \"optional\"\n  }\n\nReturns a per-clip result list so the UI can surface \"3 of 4 queued\"\nfeedback. Failures don't short-circuit — each clip is independent.","operationId":"zernio_bulk_dashboard_publish_zernio_bulk_post_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard/publish/zernio/feed.json":{"get":{"tags":["zernio"],"summary":"Zernio Feed","description":"Tenant-scoped publish history as JSON.\n\nReads the LOCAL zernio_publishes table (never the raw company-wide\nZernio feed — see migration 030) and joins live status from Zernio\nby post id. Status falls back to \"queued\" when the join fails.","operationId":"zernio_feed_dashboard_publish_zernio_feed_json_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/publish/zernio/job/{post_id}":{"get":{"tags":["zernio"],"summary":"Zernio Job Detail","description":"Per-post detail page.\n\nReached from the queued banner (\"View status →\") and from feed rows.\nRenders the per-platform result of one Zernio post. Auto-polls\n/status/{post_id}.json while the post is still in a non-terminal\nstate, then halts once settled.\n\nWhen Zernio can't serve the post we fall back to the webhook-fed\nlocal record (see _local_post_status); only a genuine 404 with no\nlocal row surfaces an operator-facing \"no longer available\" state.","operationId":"zernio_job_detail_dashboard_publish_zernio_job__post_id__get","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post 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/zernio/status/{post_id}.json":{"get":{"tags":["zernio"],"summary":"Zernio Status","description":"Poll a single post by post_id. Used by the toast that surfaces\nright after a publish click — flips from PUBLISHING → PUBLISHED in\nthe UI without a full page reload.\n\nMirrors the job page's fallback: a Zernio fetch failure tries the\nwebhook-fed local record first; a genuine 404 with no local row\nsettles the poller (200 + terminal UNAVAILABLE) so the page stops\nrefreshing instead of looping on a 502 forever.","operationId":"zernio_status_dashboard_publish_zernio_status__post_id__json_get","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"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 ChalyClip'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 ChalyClip'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":["chalyb"],"summary":"Provision Tenant","description":"Idempotent tenant provisioning called by Chalyb.","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":"Chalyb integration not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/tenants/{tenant_id}/status":{"post":{"tags":["chalyb"],"summary":"Set Tenant Status","description":"Pause / resume a tenant remotely. Called by Chalyb 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":"Chalyb integration not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sso":{"get":{"tags":["chalyb"],"summary":"Sso Finalize","description":"Verify a Chalyb-signed token, set the session cookie, redirect home.\n\nHMAC verification is unconditional. When `CHALYB_SSO_SECRET` is unset\nwe return 503 so the misconfiguration is loud — *never* mint a session\nfrom an unverified payload. (Previously, an \"operator-opt-in lax mode\"\ndecoded the payload without verifying and trusted the claimed\ntenant_id, which let an unauthenticated attacker take over or\nauto-provision any tenant. Removed.)","operationId":"sso_finalize_auth_sso_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}},{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"303":{"description":"Token valid; cookie set; redirected to `next` (default /dashboard/start)"},"400":{"description":"Missing or malformed token"},"401":{"description":"Token signature failed or expired"},"503":{"description":"Chalyb SSO not configured"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/sso-diag":{"get":{"tags":["chalyb"],"summary":"Sso Diag","description":"Operator diagnostic for 'Strict SSO verify failed: bad signature'\nand the provisioning-mismatch symptom.\n\nReturns non-reversible fingerprints of the loaded CHALYB_SSO_SECRET\nand CHALYB_ADMIN_TOKEN. Compare each `fingerprint` against\nSHA256(value)[:12] of the corresponding secret configured on the\nChalyb side. Equal fingerprint → the bytes match; unequal → the\nvalues differ (or one side has stray whitespace — see\n`has_surrounding_whitespace`).\n\nAuth: the ChalyClip admin token, accepted via either the\n`Authorization: Bearer <token>` header OR a `?key=<token>` query\nparam (browser convenience — operators hit this from a tab). Gating\nit keeps the optional token-decode path from leaking SSO payloads\n(user_id / email / tenant_id) publicly. Note: the value you must\npresent here is the admin token as configured ON THIS ChalyClip\ninstance — if your own value is rejected, that's itself a signal the\nadmin token isn't what you think it is.\n\nOptional `?token=<the failing SSO link's token>` (the value after\n`?token=` in the SSO URL): decodes the payload WITHOUT verifying\n(showing tenant_id + how old the link is) AND runs the real strict\nverify against the loaded secret, reporting the exact failure. This\ncollapses 'why does THIS link fail' to one answer:\n\n    strict_verify FAIL: bad signature\n        → secret mismatch, OR the link was signed by an OLD secret\n          (regenerate the link after aligning both sides)\n    strict_verify FAIL: token expired\n        → the secrets are FINE; it's a stale link / clock skew\n    strict_verify PASS\n        → this token is valid against the loaded secret; the failure\n          you saw was a different (older) link\n\nReminder: get_settings() is lru_cached — if you changed the env var\non Railway but the fingerprint here still shows the old value, the\nprocess hasn't restarted. Redeploy / restart to pick it up.","operationId":"sso_diag_api_admin_sso_diag_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}},{"name":"key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"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_brand_kits_create_dashboard_brand_kits_post":{"properties":{"name":{"type":"string","title":"Name"},"primary_color":{"type":"string","title":"Primary Color","default":"#FF3366"},"accent_color":{"type":"string","title":"Accent Color","default":"#FFD700"},"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":""},"show_chalybclip_outro":{"type":"string","title":"Show Chalybclip Outro","default":"1"}},"type":"object","required":["name"],"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","default":"#FF3366"},"accent_color":{"type":"string","title":"Accent Color","default":"#FFD700"},"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":""},"safe_schedule_enabled":{"type":"string","title":"Safe Schedule Enabled","default":""},"content_timezone":{"type":"string","title":"Content Timezone","default":"UTC"},"safety_preset":{"type":"string","title":"Safety Preset","default":"recommended"},"show_chalybclip_outro":{"type":"string","title":"Show Chalybclip Outro","default":""}},"type":"object","required":["name"],"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_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_sources_add_dashboard_sources_post":{"properties":{"channel_url":{"type":"string","title":"Channel Url"},"persona_id":{"type":"string","title":"Persona Id"},"platform":{"type":"string","title":"Platform","default":""},"language":{"type":"string","title":"Language","default":""},"label":{"type":"string","title":"Label","default":""},"max_per_poll":{"type":"integer","title":"Max Per Poll","default":3},"polls_per_day":{"type":"integer","title":"Polls Per Day","default":1}},"type":"object","required":["channel_url","persona_id"],"title":"Body_sources_add_dashboard_sources_post"},"Body_sources_schedule_dashboard_sources__watch_id__schedule_post":{"properties":{"polls_per_day":{"type":"integer","title":"Polls Per Day"}},"type":"object","required":["polls_per_day"],"title":"Body_sources_schedule_dashboard_sources__watch_id__schedule_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"},"Body_url_job_create_dashboard_url_jobs_post":{"properties":{"vod_url":{"type":"string","title":"Vod Url"}},"type":"object","required":["vod_url"],"title":"Body_url_job_create_dashboard_url_jobs_post"},"Body_zernio_claim_existing_dashboard_publish_zernio_accounts_claim_post":{"properties":{"profile_id":{"type":"string","title":"Profile Id"}},"type":"object","required":["profile_id"],"title":"Body_zernio_claim_existing_dashboard_publish_zernio_accounts_claim_post"},"Body_zernio_draft_publish_dashboard_publish_zernio_draft__post_id__publish_post":{"properties":{"scheduled_for":{"type":"string","title":"Scheduled For","default":""}},"type":"object","title":"Body_zernio_draft_publish_dashboard_publish_zernio_draft__post_id__publish_post"},"Body_zernio_post_clip_dashboard_publish_zernio_post__clip_id__post":{"properties":{"platforms":{"type":"string","title":"Platforms"},"title":{"type":"string","title":"Title","default":""},"description":{"type":"string","title":"Description","default":""},"mode":{"type":"string","title":"Mode","default":"now"},"scheduled_for":{"type":"string","title":"Scheduled For","default":""},"first_comment":{"type":"string","title":"First Comment","default":""},"tiktok_privacy":{"type":"string","title":"Tiktok Privacy","default":""}},"type":"object","required":["platforms"],"title":"Body_zernio_post_clip_dashboard_publish_zernio_post__clip_id__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."},"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","maxLength":2048,"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 Chalyb. See docs/chalyb_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)."}}}}