{"openapi":"3.1.0","info":{"title":"Storyflo inference router","description":"Reads articles. Picks a TTS provider per request based on voice, availability, and cost. Caches by sha256(provider,voice,text).","version":"0.1.0"},"paths":{"/v1/health":{"get":{"summary":"Health","operationId":"health_v1_health_get","parameters":[{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/version":{"get":{"summary":"Version","description":"Lightweight build-info probe for Better Stack monitor 4414314.","operationId":"version_v1_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/health/worker":{"get":{"summary":"Health Worker","description":"D-4.8 · deep worker health probe.\n\nReturns 200 + healthy=true only when:\n  - At least one render in the last 10 minutes succeeded, OR\n  - The active queue is zero (nothing to do)\nOtherwise 200 + healthy=false with a diagnosis string. Returning\na non-200 here would cause Fly to recycle the machine, which is\ntoo aggressive — we want to alert via Telegram (existing reward\nstall worker pattern) and keep serving. Probe-driven dashboards\n(uptime monitors) interpret healthy=false themselves.","operationId":"health_worker_v1_health_worker_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Worker V1 Health Worker Get"}}}}}}},"/v1/health/cache":{"get":{"summary":"Health Cache","description":"Detailed cache backend introspection. Public so the deploy\nsmoke-test can verify which backend is live and that R2 (when\nconfigured) is actually reachable. Listing R2 keys here would be\nan unbounded LIST call against the bucket, so we keep ``recent_keys``\nfilesystem-only and return ``[]`` for R2.","operationId":"health_cache_v1_health_cache_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/render":{"post":{"summary":"Render","operationId":"render_v1_render_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhook/publish":{"post":{"summary":"Webhook Publish","description":"CMS webhook. Called by a publisher's CMS within seconds of publish; we\npre-render the audio so the player streams without inference latency on\nthe first listen.","operationId":"webhook_publish_v1_webhook_publish_post","parameters":[{"name":"x-readout-signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Signature"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/audio/{key}.wav":{"get":{"summary":"Serve Audio","operationId":"serve_audio_v1_audio__key__wav_get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/audio/{key}.opus":{"get":{"summary":"Serve Audio Opus","description":"Sibling of ``/v1/audio/{key}.wav`` for the Ogg-Opus encoding.\n\nProduction uses a public R2 bucket so listeners hit R2 directly via\nthe ``opus_url`` field on the audio response — this handler is the\nfallback for filesystem-backend dev + private-bucket R2.","operationId":"serve_audio_opus_v1_audio__key__opus_get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/audio/{key}.mp3":{"get":{"summary":"Serve Audio Mp3","description":"Sibling of ``/v1/audio/{key}.wav`` for the MP3 encoding.\n\nMP3 is the canonical R2 upload format for new renders as of 2026-06-10,\nreplacing the WAV master to free ~95% of R2 bucket space (WAV was 96%\nof the 10 GB free-tier cap). Apple Podcasts / Spotify ingest both accept\naudio/mpeg (MP3); Opus is the web-player format for Chrome/Firefox/Safari 17+.\n\nFilesystem backend (dev): streams the local .mp3 file if present.\nR2 backend: 302-redirects to the R2 object (public bucket URL or\n1-hour presigned URL, same as the .wav and .opus routes).","operationId":"serve_audio_mp3_v1_audio__key__mp3_get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/jobs":{"post":{"summary":"Enqueue Job","description":"Queue a render. Returns immediately with the job id; the worker\npicks it up within ``IDLE_POLL_SECONDS``. Use ``GET /v1/jobs/{id}``\nto poll for completion.","operationId":"enqueue_job_v1_jobs_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobEnqueueRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/jobs/{job_id}":{"get":{"summary":"Get Job","operationId":"get_job_v1_jobs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/plays":{"post":{"summary":"Post Play","description":"Record a play. Writes the play row + the three allocation ledger\nlegs in a single transaction.","operationId":"post_play_v1_plays_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlayRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlayResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stories/search":{"get":{"summary":"Stories Search","description":"Search Storyflo's audio archive.\n\nWhen ``expand=True`` (default) and local results are thin (< 5),\nwe also race a Hacker News + Brave Search News + GNews fan-out\nand surface the top external hits as 'suggestions' the listener\ncan forward to story@storyflo.com to ingest. External results are\nNOT in our DB until the listener forwards — they're discovery\nnudges, not silently-ingested content.\n\nHot-query speedup: responses are wrapped by a 60s in-process\nLRU+TTL cache keyed by ``(query, vertical, limit, offset)``. The\nwatchdog smoke-tests ``?q=tech`` every minute and the frontend\nhero suggests a small fixed set of common terms, so a tiny per-\nworker cache collapses >90% of that load to one warm response\nper minute. ``degraded=True`` responses are NOT cached by default\n(don't lock in an empty result on a statement_timeout blip).\nOperator can bypass per-request with\n``X-Storyflo-Search-Cache: bypass`` (used by smoke probes that\nwant to measure cold-pass latency) or globally via env\n``SEARCH_CACHE_TTL_SEC=0``.\n\nPool-exhaustion fix (mirrors PR #657 for /v1/search/hybrid):\ndeliberately no ``db: AsyncSession = Depends(get_db_replica)``\nparameter. With basic-plan PG sized at ``DB_POOL_SIZE=5 +\nMAX_OVERFLOW=3`` (total 8 conns, see\n``[[storyflo-prod-deploy-discipline-2026-06-19]]``), a request-\nscoped Depends session would be held for the full duration of\n``cache.get_or_compute`` — including for non-owners merely\nawaiting another coroutine's Future inside ``search_cache.py``.\nThe owner opens a short-lived replica session inside ``_compute``\nand releases it before the external fan-out + Future resolution;\nnon-owners hold zero connections while waiting.","operationId":"stories_search_v1_stories_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200,"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"default":0,"title":"Offset"}},{"name":"expand","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Expand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Stories Search V1 Stories Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/search-cache/stats":{"get":{"summary":"Admin Search Cache Stats","description":"Read-only stats for the /v1/stories/search in-process cache.\n\nReturns hit/miss counters, current entry count, eviction count, and\nthe live TTL + capacity for the worker process serving this\nrequest. Each uvicorn worker keeps its own cache, so divergent\nstats across calls are normal under multi-worker deploys.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_search_cache_stats_v1_admin_search_cache_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Search Cache Stats V1 Admin Search Cache Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/search/reindex":{"post":{"summary":"Admin Search Reindex","description":"Fire ``REINDEX INDEX CONCURRENTLY ix_articles_search_vector`` as a\nFastAPI background task — long-running but the uvicorn worker\noutlives a typical fly ssh session, so this is the canonical\nmaintenance lever for the GIN search index.\n\nReturns immediately. Poll ``/v1/admin/search/reindex/status`` to\nwatch elapsed_sec + ok flip, or check pg index size directly.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_search_reindex_v1_admin_search_reindex_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Search Reindex V1 Admin Search Reindex Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/search/reindex/status":{"get":{"summary":"Admin Search Reindex Status","description":"Live status of the most recent REINDEX kicked off by this worker.\n\nEach uvicorn worker tracks its own attempt — if you fire the\nrebuild on one worker and poll another, you'll see ``running=False``\n+ all-None fields on the worker that wasn't asked.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_search_reindex_status_v1_admin_search_reindex_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Search Reindex Status V1 Admin Search Reindex Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/trending-cache/stats":{"get":{"summary":"Admin Trending Cache Stats","description":"Read-only stats for the /v1/trending in-process cache.\n\nSame shape + per-worker semantics as the search-cache stats above.\nTrending Q2 (24h active-publishers aggregation) was the slow leg\n(~9.9s wall-clock) before PR #336 landed this cache; expect hit\nrates >90% under steady operator-dashboard load.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_trending_cache_stats_v1_admin_trending_cache_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Trending Cache Stats V1 Admin Trending Cache Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stories/feed":{"get":{"summary":"Stories Feed","description":"Public trending feed. Defaults to ``audio_ready_only=True`` so\nvisitors only see articles with rendered audio they can actually\nplay. Pass ``?audio_ready_only=false`` for admin/diagnostic\nsurfaces that want the full picture.\n\nSEO · pass ``?exclude_stubs=true`` to get ONLY the articles this API\nrenders in full to an anonymous viewer — the ones /story/[slug]\nlets Google index. Everything else is a thin ``publisher_only`` /\n``listener_private`` attribution stub that the story page marks\n``noindex``. The frontend sitemap builder uses this so it no longer\nhas to fetch every candidate slug's detail just to learn which are\nindexable (that per-slug hydration ran under a 15s wall-clock\ndeadline and was silently truncating the sitemap to a fraction of\nthe real catalogue).\n\nItem #15 · Pass ``?personalized=true&listener_token=...`` to rank\nby cosine similarity to the listener's taste vector (pgvector +\ntext-embedding-3-small). Falls back gracefully to recency when\nthe listener has no taste vector yet or pgvector isn't installed.","operationId":"stories_feed_v1_stories_feed_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"default":0,"title":"Offset"}},{"name":"curated_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Curated Only"}},{"name":"audio_ready_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Audio Ready Only"}},{"name":"personalized","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Personalized"}},{"name":"exclude_stubs","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Exclude Stubs"}},{"name":"listener_token","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Stories Feed V1 Stories Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/topic-art":{"get":{"summary":"Topic Art","description":"Generate (or serve cached) AI topic art for an OG card. Public so\nthe frontend's edge OG handler can fetch it server-side without\nleaking the API key.","operationId":"topic_art_v1_topic_art_get","parameters":[{"name":"prompt","in":"query","required":true,"schema":{"type":"string","title":"Prompt"}},{"name":"w","in":"query","required":false,"schema":{"type":"integer","default":1200,"title":"W"}},{"name":"h","in":"query","required":false,"schema":{"type":"integer","default":630,"title":"H"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/alpha/signup":{"post":{"summary":"Alpha Signup","description":"Register an email for the private alpha. Idempotent — the same\nemail twice returns 201 with ``{\"already_registered\": true}`` so the\nUX shows the same success state regardless. We silently absorb the\nduplicate to avoid email-enumeration leaks.","operationId":"alpha_signup_v1_alpha_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlphaSignupRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Alpha Signup V1 Alpha Signup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/rotate":{"post":{"summary":"Listener Rotate Token","description":"D-1.4 · listener-initiated token rotation.\n\nUsed when a listener suspects their feed URL has leaked (forwarded,\nposted, screenshotted into a public chat). Mints a fresh token,\nswaps it on the row, returns the new token + new feed URL. The\nold token is immediately invalid for /v1/listeners/{token}/* and\nthe public RSS feed.","operationId":"listener_rotate_token_v1_listeners__token__rotate_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Rotate Token V1 Listeners  Token  Rotate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/clickthrough/substack-paywall":{"post":{"summary":"Listener Substack Paywall Clickthrough","description":"Record a listener click-through on a Substack paywall CTA.\n\nWrites a zero-micros ``LedgerEntry`` tagged\n``source='substack_paywall_click_through'`` so the author conversion\ndashboard can surface per-publisher counts as the leading indicator\nfor \"Storyflo audio drove a paid Substack subscription\". See\n``app/db/ledger.py::record_substack_paywall_click_through`` for the\nwriter and the Substack acquisition wedge memo for the why.\n\nIdempotency: a second POST with the same (listener, publisher) within\n30 seconds returns the previously written entry id with\n``duplicate: true`` rather than double-counting. Beyond 30s the next\nPOST writes a new row — distinct browse sessions on the same author\nare legitimately distinct conversion signals.","operationId":"listener_substack_paywall_clickthrough_v1_listeners__listener_token__clickthrough_substack_paywall_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubstackClickThroughRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Substack Paywall Clickthrough V1 Listeners  Listener Token  Clickthrough Substack Paywall Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/subscribe":{"post":{"summary":"Listener Subscribe","description":"Mint (or fetch) a private podcast feed token for the given email.\n\nIdempotent on email — if the address already has a subscription we\nreturn the same token and just update the verticals filter. The\nreturned ``feed_url`` is what the listener pastes into Spotify /\nApple Podcasts / Audible to subscribe.","operationId":"listener_subscribe_v1_listeners_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListenerSubscribeRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listener Subscribe V1 Listeners Subscribe Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listeners/{token}/pre-warm":{"post":{"summary":"Admin Listener Pre Warm","description":"Operator-fire pre-warm for a specific listener token.\n\nUsed to re-prime a listener whose first-touch warm failed, or to\nmanually warm a high-value early-access listener.\n\nAuth: admin token (X-Storyflo-Email-Token).\nBody: {\"count\": int (1-10, default 5), \"force\": bool (default false)}\nPass force=true to bypass the ``first_render_done_at`` skip.","operationId":"admin_listener_pre_warm_v1_admin_listeners__token__pre_warm_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdminPreWarmRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Listener Pre Warm V1 Admin Listeners  Token  Pre Warm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/circle/wallet-audit":{"get":{"summary":"Admin Circle Wallet Audit","description":"Read-only reconciliation of Circle wallets vs ListenerSubscription rows.\n\nSurfaces ghost wallets: addresses Circle has minted that are no\nlonger (or never were) bound to a listener row. Circle's\nProgrammable Wallets API does not expose a delete endpoint, so\nthis is informational only — pair with the\nCIRCLE_AUTO_MINT_ENABLED flag (defaults off) to stop the leak at\nthe source. Output drives the 1k/mo free-tier capacity decision.\n\nReturns:\n    circle_total: count of wallets in the storyflo-listeners walletSet\n    db_total: count of ListenerSubscription rows with wallet_address\n    orphan_count: Circle wallets whose address has no matching DB row\n    orphan_sample: up to 20 orphan addresses (truncated for response size)","operationId":"admin_circle_wallet_audit_v1_admin_circle_wallet_audit_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Circle Wallet Audit V1 Admin Circle Wallet Audit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/cancelled-audio/cleanup":{"post":{"summary":"Admin Cancelled Audio Cleanup","description":"Delete R2 audio orphaned by cancelled render_jobs for a publisher.\n\nPairs with ``scripts/cleanup_cancelled_audio.py`` — same core logic,\nsame idempotency semantics, just exposed as HTTP so the operator\ncan drive it without a fly-ssh session.\n\nAuth: admin token (X-Storyflo-Email-Token).\n\nBody:\n    publisher_name_pattern: SQL LIKE pattern, default 'Google News%'\n    dry_run: bool, default true (recommended first pass)\n    limit:   int, default 5000  (max render_job rows per call)\n    batch_size: int, default 1000 (S3 delete_objects batch size)\n\nResponse: ``{rows_scanned, object_keys_targeted, deleted, rows_nulled,\n             errors[], dry_run, publisher_name_pattern}``.","operationId":"admin_cancelled_audio_cleanup_v1_admin_cancelled_audio_cleanup_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdminCancelledAudioCleanupRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Cancelled Audio Cleanup V1 Admin Cancelled Audio Cleanup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/verticals":{"get":{"summary":"Listener Vertical Catalog Get","description":"The canonical listener-facing vertical catalog.\n\nEvery slug is verified to return audio-ready articles — see the\ncorpus-volume audit in ``app/listener_verticals.py``. Static content\nwith no DB touch, so it caches hard at the edge.","operationId":"listener_vertical_catalog_get_v1_listeners_verticals_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listener Vertical Catalog Get V1 Listeners Verticals Get"}}}}}}},"/v1/listeners/{listener_token}":{"get":{"summary":"Listener Preferences Get","description":"Read the current customization for a listener. The token IS the\nauth — anyone with the token can read + write the preferences, same\ncontract as the podcast feed.","operationId":"listener_preferences_get_v1_listeners__listener_token__get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Preferences Get V1 Listeners  Listener Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Listener Preferences Patch","description":"Update one or more listener preferences. Only fields present in the\nbody are touched — other columns stay as-is.","operationId":"listener_preferences_patch_v1_listeners__listener_token__patch","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListenerPreferences"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Preferences Patch V1 Listeners  Listener Token  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Listener Unsubscribe","description":"Hard-delete the listener's subscription. The token is invalidated\nimmediately — the feed URL 404s on next refresh.","operationId":"listener_unsubscribe_v1_listeners__listener_token__delete","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Unsubscribe V1 Listeners  Listener Token  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/preferences/vertical":{"put":{"summary":"Listener Set Preferred Vertical","description":"Pin the listener's primary vertical. Drives the queue re-rank\nboost. Pass {\"vertical\": null} to clear.\n\nAccepted slugs are ``listener_verticals.LISTENER_VERTICALS`` (the\nsame catalog ``GET /v1/listeners/verticals`` serves) plus every\nlegacy alias, which is canonicalized on the way in.\n\n2026-08-11 — this used to validate against\n``vertical_classifier.ALL_VERTICALS``, the CLASSIFIER taxonomy. That\nset contains ``young_moms``/``yoga``/``other`` but NOT ``media``,\n``policy``, ``world`` or ``gaming`` — all four of which the listener\nUI offered as chips. Pinning any of them 400'd, which is precisely\nwhy the signup card had to keep two disjoint chip rows with\ndifferent selection semantics. Validating against the listener\ncatalog is what lets one unified multi-select row exist.","operationId":"listener_set_preferred_vertical_v1_listeners__listener_token__preferences_vertical_put","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerVerticalPref"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Set Preferred Vertical V1 Listeners  Listener Token  Preferences Vertical Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/queue":{"get":{"summary":"Listener Queue","description":"JSON queue for the listener dashboard. Returns recent articles\nmatching the listener's verticals with audio render status — so the\nUI can show what's ready to play, what's still rendering, and what\nfailed. Mirrors the RSS feed selection logic but emits JSON and\nincludes pending/rendering items (which RSS skips).","operationId":"listener_queue_v1_listeners__listener_token__queue_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Queue V1 Listeners  Listener Token  Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/feed":{"get":{"summary":"Listener Vertical Feed","description":"Vertical-scoped recency feed for a signed-in listener.\n\nReturns the most-recent ``Article`` rows whose ``vertical`` is in\nthe listener's subscribed verticals, sorted by ``created_at`` DESC.\nEach item carries an ``audio_ready`` flag + a resolved ``audio_url``\nwhen a ``done`` ``RenderJob`` exists, so the UI can render a play\nbutton immediately for warm rows and a \"tap to render\" affordance\nfor cold ones.\n\nOrdering is deliberately ``created_at`` DESC — NOT ``trending_score``.\nThe trending-score worker is being decommissioned as part of the\ncost restructure; recency is good enough for the listener feed and\navoids an extra index dependency the new architecture won't keep.\n\nListener-private (forwarded) articles owned by OTHER listeners are\nstripped via the shared ``_listener_private_slugs`` DMCA filter so\na feed read can't leak the slug of someone else's private send.\n\nRate-limited per IP at the standard audio-read budget.","operationId":"listener_vertical_feed_v1_listeners__listener_token__feed_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Vertical Feed V1 Listeners  Listener Token  Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/render-now":{"post":{"summary":"Article Render Now","description":"Listener-driven render-on-demand.\n\nResolves the article by ``slug``. If a ``done`` render-job already\nexists, returns 200 + the audio URL immediately (idempotent). Else\nenqueues a fresh job with ``priority=\"high\"`` and returns 202 with\na Location header pointing at the public audio polling endpoint\n``/v1/articles/{slug}/audio``.\n\nConcurrency: at most ``MAX_IN_FLIGHT_RENDERS_PER_LISTENER`` queued/\nrunning render-jobs may exist at once for articles the listener\nhas forwarded (intake_listener_email match). Excess requests get a\n429 with a ``Retry-After`` so the client backs off — matches the\ncontract of the per-IP rate-limit but uses worker depth as the\nsignal.","operationId":"article_render_now_v1_articles__slug__render_now_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RenderNowRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/flo":{"get":{"summary":"Listener Flo Get","description":"Return the listener's full flo (defaults merged with overrides),\nplus the active block resolved against their saved timezone.","operationId":"listener_flo_get_v1_listener__listener_token__flo_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Flo Get V1 Listener  Listener Token  Flo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Listener Flo Put","description":"Patch one or more blocks. Fields not present are left as-is.\nPass `manual_order` to persist a drag-drop reorder for that block.","operationId":"listener_flo_put_v1_listener__listener_token__flo_put","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_FloUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Flo Put V1 Listener  Listener Token  Flo Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/flo/now":{"get":{"summary":"Listener Flo Now","description":"Return today's queue ranked specifically for the active block.\nHonors `manual_order` first, then verticals + tone scoring, then\nrecency. Plays the role of /listen/queue when flo is enabled.\n\n2026-06-17 perf SEV · candidate-SELECT projection fix\n(PYTHON-FASTAPI-4M, ~379 events/24h). The candidate SELECT pulled\nfull ``Article`` entities at ``limit*6`` rows — 120 wide rows per\ncall (the rerank scaffolding picks ``limit*2`` for the job batch\nand then 20 to emit). The wide JSON columns (embedding, parsed_json,\nchapters_json, story_segments, body_segments, transcript_alignment,\nwaveform_peaks, audio_variants) were detoasted for every candidate\neven though ``score_article`` + the response loop touch only ten\nscalar columns. Apply ``_flo_now_article_load_only`` for the same\nwin as the public-RSS / trending-feed projections.","operationId":"listener_flo_now_v1_listener__listener_token__flo_now_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Flo Now V1 Listener  Listener Token  Flo Now Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/digest/run":{"post":{"summary":"Admin Run Digest","description":"Iterate every active listener subscription and send a daily digest\nemail summarising new audio in their selected verticals. Idempotent\non calendar day — re-running within 20 hours of the last send for a\ngiven listener is a silent no-op.\n\nAuth: admin token (X-Storyflo-Email-Token; ADMIN_TOKEN or legacy\nEMAIL_INTAKE_TOKEN). Designed to be hit once a day by Fly cron /\nexternal scheduler.","operationId":"admin_run_digest_v1_admin_digest_run_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Run Digest V1 Admin Digest Run Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/listener-magic-link":{"post":{"summary":"Listener Magic Link","description":"Send a sign-in link to an existing listener subscription. We\nintentionally return the same response shape whether or not the\nemail exists — prevents email enumeration. The actual email is only\nsent when a subscription matches AND Postmark is configured.","operationId":"listener_magic_link_v1_auth_listener_magic_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listener Magic Link V1 Auth Listener Magic Link Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/listener-magic-link/exchange":{"post":{"summary":"Listener Magic Link Exchange","description":"D-1.7 · single-use exchange. POST {\"magic\": \"<plaintext>\"} →\nreturns the listener's stable feed token + email + feed_url. Marks\nthe magic token used_at=now() so any second exchange returns 410.\n\nErrors:\n  - 400 missing magic param\n  - 410 already used / expired\n  - 404 unknown token (constant-time-ish via sha256 lookup)","operationId":"listener_magic_link_exchange_v1_auth_listener_magic_link_exchange_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listener Magic Link Exchange V1 Auth Listener Magic Link Exchange Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/auth/magic-link/request":{"post":{"summary":"Admin Magic Link Request","description":"Send a sign-in link to an allowlisted admin email. Same response\nshape regardless of whether the email is on the allowlist — prevents\nenumeration of who's authorized. Allowlist via\n``ADMIN_MAGIC_LINK_EMAILS`` env var (default ``story@storyflo.com``).","operationId":"admin_magic_link_request_v1_admin_auth_magic_link_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMagicLinkRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Admin Magic Link Request V1 Admin Auth Magic Link Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/auth/magic-link/exchange":{"post":{"summary":"Admin Magic Link Exchange","description":"Redeem an admin magic link. POST {\"magic\": \"<plaintext>\"} → returns\nthe admin token the frontend stores in localStorage / cookie.\n\nErrors:\n  - 400 missing magic param\n  - 410 used / expired / wrong-scope / unknown (all collapsed to\n    the same status so timing leaks don't differentiate failure modes)","operationId":"admin_magic_link_exchange_v1_admin_auth_magic_link_exchange_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Admin Magic Link Exchange V1 Admin Auth Magic Link Exchange Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/auth/login":{"post":{"summary":"Admin Password Login","description":"Shared email+password admin sign-in → returns the admin token.\n\nA friendlier alternative to pasting ``admin_token`` directly or using\nthe magic link: POST {\"email\": ..., \"password\": ...} and, on a correct\nmatch, get back the same token the rest of /v1/admin/* expects in\n``X-Storyflo-Email-Token``.\n\nAuth: ``ADMIN_LOGIN_EMAIL`` + ``ADMIN_LOGIN_PASSWORD`` env vars, matched\nagainst ``ADMIN_TOKEN`` as the returned secret. When any of the three is\nunset the endpoint is disabled (503) — we never authenticate against a\nblank credential.\n\nThe /v1/admin/* per-IP rate-limit middleware (_admin_path_rate_limit)\nalready throttles this path by prefix, on top of the per-route limit\nabove, so credential-stuffing is bounded.\n\nErrors:\n  - 503 password login not configured (any of email/password/token unset)\n  - 401 invalid credentials (generic — bad email and bad password are\n    indistinguishable, by status and body and by timing)","operationId":"admin_password_login_v1_admin_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPasswordLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Admin Password Login V1 Admin Auth Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/podcasts/storyflo.xml":{"get":{"summary":"Storyflo Public Podcast","description":"All-trending public feed (no vertical filter). The URL we\nsubmit to Spotify Podcasters / Apple / Audible — must stay stable\nforever once a directory ingests it. HEAD is registered alongside\nGET because Apple Podcasts Connect HEAD-probes feeds before queueing\ningest; absent HEAD support, the probe sees 405 and rejects.","operationId":"storyflo_public_podcast_v1_podcasts_storyflo_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Public Podcast","description":"All-trending public feed (no vertical filter). The URL we\nsubmit to Spotify Podcasters / Apple / Audible — must stay stable\nforever once a directory ingests it. HEAD is registered alongside\nGET because Apple Podcasts Connect HEAD-probes feeds before queueing\ningest; absent HEAD support, the probe sees 405 and rejects.","operationId":"storyflo_public_podcast_v1_podcasts_storyflo_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-declassified.xml":{"get":{"summary":"Storyflo Declassified Podcast","description":"Dedicated Declassified RSS feed — ``Storyflo Declassified — The\nUntold Cases``. See ``_build_declassified_podcast_rss`` + the module\ndocstring above for the architecture rationale.\n\nRegistered BEFORE the wildcard ``/v1/podcasts/storyflo-{vertical}.xml``\nroute so this specific path wins FastAPI's route-matching pass;\nrouting through the wildcard would 404 because ``declassified``\nisn't (and shouldn't be) a registered vertical in\n``PODCAST_VERTICAL_META``.\n\nHEAD is registered alongside GET because Apple Podcasts Connect\nHEAD-probes feeds before queueing ingest; absent HEAD support, the\nprobe sees 405 and rejects the show submission.\n\nNo ``Depends(get_db_replica)`` — the builder routes its single read\nthrough ``run_replica_read_with_retry`` (fresh replica session per\nattempt) so a hot-standby recovery conflict is retried instead of\n500-ing (2026-07-10 Sentry). On any build failure we mirror the\nper-vertical feed's degrade ladder: serve the last-cached body even\npast TTL, else a clean 503 + Retry-After — directories can auto-pause\nfeeds that 500 repeatedly.","operationId":"storyflo_declassified_podcast_v1_podcasts_storyflo_declassified_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Declassified Podcast","description":"Dedicated Declassified RSS feed — ``Storyflo Declassified — The\nUntold Cases``. See ``_build_declassified_podcast_rss`` + the module\ndocstring above for the architecture rationale.\n\nRegistered BEFORE the wildcard ``/v1/podcasts/storyflo-{vertical}.xml``\nroute so this specific path wins FastAPI's route-matching pass;\nrouting through the wildcard would 404 because ``declassified``\nisn't (and shouldn't be) a registered vertical in\n``PODCAST_VERTICAL_META``.\n\nHEAD is registered alongside GET because Apple Podcasts Connect\nHEAD-probes feeds before queueing ingest; absent HEAD support, the\nprobe sees 405 and rejects the show submission.\n\nNo ``Depends(get_db_replica)`` — the builder routes its single read\nthrough ``run_replica_read_with_retry`` (fresh replica session per\nattempt) so a hot-standby recovery conflict is retried instead of\n500-ing (2026-07-10 Sentry). On any build failure we mirror the\nper-vertical feed's degrade ladder: serve the last-cached body even\npast TTL, else a clean 503 + Retry-After — directories can auto-pause\nfeeds that 500 repeatedly.","operationId":"storyflo_declassified_podcast_v1_podcasts_storyflo_declassified_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-theo.xml":{"get":{"summary":"Storyflo Host Podcast Theo","description":"Theo's daily tech brief — see ``_PER_HOST_PODCAST_META['theo']``.","operationId":"storyflo_host_podcast_theo_v1_podcasts_storyflo_theo_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Theo","description":"Theo's daily tech brief — see ``_PER_HOST_PODCAST_META['theo']``.","operationId":"storyflo_host_podcast_theo_v1_podcasts_storyflo_theo_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-mason.xml":{"get":{"summary":"Storyflo Host Podcast Mason","description":"Mason's daily markets brief — see ``_PER_HOST_PODCAST_META['mason']``.","operationId":"storyflo_host_podcast_mason_v1_podcasts_storyflo_mason_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Mason","description":"Mason's daily markets brief — see ``_PER_HOST_PODCAST_META['mason']``.","operationId":"storyflo_host_podcast_mason_v1_podcasts_storyflo_mason_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-riley.xml":{"get":{"summary":"Storyflo Host Podcast Riley","description":"Jessica's daily news brief — see ``_PER_HOST_PODCAST_META['riley']``.","operationId":"storyflo_host_podcast_riley_v1_podcasts_storyflo_riley_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Riley","description":"Jessica's daily news brief — see ``_PER_HOST_PODCAST_META['riley']``.","operationId":"storyflo_host_podcast_riley_v1_podcasts_storyflo_riley_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-iris.xml":{"get":{"summary":"Storyflo Host Podcast Iris","description":"Chloe's daily health brief — see ``_PER_HOST_PODCAST_META['iris']``.","operationId":"storyflo_host_podcast_iris_v1_podcasts_storyflo_iris_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Iris","description":"Chloe's daily health brief — see ``_PER_HOST_PODCAST_META['iris']``.","operationId":"storyflo_host_podcast_iris_v1_podcasts_storyflo_iris_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-brock.xml":{"get":{"summary":"Storyflo Host Podcast Brock","description":"Brock's daily sports brief — see ``_PER_HOST_PODCAST_META['brock']``.","operationId":"storyflo_host_podcast_brock_v1_podcasts_storyflo_brock_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Brock","description":"Brock's daily sports brief — see ``_PER_HOST_PODCAST_META['brock']``.","operationId":"storyflo_host_podcast_brock_v1_podcasts_storyflo_brock_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/storyflo-wit.xml":{"get":{"summary":"Storyflo Host Podcast Wit","description":"Wit's daily comedy brief — see ``_PER_HOST_PODCAST_META['wit']``.","operationId":"storyflo_host_podcast_wit_v1_podcasts_storyflo_wit_xml_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Host Podcast Wit","description":"Wit's daily comedy brief — see ``_PER_HOST_PODCAST_META['wit']``.","operationId":"storyflo_host_podcast_wit_v1_podcasts_storyflo_wit_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/v1/podcasts/creator/{tenant_slug}.xml":{"get":{"summary":"Creator Podcast Feed","description":"Per-creator claimable podcast feed — see the section comment on\n``_build_creator_podcast_rss`` for gate semantics.","operationId":"creator_podcast_feed_v1_podcasts_creator__tenant_slug__xml_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Creator Podcast Feed","description":"Per-creator claimable podcast feed — see the section comment on\n``_build_creator_podcast_rss`` for gate semantics.","operationId":"creator_podcast_feed_v1_podcasts_creator__tenant_slug__xml_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/podcasts/u/{token}.xml":{"get":{"summary":"Storyflo Private User Podcast","description":"Per-user private podcast feed — the token in the path IS the auth\n(Substack/Patreon model). Returns the listener's personalized daily\nbrief as a valid podcast RSS 2.0 + iTunes feed.\n\nHEAD is registered alongside GET because Apple Podcasts Connect HEAD-\nprobes feeds before queueing ingest — absent HEAD the probe sees 405\nand rejects the subscribe. An unknown / rotated-away token 404s.","operationId":"storyflo_private_user_podcast_v1_podcasts_u__token__xml_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Storyflo Private User Podcast","description":"Per-user private podcast feed — the token in the path IS the auth\n(Substack/Patreon model). Returns the listener's personalized daily\nbrief as a valid podcast RSS 2.0 + iTunes feed.\n\nHEAD is registered alongside GET because Apple Podcasts Connect HEAD-\nprobes feeds before queueing ingest — absent HEAD the probe sees 405\nand rejects the subscribe. An unknown / rotated-away token 404s.","operationId":"storyflo_private_user_podcast_v1_podcasts_u__token__xml_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/private-feed":{"get":{"summary":"Listener Get Private Feed Url","description":"Return (and lazily mint) the listener's PRIVATE podcast feed URL.\n\nAuth: the ``listener_token`` in the path IS the auth — same contract\nas ``GET /v1/listeners/{listener_token}`` (anyone with the token can\nread + manage the listener). On first call this mints the distinct\n``private_feed_token`` and persists it; subsequent calls return the\nsame URL so a subscribed podcast app keeps working.\n\nThe returned ``feed_url`` is www-canonical (the apex sub-path 404s)\nand copy-paste-ready for Apple Podcasts / Overcast / Pocket Casts /\nSnipd. ``instructions`` is a short human string for the /listen UI.","operationId":"listener_get_private_feed_url_v1_listeners__listener_token__private_feed_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Get Private Feed Url V1 Listeners  Listener Token  Private Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/podcasts/storyflo-{vertical}.xml":{"get":{"summary":"Storyflo Public Podcast By Vertical","description":"Per-vertical public podcast feed — one show per canonical\ntaxonomy slug (tech / health / young_moms / science /\nfinance / news). Each gets its own Spotify directory listing\nsubmission with vertical-flavored channel title + iTunes category.\n\nSpecial case — ``vertical=\"trending\"``: rounds out the URL pattern\nso every slug in PODCAST_VERTICAL_META resolves. Short-circuited\nthrough the all-trending root builder (``vertical=None``) since\n\"trending\" is a virtual cross-vertical slug, not a real vertical\ncolumn value in ``articles`` / ``persona_briefings`` (there are no\nrows where ``vertical='trending'`` — see the briefing-vertical\ninventory in MEMORY 2026-05-30). Without this short-circuit, the\nper-vertical query path would 200 but emit zero items.\n\n2026-06-05 · public URL alias resolution. The listener-facing show\nnamespace is ``parenting`` (the post-pivot rebrand) but the\ncanonical taxonomy key is still ``young_moms`` because the article-\nlevel vertical column hasn't been migrated yet. Without this alias\ntable, ``/v1/podcasts/storyflo-parenting.xml`` 404s even though the\nlisten-grid happily renders parenting cards. Spotify/Apple ingest\non the publicly-shared ``-parenting.xml`` URL — must resolve.\nAliases live HERE (request-edge) rather than in\n``PODCAST_VERTICAL_META`` so the canonical metadata stays single-\nsource-of-truth.","operationId":"storyflo_public_podcast_by_vertical_v1_podcasts_storyflo__vertical__xml_get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Storyflo Public Podcast By Vertical","description":"Per-vertical public podcast feed — one show per canonical\ntaxonomy slug (tech / health / young_moms / science /\nfinance / news). Each gets its own Spotify directory listing\nsubmission with vertical-flavored channel title + iTunes category.\n\nSpecial case — ``vertical=\"trending\"``: rounds out the URL pattern\nso every slug in PODCAST_VERTICAL_META resolves. Short-circuited\nthrough the all-trending root builder (``vertical=None``) since\n\"trending\" is a virtual cross-vertical slug, not a real vertical\ncolumn value in ``articles`` / ``persona_briefings`` (there are no\nrows where ``vertical='trending'`` — see the briefing-vertical\ninventory in MEMORY 2026-05-30). Without this short-circuit, the\nper-vertical query path would 200 but emit zero items.\n\n2026-06-05 · public URL alias resolution. The listener-facing show\nnamespace is ``parenting`` (the post-pivot rebrand) but the\ncanonical taxonomy key is still ``young_moms`` because the article-\nlevel vertical column hasn't been migrated yet. Without this alias\ntable, ``/v1/podcasts/storyflo-parenting.xml`` 404s even though the\nlisten-grid happily renders parenting cards. Spotify/Apple ingest\non the publicly-shared ``-parenting.xml`` URL — must resolve.\nAliases live HERE (request-edge) rather than in\n``PODCAST_VERTICAL_META`` so the canonical metadata stays single-\nsource-of-truth.","operationId":"storyflo_public_podcast_by_vertical_v1_podcasts_storyflo__vertical__xml_get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/p":{"get":{"summary":"Storyflo Public Podcast Short","description":"Short-URL alias for the flagship all-trending feed.\n\nHEAD is registered alongside GET so podcast directories that\nprobe-before-subscribe (some Apple ingest paths, several aggregator\nbots) don't see a 405. Starlette runs the GET body builder and\nstrips the body on HEAD; the only observable response is headers\n+ content-length, which is what those probes want.","operationId":"storyflo_public_podcast_short_p_get","responses":{"200":{"description":"Successful Response"}}},"head":{"summary":"Storyflo Public Podcast Short","description":"Short-URL alias for the flagship all-trending feed.\n\nHEAD is registered alongside GET so podcast directories that\nprobe-before-subscribe (some Apple ingest paths, several aggregator\nbots) don't see a 405. Starlette runs the GET body builder and\nstrips the body on HEAD; the only observable response is headers\n+ content-length, which is what those probes want.","operationId":"storyflo_public_podcast_short_p_get","responses":{"200":{"description":"Successful Response"}}}},"/p/{vertical}":{"get":{"summary":"Storyflo Public Podcast By Vertical Short","description":"Short-URL alias for per-vertical feeds. Delegates to the same\nbuilder + alias map as ``/v1/podcasts/storyflo-{vertical}.xml``.\nHEAD support — see ``storyflo_public_podcast_short`` for rationale.","operationId":"storyflo_public_podcast_by_vertical_short_p__vertical__get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Storyflo Public Podcast By Vertical Short","description":"Short-URL alias for per-vertical feeds. Delegates to the same\nbuilder + alias map as ``/v1/podcasts/storyflo-{vertical}.xml``.\nHEAD support — see ``storyflo_public_podcast_short`` for rationale.","operationId":"storyflo_public_podcast_by_vertical_short_p__vertical__get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/podcasts/{listener_token}.xml":{"get":{"summary":"Listener Podcast Feed","description":"RSS 2.0 + iTunes namespace feed of the listener's recent stories.\n\nThe token in the URL IS the credential — anyone with the URL can\nsubscribe in Spotify / Apple Podcasts / Audible. Up to 50 most\nrecent stories matching the listener's verticals (or unfiltered if\nverticals is empty), and only items whose audio is rendered + ready\nare emitted as ``<enclosure>`` items.\n\n2026-06-17 perf SEV · listener-RSS N+1 + projection fix. The Apple/\nSpotify crawler camps this URL at hundreds of builds/hr; the old\nhandler (a) ``SELECT *`` from articles — detoasting the wide JSON\ncolumns (``embedding``, ``parsed_json``, ``chapters_json``,\n``story_segments``, ``body_segments``, ``transcript_alignment``,\n``waveform_peaks``, ``audio_variants``) for up to 200 rows per\nbuild — and (b) issued one ``RenderJob ORDER BY queued_at DESC\nLIMIT 1`` query per article inside the items loop (~50 sequential\nround-trips per feed). On a pgbouncer-fronted MPG that monopolizes\npool checkouts, manifesting as PYTHON-FASTAPI-4F in Sentry (934\nevents/24h) and 17-18s warm /v1/stories/search probes (pool\nstarvation, not query-plan). The fix mirrors the proven public-RSS\npattern: ``_public_rss_article_load_only()`` for the Article SELECT,\n``_public_rss_latest_jobs_stmt()`` for the latest-job-per-article\nbatch (one window-function query, no per-item ``LIMIT 1`` round-\ntrips). Read-only path — routed through ``get_db_replica`` so\ncrawler traffic stays off the primary entirely.","operationId":"listener_podcast_feed_v1_podcasts__listener_token__xml_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/categories":{"get":{"summary":"Categories List","description":"All active personas as category cards. One row per persona with\npublished-briefing count so the frontend can render chip UI. 2026-05-20\nsprint: surfaces the 24 personas added 2026-05-19.","operationId":"categories_list_v1_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Categories List V1 Categories Get"}}}}}}},"/v1/today-episodes":{"get":{"summary":"Today Episodes","operationId":"today_episodes_v1_today_episodes_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","description":"Language for the grid. ``en`` (default) returns the 9 daily compilation episodes; ``es`` returns Spanish persona briefings (Diego Cripto, Valentina Mercados, Lucía Finanzas, Mateo Cultura, Tomás Tech Brief).","default":"en","title":"Lang"},"description":"Language for the grid. ``en`` (default) returns the 9 daily compilation episodes; ``es`` returns Spanish persona briefings (Diego Cripto, Valentina Mercados, Lucía Finanzas, Mateo Cultura, Tomás Tech Brief)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Today Episodes V1 Today Episodes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stories/verticals":{"get":{"summary":"Stories Verticals","description":"Distinct verticals across the corpus — populates filter chips on the\npublic discovery page. No rate limit; small response, hit once on\npage load.\n\nUnion of article-derived verticals (canonical six + ≥3-article extras)\nand persona-defined verticals (briefing-only verticals from the 24\npersonas added 2026-05-19). Registered BEFORE the ``/v1/stories/{slug}``\ncatch-all.\n\nPerformance (SEV 2026-06-15): the article-side ``list_verticals`` SELECT\nGROUP BY runs a full index-scan + HashAggregate over every row in\n``articles``. Cold prod wall-clock was timing out at 15s, blocking the\nchip row on ``storyflo.com/stories``. Wrapped in\n``verticals_cache.VerticalsCache`` (5-min TTL, single-flight) so the\ncold path fires at most once per worker per TTL window. Same shape\nas ``trending_cache`` / ``search_cache``. The persona-vertical SELECT\nis in the cached compute too — same dimensionality (~24 active\npersonas), and the merged ``verticals`` list is what downstream\ncallers consume, so caching the union not the parts matches the\ninvalidation cadence.\n\nPool-exhaustion fix 2026-06-21 (follow-up to PRs #657 + #658):\ndeliberately no ``db: AsyncSession = Depends(get_db_replica)``\nparameter. The chip row is on the storyflo.com/stories home path\nand shares the same anti-pattern that caused the 2026-06-20 P0 on\n/v1/search/hybrid — a request-scoped Depends session held across\n``cache.get_or_compute`` exhausts the basic-plan pool\n(POOL_SIZE=5 + MAX_OVERFLOW=3 = 8 total) on a single-flight wait\nstorm. Only the OWNER opens a short-lived replica session inside\n``_compute`` and releases it via ``_close_session_safely`` before\nreturn; non-owners hold zero connections while awaiting the shared\nFuture.","operationId":"stories_verticals_v1_stories_verticals_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Stories Verticals V1 Stories Verticals Get"}}}}}}},"/v1/signalflo/desk/{tenant_slug}/book":{"get":{"tags":["signalflo"],"summary":"Public Desk Book","description":"A desk's published book, newest first.\n\n404s for an unknown desk rather than returning an empty book, so a\nconsumer can tell \"this desk has published nothing yet\" apart from \"this\ndesk does not exist\" — the two need different copy on the page.\n\nUses ``session_scope()`` rather than ``Depends(get_db)``: this handler\nmakes three round-trips, and a request-scoped session held across all of\nthem is the pool-exhaustion pattern the static audit exists to catch.\nThe prod pool is 5 + 3 overflow, so a public endpoint holding a\nconnection longer than it needs is a real outage risk, not a style\npoint.","operationId":"public_desk_book_v1_signalflo_desk__tenant_slug__book_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(open|closed)$"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Public Desk Book V1 Signalflo Desk  Tenant Slug  Book Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stories/{slug}":{"get":{"summary":"Stories Get","description":"Return a single story by slug. Resolves real DB rows first, then\nfalls back to the demo set — so the public ``/story/[slug]`` page on\nthe frontend works for both onboarded publishers' articles and the\ncurated demo content. When the article has a completed render-job,\n``audio_url`` is populated so the frontend player can stream the\npre-rendered audio without paying for a second inference call.\n\nPerf 5-17: a single story is effectively immutable for the cache\nwindow — body_text + audio_url only flip when a render completes or\nan operator re-curates. 60s shared cache + SWR absorbs the bursty\n\"10 listeners open the same share link\" pattern (e.g., social shares)\nwithout hitting Fly for every fetch. The frontend story page was\ncold-fetching this endpoint on every nav and paying 18s on cold\nstart; the edge cache eliminates that path entirely after the first\nvisitor.\n\nPool-safety (2026-08-11 SEV) — TWO PHASES, ONE CONNECTION AT A TIME\n==================================================================\n\nThis handler used to carry ``db: AsyncSession = Depends(get_db)``.\nThat bound a PRIMARY connection to the whole request while phase 1\nbelow independently checked out a REPLICA connection — so every\n``/v1/stories/{slug}`` hit pinned **two** pooled connections at\nonce, and held the primary one completely idle across the expensive\nreplica read AND across the synchronous boto3 R2 ``head_object``\nround-trips ``get_story_full`` issues per audio variant\n(``app/search.py`` ``has_audio``).\n\nOn 2026-08-11 a ~3.4x burst on this endpoint (713 requests in five\nminutes, almost all ``daily-brief-*``) drove the single app task to\n99% CPU; with the event loop starved, every parked coroutine kept\nits connections, and BOTH pools hit their 15+5 ceiling within\n~90 seconds. Twelve endpoints returned\n``QueuePool limit of size 15 overflow 5 reached``.\n\nFix: run the expensive read holding ZERO primary connections, then\nopen a short-lived primary session for the cheap point-lookups and\nrelease it in a ``finally``. Peak concurrent connections per\nrequest drops from 2 to 1, and the primary is never held across R2.","operationId":"stories_get_v1_stories__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Stories Get V1 Stories  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/story/{slug}/cross-links":{"get":{"summary":"Story Cross Links","description":"Resolve the bi-parallel source cross-links for a story. For each\nout-bound source the daily brief cites, surface ONE of our related\ncontent pieces framed by the source publisher's opt-in status\n(partner vs. claim) and suppress any source under takedown. See\n``app/cross_link.resolve_cross_links`` for the locked contract.","operationId":"story_cross_links_v1_story__slug__cross_links_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Story Cross Links V1 Story  Slug  Cross Links Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/badge/resolve":{"get":{"summary":"Badge Resolve","description":"Resolve an arbitrary article URL to its Storyflo audio. Used by the\nnewsletter embed badge — newsletter authors drop a one-line `<a>` or\n`<iframe>` linking to ``/embed/badge?u=<url>`` and Storyflo handles\nthe rest:\n\n  - if we already rendered audio for that URL, return it immediately\n  - if not, queue a render-on-demand (priority lane) and return\n    ``status='rendering'`` so the badge can poll until ready\n  - tag every produced article with ``origin='subscribed'`` and\n    ``sender_email=<host>`` so per-publisher rev-share accrues from\n    the first listen, no claim flow required up front","operationId":"badge_resolve_v1_badge_resolve_get","parameters":[{"name":"u","in":"query","required":true,"schema":{"type":"string","minLength":4,"maxLength":2048,"title":"U"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Badge Resolve V1 Badge Resolve Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publishers/{tenant_slug}/tiers":{"get":{"summary":"Publisher Tiers Get","description":"Public endpoint — returns the subscription tiers for a publisher.\nUsed by the Subscribe modal on /story/[slug] before any auth.","operationId":"publisher_tiers_get_v1_publishers__tenant_slug__tiers_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Tiers Get V1 Publishers  Tenant Slug  Tiers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/subscribe/{tenant_slug}":{"post":{"summary":"Listener Subscribe Publisher","description":"Initiate a paid subscription from a listener to a publisher.\n\nReturns either a Stripe Checkout URL (fiat path) or a payment intent\npayload for the on-chain path. The actual subscription row lands in\n`publisher_subscriptions` with status='pending' until the rail\nconfirms the first payment via webhook (Stripe) or onchain\nconfirmation (x402).","operationId":"listener_subscribe_publisher_v1_listener__listener_token__subscribe__tenant_slug__post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubscribeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Subscribe Publisher V1 Listener  Listener Token  Subscribe  Tenant Slug  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/subscriptions":{"get":{"summary":"Listener Subscriptions List","description":"List a listener's active publisher subscriptions.","operationId":"listener_subscriptions_list_v1_listener__listener_token__subscriptions_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Subscriptions List V1 Listener  Listener Token  Subscriptions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/subscribers":{"get":{"summary":"Publisher Subscribers List","description":"Operator/publisher-dashboard endpoint — list paid subscribers\nfor export. Authenticates on the admin token for now; per-publisher\nauth lands when the publisher dashboard supports magic-link login.","operationId":"publisher_subscribers_list_v1_publisher__tenant_slug__subscribers_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Subscribers List V1 Publisher  Tenant Slug  Subscribers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/subscribers/import":{"post":{"summary":"Publisher Subscribers Import","description":"Import an existing audience into the email relay.\n\nAccepts a pasted CSV export (``csv``) or a structured ``emails`` list.\nEvery valid, deduped, not-already-present email becomes an ACTIVE\nfree-tier PublisherSubscription so the relay starts delivering narrated\nstories to that reader. Idempotent — re-importing the same list only\ncounts the already-present rows; existing paid rows are never\ndowngraded. Publisher-token auth.","operationId":"publisher_subscribers_import_v1_publisher__tenant_slug__subscribers_import_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubscriberImportRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubscriberImportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/email-relay":{"post":{"summary":"Publisher Email Relay Toggle","description":"Author flips the email-relay opt-in. When True, every rendered\narticle fans out to active PublisherSubscription.listener_email\nvia Postmark. Publisher-token auth so authors can self-serve from\n/publisher/dashboard.","operationId":"publisher_email_relay_toggle_v1_publisher__tenant_slug__email_relay_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EmailRelayToggle"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Email Relay Toggle V1 Publisher  Tenant Slug  Email Relay Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/monetization":{"get":{"summary":"Publisher Monetization Get","description":"Current 'charge agents for my audio' settings. Publisher-token auth.","operationId":"publisher_monetization_get_v1_publisher__tenant_slug__monetization_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Monetization Get V1 Publisher  Tenant Slug  Monetization Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Publisher Monetization Set","description":"Publisher flips the 'charge agents for my audio' toggle + sets the\nper-fetch price (USDC, stored as 6-dp micros). Inert until the operator\nflips X402_AUDIO_RESOURCES on prod. Publisher-token auth so authors\nself-serve from /publisher/dashboard.","operationId":"publisher_monetization_set_v1_publisher__tenant_slug__monetization_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MonetizationSettings"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Monetization Set V1 Publisher  Tenant Slug  Monetization Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/channels":{"get":{"summary":"Publisher Channels","description":"Return the full 'where does my content flow?' channel map for a\npublisher — powers the connect-once value surface on onboarding + the\ndashboard 'Distribution' tab. Publisher-token auth (same gate as the\nrest of the dashboard) so private feed patterns + subscriber counts\naren't leaked for arbitrary slugs.","operationId":"publisher_channels_v1_publisher__tenant_slug__channels_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Channels V1 Publisher  Tenant Slug  Channels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/plan/checkout":{"post":{"summary":"Publisher Plan Checkout","description":"Start a Stripe Checkout session for an author Plus / Pro upgrade.\n\nAuth: publisher access_token (same gate as the dashboard).\nReturns ``{\"checkout_url\": \"...\"}`` — the frontend redirects the\nbrowser; Stripe handles card collection + activation; the\n/v1/stripe/webhook fires customer.subscription.created and our\npayments_publisher.handle_publisher_subscription_event flips\nplan_tier on the publisher row.\n\n503 if STRIPE_PRICE_AUTHOR_PLUS / _PRO Fly secrets aren't set\nyet (operator activation gate). 400 on tier other than\nplus|pro — Free is just \"don't pay us\", no checkout needed.","operationId":"publisher_plan_checkout_v1_publisher__tenant_slug__plan_checkout_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PlanCheckoutBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Plan Checkout V1 Publisher  Tenant Slug  Plan Checkout Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/plan":{"get":{"summary":"Publisher Plan Status","description":"Read-only status of the publisher's current plan tier. Powers\nthe dashboard banner + the pricing page's \"your current plan\"\nhighlight. Publisher-token auth so we don't leak tier state to\nanyone scraping public slugs.","operationId":"publisher_plan_status_v1_publisher__tenant_slug__plan_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Plan Status V1 Publisher  Tenant Slug  Plan Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/byo-tts":{"post":{"summary":"Publisher Byo Tts Set","description":"Pro-tier only: store the publisher's BYO TTS key (Fernet-\nencrypted at rest). Free / Plus return 403 — the upgrade prompt\non the frontend should route the publisher to /publisher/{slug}/\npricing instead.\n\nVerification mirrors the listener-side flow: a probe call against\nthe provider rejects typos with 422 before we persist anything.\nOn success we stamp byo_tts_provider + byo_tts_encrypted_key on\nthe Publisher row; subsequent renders pull the key out, decrypt\nin-process, and use it for the synthesis call.\n\nPool-safety (2026-08-11 SEV) — THREE PHASES\n===========================================\n\n``_byo.verify_key`` is a 10s-timeout httpx probe against the\nprovider. Holding ``Depends(get_db)`` across it pins a writer\nconnection for the full round-trip. Same shape, same fix as\n``app/routes/byo_tts.py::listener_byo_tts_set``: gate on a short\nsession, release it, probe holding zero connections, then persist\non a second short session.","operationId":"publisher_byo_tts_set_v1_publisher__tenant_slug__byo_tts_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherByoTtsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Byo Tts Set V1 Publisher  Tenant Slug  Byo Tts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Publisher Byo Tts Clear","description":"Clear the publisher BYO TTS key. Idempotent. Available to any\nplan tier — downgrading from Pro should not strand the key in\nstorage; explicit clear is always allowed.","operationId":"publisher_byo_tts_clear_v1_publisher__tenant_slug__byo_tts_delete","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Byo Tts Clear V1 Publisher  Tenant Slug  Byo Tts Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/voicebox":{"post":{"summary":"Publisher Voicebox Set","description":"Set (activate) the publisher's VoiceBox voice profile.\n\nGated: 503 when VOICEBOX_ENABLED is off; 403 for free tier\n(plus/pro only); 422 when consent is not granted. Any previously\n``active`` profile for this publisher is revoked first, so a publisher\nhas at most one active voice at a time. Takes effect on the next\nrender dequeue (fail-open — an inactive/absent profile is a no-op).","operationId":"publisher_voicebox_set_v1_publisher__tenant_slug__voicebox_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherVoiceboxBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Voicebox Set V1 Publisher  Tenant Slug  Voicebox Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Publisher Voicebox Revoke","description":"Revoke the publisher's active VoiceBox profile (status → revoked).\n\nIdempotent, and allowed on ANY tier / regardless of the flag —\nrevocation is a safety operation and must never be stranded (mirrors\nthe byo-tts clear semantics). Takes effect on the next render dequeue\n(same-day): queued jobs re-resolve and fall back to the host voice.","operationId":"publisher_voicebox_revoke_v1_publisher__tenant_slug__voicebox_delete","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Voicebox Revoke V1 Publisher  Tenant Slug  Voicebox Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Publisher Voicebox Status","description":"Read the publisher's active VoiceBox profile + eligibility.","operationId":"publisher_voicebox_status_v1_publisher__tenant_slug__voicebox_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Voicebox Status V1 Publisher  Tenant Slug  Voicebox Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/voicebox/enroll":{"post":{"summary":"Publisher Voicebox Enroll","description":"Self-serve enrollment (Stage 2) — build the publisher's OWN voice from a\nshort consent+sample recording (ElevenLabs Instant Voice Cloning) and stage\nit as a ``verified`` profile awaiting activation.\n\nSame gates as ``…/voicebox`` set: 503 when VOICEBOX_ENABLED is off, 403 for\nfree tier (plus/pro only), 422 when consent is not granted. The created\nvoice does NOT render until the publisher activates it (POST …/activate) —\nIVC is immediate, so ``verified`` is set on success, but ``active`` stays a\ndeliberate, publisher-approved step (mirrors the \"preview, then activate\"\nflow). At most one in-flight (pending/verified) enrollment per publisher.","operationId":"publisher_voicebox_enroll_v1_publisher__tenant_slug__voicebox_enroll_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_publisher_voicebox_enroll_v1_publisher__tenant_slug__voicebox_enroll_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Voicebox Enroll V1 Publisher  Tenant Slug  Voicebox Enroll Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/voicebox/activate":{"post":{"summary":"Publisher Voicebox Activate","description":"Activate the publisher's verified voice — flip their most-recent\n``verified`` profile to ``active`` (revoking any other active), so the\nresolver narrates their catalog in it on the next render dequeue. 503 when\nthe flag is off; 404 when there's no verified voice to activate.","operationId":"publisher_voicebox_activate_v1_publisher__tenant_slug__voicebox_activate_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Voicebox Activate V1 Publisher  Tenant Slug  Voicebox Activate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/newsletter-discovery/send-onboard":{"post":{"summary":"Admin Send Onboard Email","description":"Fire the auto-onboard outreach email to a newsletter author.\n\nGenerates a one-time claim token, persists it on a stub Publisher\nrow keyed by `tenant_slug = sender_domain`, then sends via Postmark\nwith a magic link to /publisher/claim/<token>. Idempotent — re-runs\nrotate the token rather than duplicating emails.","operationId":"admin_send_onboard_email_v1_admin_newsletter_discovery_send_onboard_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AuthorOnboardRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Send Onboard Email V1 Admin Newsletter Discovery Send Onboard Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/referrals":{"get":{"summary":"Publisher Referrals Summary","description":"C3 · author-side referral dashboard payload.\n\nCounts listeners attributed to this publisher via `?via=<slug>` plus\nthe breakdown of how many have converted to Plus/Pro tiers (the\nupgrade events that drive the 10% rev-share for Pro authors). Free\n+ Plus publishers see the same data as a \"what you'd earn at Pro\"\nteaser; Pro publishers see live rev-share.\n\nReturns lifetime totals — not bucketed by month — until the rev-share\naccounting ledger ships. Token-gated like other dashboard endpoints.","operationId":"publisher_referrals_summary_v1_publisher__tenant_slug__referrals_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Referrals Summary V1 Publisher  Tenant Slug  Referrals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/publishers/{publisher_slug}/badge.json":{"get":{"summary":"Embed Publisher Badge","description":"Public, unauthenticated badge payload for the embeddable\n\"Audio on Storyflo\" pill. Drop-in HTML snippet on a publisher's\nown site (single ``<iframe>``) fetches this to render instant\nsocial-proof: total lifetime listens + the timestamp of the most\nrecent rendered briefing.\n\nCDN-cacheable for 5 minutes. Returns 200 with zeros when the\npublisher is unknown so the badge always renders something —\npublishers who paste the snippet before claiming still see the\nstoryflo branding and a click-through to the homepage with their\nslug as `?ref=` for attribution.","operationId":"embed_publisher_badge_v1_embed_publishers__publisher_slug__badge_json_get","parameters":[{"name":"publisher_slug","in":"path","required":true,"schema":{"type":"string","title":"Publisher Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embed Publisher Badge V1 Embed Publishers  Publisher Slug  Badge Json Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/by-slug/{tenant_slug}/claim-info":{"get":{"summary":"Publisher Claim Info By Slug","description":"Public-by-slug lookup so the referral-style claim URL\n(/publisher/claim/auto?ref=<slug>, sent by already-claimed authors\nto their peers) can resolve to a real claim link without leaking\na hot claim_token in the email body.\n\nReturns a never-fails 200 with `{exists, name, claim_url, claimed,\nlatest_article_slug}`. Mints a fresh claim_token on the publisher\nif one doesn't already exist so the link always works. When the\npublisher doesn't exist at all we return `exists=False` so the\nlanding page can show a friendly 'we don't have this newsletter\nyet, forward us an issue' CTA.","operationId":"publisher_claim_info_by_slug_v1_publisher_by_slug__tenant_slug__claim_info_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Claim Info By Slug V1 Publisher By Slug  Tenant Slug  Claim Info Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/claim/{claim_token}":{"get":{"summary":"Publisher Claim Lookup","description":"Resolve a claim token to a stub publisher row + a sample of\nalready-narrated articles. The frontend /publisher/claim/[token]\npage leads with the audio samples so the author hears their own\nnewsletter narrated before they're asked for a wallet address.\n\nResponse shape: publisher metadata, the three most-recent ready-\nto-play rendered articles (title + slug + audio_url + opus_url +\nduration), and a small projection card (article count + total\nlisten seconds) anchored to real catalogue numbers rather than\nhypothetical figures.","operationId":"publisher_claim_lookup_v1_publisher_claim__claim_token__get","parameters":[{"name":"claim_token","in":"path","required":true,"schema":{"type":"string","title":"Claim Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Claim Lookup V1 Publisher Claim  Claim Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Publisher Claim Accept","description":"Author accepts the claim — burns the token, persists payout\nconfig + display name. Returns a richer payload so the frontend\ncan deep-link straight into the dashboard and offer the Stripe\nConnect handoff inline (no second round-trip):\n\n  - dashboard_url: signed magic link with slug+token\n  - welcome_email_queued: bool — was the dashboard-ready email sent\n  - stripe_connect_url: one-time Connect onboarding link (when subs on)","operationId":"publisher_claim_accept_v1_publisher_claim__claim_token__post","parameters":[{"name":"claim_token","in":"path","required":true,"schema":{"type":"string","title":"Claim Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ClaimAcceptRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Claim Accept V1 Publisher Claim  Claim Token  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/claim/{claim_token}/resend-access":{"post":{"summary":"Publisher Claim Resend Access","description":"Rescue path for the already-claimed 409: re-send the dashboard\nmagic-link email to the publisher's own contact_email. The token in\nthe path must hash-match ``raw_json.claimed_token_history`` on a\nclaimed publisher — a live (unclaimed) token 404s here, because the\nclaim page itself is the right flow while the token is still hot.\n\nRate-limited to 1/hour per publisher via ``kv_store`` — possession\nof a burned token must not become an email-flood primitive. The\nemail goes only to the address already on file, never to the caller.","operationId":"publisher_claim_resend_access_v1_publisher_claim__claim_token__resend_access_post","parameters":[{"name":"claim_token","in":"path","required":true,"schema":{"type":"string","title":"Claim Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Claim Resend Access V1 Publisher Claim  Claim Token  Resend Access Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stripe/webhook":{"post":{"summary":"Stripe Webhook","description":"Stripe webhook for subscription events. Verifies the signature\nvia STRIPE_WEBHOOK_SECRET, then routes:\n\n  checkout.session.completed       → mark sub active, set next_renewal\n  invoice.payment_succeeded        → bump next_renewal forward\n  invoice.payment_failed           → mark failed\n  customer.subscription.deleted    → mark cancelled","operationId":"stripe_webhook_v1_stripe_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Stripe Webhook V1 Stripe Webhook Post"}}}}}}},"/v1/x402/subscribe/confirm":{"post":{"summary":"X402 Subscribe Confirm","description":"Confirm an on-chain USDC payment for a publisher subscription.\nThe frontend calls this AFTER the wallet returns a successful\nsendTransaction — we verify the tx on-chain (Base mainnet RPC: receipt\nstatus + a USDC Transfer of >= the plan price to the PUBLISHER's payout\nwallet) BEFORE marking the subscription active.\n\nTrust model: the on-chain verification IS the authorization — no valid,\ncorrectly-priced USDC payment to the publisher's payout wallet means no\nactivation, so guessing a ``subscription_id`` (a DB PK) buys nothing. The\nreceiver verified here MUST match the wallet the payer was instructed to\npay in ``listener_subscribe_publisher`` (``pub.payout_wallet_address``),\nnot the platform x402 receiver. The chain is pinned to base-mainnet so a\nfree testnet-USDC transfer cannot be passed off as payment. The ``tx_hash``\nis required to be globally unused (across subscriptions and X402Payment\nreceipts) so a settled payment can't be replayed onto another sub.\n\nPool-safety: this handler does NOT take ``Depends(get_db)``. It opens a\nshort read session, releases it, runs the external Base-RPC verification\nwith NO pooled connection held, then opens a short write session — the\ncanonical fix for the pool-exhaustion anti-pattern\n(tests/test_pool_exhaustion_static_audit.py). Holding a connection across\n``_eth_get_receipt`` would be exactly that anti-pattern.\n\nKnown residual (tracked follow-up, needs a coordinated FE change): the\npayment is bound to the publisher + amount but NOT to the individual\nsubscriber, so a same-publisher/same-tier front-runner could confirm THEIR\npending sub with a victim's just-broadcast tx. Closing it requires binding\na listener/publisher auth token on confirm (verify sub.listener_email) —\na breaking FE-contract change deferred to its bridge PR. This is strictly\nnarrower than the free-activation hole this fix closes.","operationId":"x402_subscribe_confirm_v1_x402_subscribe_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_X402ConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response X402 Subscribe Confirm V1 X402 Subscribe Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/subscription-revenue":{"get":{"summary":"Publisher Subscription Revenue","description":"Per-publisher subscription revenue rollup. MRR, paid subscriber\ncount, churn signal (cancelled in window), payment-rail mix.\nPublisher-token auth so authors self-serve from /publisher/dashboard.","operationId":"publisher_subscription_revenue_v1_publisher__tenant_slug__subscription_revenue_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Subscription Revenue V1 Publisher  Tenant Slug  Subscription Revenue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/subscription/{subscription_id}/cancel":{"post":{"summary":"Listener Cancel Subscription","description":"Listener cancels their subscription. For Stripe-rail subs we\ncancel via the Stripe API; for x402 we just mark cancelled and let\nthe next renewal fail naturally (no auto-charge for crypto).","operationId":"listener_cancel_subscription_v1_listener__listener_token__subscription__subscription_id__cancel_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Cancel Subscription V1 Listener  Listener Token  Subscription  Subscription Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/economics":{"get":{"summary":"Admin Economics","description":"Returns platform-wide cost vs revenue for the trailing window.\n\n``totals`` reports ACTUALS ONLY (2026-07-16 revenue-accuracy fix —\nmodeled numbers must never masquerade as actuals):\n\n  Cost    = sum(render_jobs.cost_micros) for completed jobs in window\n  Revenue = settled ``embedder_revenue_events`` rollup (money that\n            actually credited an embedder balance) + settled x402\n            receipts (``x402_payments``), both on the same window\n  Margin  = Revenue - Cost\n\n``plays.ad_revenue_micros`` is deliberately NOT read here: the\ncolumn has no writer anywhere, so summing it always yields $0 —\nreal ad / affiliate money lands in ``embedder_revenue_events``.\n\nThe modeled Plus-ARPU projection (plus_listens × the $0.05\n``plus_subscriber`` benchmark) lives under\n``assumptions.modeled.plus_arpu_usd`` and is EXCLUDED from\n``totals.total_revenue_usd`` / ``totals.margin_usd``. Field names\nare kept for FE compatibility; only the semantics were corrected\n(pre-fix responses summed the modeled term into both totals and\nread ad revenue from the always-zero plays column).\n\nPer-publisher breakdown shows cost-per-publisher so the operator\ncan see which publishers cost the most to render. Forward-looking\nprojections use ``app.economics.REVENUE_DEFAULTS_MICROS_PER_LISTEN``\nas the unit value and live under ``assumptions``.","operationId":"admin_economics_v1_admin_economics_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Economics V1 Admin Economics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers":{"post":{"summary":"Admin Publisher Create","description":"Create a publisher record. Operator-gated by api_key for v0 — full\nself-serve onboarding will land alongside storyflo-platform's auth\nflow. Idempotent on tenant_slug.","operationId":"admin_publisher_create_v1_admin_publishers_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin Publisher List","operationId":"admin_publisher_list_v1_admin_publishers_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublisherView"},"title":"Response Admin Publisher List V1 Admin Publishers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/onboard":{"post":{"summary":"Publisher Onboard","description":"Self-serve publisher signup. Creates a Publisher row, mints an\naccess_token, and registers an RSS IntakeSource in the *pending*\nstate. Operators flip status=active after a quick vetting pass; the\nintake worker only polls active sources.","operationId":"publisher_onboard_v1_publisher_onboard_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherOnboardRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherOnboardResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/business/apply":{"post":{"summary":"Business Apply","description":"Public apply-form endpoint. Anyone can POST here — we file a\npending PublisherApplication row + ping the operator via Telegram +\nnotification email. Does NOT auto-opt-in; operator reviews via\n``GET /v1/admin/publisher-applications`` then promotes via\n``POST /v1/admin/publisher-applications/{id}/approve``.\n\nIdempotent on the (contact_email, publication_name) pair within a\n24h window — re-submissions return the existing pending row instead\nof cluttering the inbox.","operationId":"business_apply_v1_business_apply_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessApplyRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessApplyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publisher-applications":{"get":{"summary":"Admin List Publisher Applications","description":"Operator review queue. Defaults to pending applications, sorted\nnewest-first. Auth: X-Storyflo-Email-Token (admin).","operationId":"admin_list_publisher_applications_v1_admin_publisher_applications_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"pending","title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublisherApplicationView"},"title":"Response Admin List Publisher Applications V1 Admin Publisher Applications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/opt-in":{"post":{"summary":"Admin Publisher Opt In","description":"Direct operator opt-in endpoint — flips opt_in_at + onboarded_at,\nmints hex_code, persists payout fields. Idempotent: re-running\nagainst an already-opted-in publisher returns the existing record\n(with payout fields updated in-place).\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_publisher_opt_in_v1_admin_publishers__tenant_slug__opt_in_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherOptInRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherOptInResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publisher-applications/{application_id}/approve":{"post":{"summary":"Admin Approve Publisher Application","description":"Operator approves a pending application. Internally calls the\nopt-in helper (sets opt_in_at, mints hex_code, persists payout\nfields) then marks the application status=approved and emails the\napplicant their welcome packet (hex + embed snippet + dashboard\nlink). Idempotent — re-approving an approved application returns\nthe existing publisher row without re-sending the welcome email.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_approve_publisher_application_v1_admin_publisher_applications__application_id__approve_post","parameters":[{"name":"application_id","in":"path","required":true,"schema":{"type":"string","title":"Application Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherApplicationApproveRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherApplicationApproveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{publisher_id}/resend-welcome":{"post":{"summary":"Admin Resend Publisher Welcome","description":"Re-send the welcome packet (HEX + embed + dashboard) to an existing\npublisher — recovery for a welcome email that failed on approval (e.g. a\ntransient provider error). Idempotent from the operator's side: safe to\ncall repeatedly. Auth: admin token (X-Storyflo-Email-Token).","operationId":"admin_resend_publisher_welcome_v1_admin_publishers__publisher_id__resend_welcome_post","parameters":[{"name":"publisher_id","in":"path","required":true,"schema":{"type":"string","title":"Publisher Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Resend Publisher Welcome V1 Admin Publishers  Publisher Id  Resend Welcome Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/intake/web":{"post":{"summary":"Web Intake","description":"Single-URL ingest from the listener-side ``Send via web`` form.\nReuses the email-intake pipeline (URL extraction, fetch, parse,\ninsert under storyflo-sends, enqueue render-job).","operationId":"web_intake_v1_intake_web_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebIntakeRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Web Intake V1 Intake Web Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/queue/add":{"post":{"summary":"Listener Queue Add","description":"Ingest a URL on behalf of a listener. Reuses /v1/intake/web's\npipeline and additionally records a ListenerHistory stub (0 seconds,\ncompleted=false) so the article surfaces on /listen/queue tagged as\nlistener-added. Best-effort: returns 200 on dedupe/no-extract.\n\nSecurity: the listener_token is the bearer-secret for this endpoint\nso token lookup must NOT leak (a) whether a token exists via 404 vs\n200 timing, (b) whether a token is malformed vs unknown vs revoked\nvia distinguishable error strings. We fetch by indexed token,\nconstant-time compare to defeat a hypothetical prefix-scan, floor\nthe not-found latency, and use a single generic error message.\nPer-IP rate-limit coverage is provided by the route's\n``enforce_rate_limit`` dependency (queue-add bucket, 60/min).","operationId":"listener_queue_add_v1_listener__listener_token__queue_add_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListenerQueueAddRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Queue Add V1 Listener  Listener Token  Queue Add Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/intake/email":{"post":{"summary":"Email Intake","description":"Inbound email handler — story@storyflo.com forwarded payloads.\n\nAuth: ``X-Storyflo-Email-Token`` header OR ``?token=`` query param\nMUST match ``EMAIL_INTAKE_TOKEN`` in the env. Header preferred; query\nparam exists for inbound providers (e.g. Postmark) whose UI doesn't\nexpose custom-header config reliably.\n\nBehavior: extracts URLs from text/html, fetches each, parses title +\nbody, inserts as Articles under the canonical ``storyflo-sends``\npublisher, enqueues render-jobs.","operationId":"email_intake_v1_intake_email_post","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntakeRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Email Intake V1 Intake Email Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/refresh-body/{slug}":{"post":{"summary":"Admin Refresh Body","description":"Re-fetch an article's source URL and re-extract body_text using\nthe current readability extractor. Use this to backfill articles\ningested before the extractor was wired up. Auth: admin token\n(X-Storyflo-Email-Token; ADMIN_TOKEN or legacy EMAIL_INTAKE_TOKEN).","operationId":"admin_refresh_body_v1_admin_refresh_body__slug__post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Refresh Body V1 Admin Refresh Body  Slug  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audit":{"get":{"summary":"Admin Audit List","description":"D-1.13 · paginated admin-audit reader. Filters by action when\ngiven, returns most-recent first. Append-only model — no edit /\ndelete surface.","operationId":"admin_audit_list_v1_admin_audit_get","parameters":[{"name":"action","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Audit List V1 Admin Audit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/metrics/bi-daily":{"get":{"summary":"Admin Metrics Bi Daily","description":"Bi-daily snapshot for the Telegram digest routine. One curl call\nreturns all the numbers the routine needs to post a clean digest:\n  - articles: ingested counts (24h / 7d / total alive)\n  - briefings: published counts (24h / total / by-vertical 24h)\n  - sources: active intake count by status\n  - invites: sent/accepted from outreach funnel\n  - render_queue: depth by status","operationId":"admin_metrics_bi_daily_v1_admin_metrics_bi_daily_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Metrics Bi Daily V1 Admin Metrics Bi Daily Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bot-traffic/snapshot":{"get":{"summary":"Admin Bot Traffic Snapshot","description":"Bot vs human request rollup over the last 24h + 7d (task #32).\n\nL3 ad-inventory monetization needs honest impression counts —\ncrawler + indexer + generic-HTTP-library traffic distorts the\nheadline number we'd quote to advertisers. This endpoint returns\nthe per-cohort counts so the rollup can subtract the bot share.\n\nIn-process counter (per worker, 1h buckets, 7d window). Operator\nauth via ``x-storyflo-email-token`` like the rest of /v1/admin.\n\nClassification rules live in ``app/bot_classifier.py``; MCP /\nClaude / Cursor / podcast-client UAs classify as human.","operationId":"admin_bot_traffic_snapshot_v1_admin_bot_traffic_snapshot_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Bot Traffic Snapshot V1 Admin Bot Traffic Snapshot Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/distribution/snapshot":{"get":{"summary":"Admin Distribution Snapshot","description":"Distribution & Discovery Observability snapshot — one shot of\nhow content is flowing through the platform right now: agents (per\ntool, per client, revenue), listeners (plays, completions, plus\nshare), the intake funnel (intake → quality → summary → rendered\n→ played) across 24h/7d/30d windows, subscription state, and the\nshare of in-window published stories that have a matched Kalshi\nevent contract.\n\nCached for 15 min via ``kv_store`` row ``distribution:v1`` — the\noperator can read this on every page load without grinding the DB.\nPass ``?refresh=1`` to force a recompute (use sparingly; the\nqueries are cheap individually but the consolidated run touches\nfive tables).\n\nSee ``app/distribution_observability.py`` for the column semantics\nand the query bodies.","operationId":"admin_distribution_snapshot_v1_admin_distribution_snapshot_get","parameters":[{"name":"refresh","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Refresh"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Distribution Snapshot V1 Admin Distribution Snapshot Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/flywheel/snapshot":{"get":{"summary":"Admin Flywheel Snapshot","description":"Agentic lead-gen Flywheel snapshot — the growth loop read end-to-\nend as one funnel: discovery → agent acquisition → agent activation →\npull-through → listener conversion → retention → monetization, with\nthe headline **flywheel coefficient** (partner surfaces created ÷ new\nlisteners) on top.\n\nFan-reads the (already-cached) Distribution & Discovery snapshot for\nthe agent / listener / subscription / discovery-source stages, then\nlayers on the flywheel-specific reads:\n\n  * cross-embedder ``embedder_revenue_events`` rollup folded into the\n    clicks→signups→deposits→sales ladder + a house-vs-attributed\n    split (our first-party surface vs real partner embedders);\n  * new-vs-returning MCP callers (first-seen vs repeat);\n  * the pull-through stage (plays + subs carrying an embedder\n    attribution) — ``instrumented: false`` until the additive\n    ``source_embedder_slug`` / ``referred_by_embedder_slug`` columns\n    land, then it lights up automatically (runtime column probe).\n\nCached for 15 min via ``kv_store`` row ``flywheel:v1`` — poll-safe.\nPass ``?refresh=1`` to force a recompute. See\n``app/flywheel_observability.py`` for the query bodies + semantics.","operationId":"admin_flywheel_snapshot_v1_admin_flywheel_snapshot_get","parameters":[{"name":"refresh","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Refresh"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Flywheel Snapshot V1 Admin Flywheel Snapshot Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/onboarding/funnel":{"get":{"summary":"Admin Onboarding Funnel","description":"4-step listener onboarding funnel for the trailing ``days``.\n\nComputes day-by-day signup → email-confirm → first-track-played\n→ 2nd-day-return counts so the operator can baseline the funnel\nbefore the Stream 2a/2b UX redesigns ship + measure the delta\nafter. See ``app/onboarding_funnel.py`` for the column semantics\nof each step.\n\nReturns the per-day shapes plus a trailing-window summary so a\nsingle ``curl`` is enough to read the snapshot without\npost-processing on the operator side. Defaults to 14 days\n(matches the 2-week stabilization window); capped at 90 to keep\nthe SQL bounded.","operationId":"admin_onboarding_funnel_v1_admin_onboarding_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":14,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Onboarding Funnel V1 Admin Onboarding Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles":{"get":{"summary":"Admin Articles List","description":"Curation-focused article browser for /admin/curate. Returns\narticles with their latest render-job status so the operator can\npick what trends in the public daily RSS broadcast (is_landing=true).\n\nFilters:\n  - q: substring on title or slug\n  - vertical: exact match\n  - audio_status: ready/rendering/queued/failed/none\n  - landing_only: only currently-featured articles","operationId":"admin_articles_list_v1_admin_articles_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"audio_status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Status"}},{"name":"landing_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Landing Only"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles List V1 Admin Articles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/queue":{"get":{"summary":"Admin Curation Queue","description":"List articles currently waiting on editor review.\n\nDefault scope = ``curation_status='pending_review'`` ordered by\noldest-first so the operator clears the backlog FIFO. Filter by\n``vertical`` to focus on one queue at a time.","operationId":"admin_curation_queue_v1_admin_curation_queue_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Queue V1 Admin Curation Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{article_id}/approve":{"post":{"summary":"Admin Curation Approve","description":"Approve a pending article. Flips status to ``approved``, persists\nreviewer + notes + timestamp, then enqueues the render job that the\nintake path skipped. 409 if the article isn't currently\n``pending_review`` so a double-click can't enqueue a duplicate\nrender.","operationId":"admin_curation_approve_v1_admin_curation__article_id__approve_post","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CurationApproveBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Approve V1 Admin Curation  Article Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{article_id}/reject":{"post":{"summary":"Admin Curation Reject","description":"Reject a pending article. Status flips to ``rejected`` and the\nrow stays in the DB for audit, but is invisible on listener-facing\nsurfaces. Notes are REQUIRED — they're the operator's editorial\nrationale, useful for trend analysis and dispute handling.","operationId":"admin_curation_reject_v1_admin_curation__article_id__reject_post","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CurationRejectBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Reject V1 Admin Curation  Article Id  Reject Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{article_id}/auto-approve":{"post":{"summary":"Admin Curation Auto Approve","description":"Escape-hatch: bypass review and treat the article as if its\nvertical never required it. Sets status → ``auto_approved`` and\nenqueues the render. Useful when the operator needs to clear a\nbacklog quickly without leaving an editorial note on each row.","operationId":"admin_curation_auto_approve_v1_admin_curation__article_id__auto_approve_post","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Auto Approve V1 Admin Curation  Article Id  Auto Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{vertical}":{"get":{"summary":"Admin Curation List","description":"List editorial picks for a vertical (7 slugs + 'flagship').\n\nDefault scope = currently-active picks (no expires_at, or future\nexpires_at). Pass ``include_expired=true`` to see the full history\nfor audit/rotation work.","operationId":"admin_curation_list_v1_admin_curation__vertical__get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}},{"name":"include_expired","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Expired"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation List V1 Admin Curation  Vertical  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Admin Curation Pin","description":"Pin an article to a vertical. Idempotent on (vertical, article)\n— re-pinning the same slug updates position/note/expires_at on the\nexisting row rather than inserting a duplicate.","operationId":"admin_curation_pin_v1_admin_curation__vertical__post","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EditorialPickCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Pin V1 Admin Curation  Vertical  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{pick_id}":{"patch":{"summary":"Admin Curation Update","description":"Update a pin's position / expires_at / note. 404 on unknown id.","operationId":"admin_curation_update_v1_admin_curation__pick_id__patch","parameters":[{"name":"pick_id","in":"path","required":true,"schema":{"type":"string","title":"Pick Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EditorialPickPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Update V1 Admin Curation  Pick Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Admin Curation Unpin","description":"Unpin (delete) an editorial pick. 404 on unknown id.","operationId":"admin_curation_unpin_v1_admin_curation__pick_id__delete","parameters":[{"name":"pick_id","in":"path","required":true,"schema":{"type":"string","title":"Pick Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Unpin V1 Admin Curation  Pick Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/curation/{vertical}/reorder":{"post":{"summary":"Admin Curation Reorder","description":"Bulk-reorder picks in a vertical. The supplied ``pick_ids`` list\nbecomes the new positional order (index 0 = position 0, index 1 =\nposition 1, etc.). Ids not belonging to ``vertical`` are silently\nskipped — the UI passes its current vertical's visible list and\nwe don't want a stale tab to corrupt another vertical's order.","operationId":"admin_curation_reorder_v1_admin_curation__vertical__reorder_post","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EditorialPickReorder"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Curation Reorder V1 Admin Curation  Vertical  Reorder Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/{slug}":{"patch":{"summary":"Admin Patch Article","description":"Update vertical / title / url on an article. Used to re-categorize\nrows that landed with the wrong vertical (e.g. email-intake articles\nflagged 'sends' that are actually 'tech' / 'culture' / etc.) without\ndeleting and re-ingesting.","operationId":"admin_patch_article_v1_admin_articles__slug__patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ArticlePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Article V1 Admin Articles  Slug  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Admin Delete Article","description":"Hard-delete an article and its render jobs. Use to clean up\njunk ingested before a filter shipped, or to retract a wrong send.\nAuth: admin token (X-Storyflo-Email-Token; ADMIN_TOKEN or legacy\nEMAIL_INTAKE_TOKEN).","operationId":"admin_delete_article_v1_admin_articles__slug__delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Delete Article V1 Admin Articles  Slug  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/rewrite-cover-urls":{"post":{"summary":"Admin Rewrite Cover Urls","description":"One-shot backfill — rewrite cover_image_url values that were\npersisted as ``s3://...`` (frontend can't load) into the proxy URL\n``/v1/article-cover/{slug}.png``. The image bytes are already in R2\nso no regeneration is needed; only the URL persisted on the row\nneeds updating.","operationId":"admin_rewrite_cover_urls_v1_admin_articles_rewrite_cover_urls_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Rewrite Cover Urls V1 Admin Articles Rewrite Cover Urls Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-covers":{"post":{"summary":"Admin Backfill Covers","description":"Generate cover images for up to ``limit`` articles that don't have\none yet. Runs serially with per-article exception isolation so one\nbad provider call doesn't poison the batch. Returns counts.\n\nIdempotent — articles with ``cover_image_url`` already set are\nskipped. Safe to re-run after a partial failure.","operationId":"admin_backfill_covers_v1_admin_articles_backfill_covers_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Covers V1 Admin Articles Backfill Covers Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/article-cover/{slug_with_ext}":{"get":{"summary":"Article Cover Proxy","description":"Stream a per-article cover image from R2 when the bucket is\nprivate. Cached server-side via the standard R2 layer; the\nresponse carries an immutable 1-year Cache-Control so the CDN/\nbrowser never has to fetch the same key twice.\n\nUsed when ``r2_public_url_base`` isn't set — frontend's\n``cover_image_url`` points here instead of a direct CDN URL.","operationId":"article_cover_proxy_v1_article_cover__slug_with_ext__get","parameters":[{"name":"slug_with_ext","in":"path","required":true,"schema":{"type":"string","title":"Slug With Ext"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/topic-art-by-key/{filename}":{"get":{"summary":"Topic Art By Key","description":"Dev/no-CDN fallback for cover images stored on the filesystem cache.\nIn production with R2 + a public_url_base, the frontend hits the CDN\nURL directly and never reaches this handler.","operationId":"topic_art_by_key_v1_topic_art_by_key__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/bulk-retag":{"post":{"summary":"Admin Bulk Retag","description":"Bulk re-vertical articles. Body: {\"from\": \"sends\", \"to\": \"media\",\n\"exclude_slugs\": [\"a\", \"b\"]}. Returns count of rows updated. Used to\nfix bulk miscategorization from past intake without deleting + re-\ningesting each article one at a time.","operationId":"admin_bulk_retag_v1_admin_articles_bulk_retag_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Bulk Retag V1 Admin Articles Bulk Retag Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/regenerate-token":{"post":{"summary":"Admin Regenerate Publisher Token","description":"Mint (or rotate) the publisher's dashboard access token. Operator\nhands the token to the publisher (out-of-band, e.g. via email);\npublisher pastes it into /publisher/dashboard sign-in. Rotating\ninvalidates the previous token immediately.","operationId":"admin_regenerate_publisher_token_v1_admin_publishers__tenant_slug__regenerate_token_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Regenerate Publisher Token V1 Admin Publishers  Tenant Slug  Regenerate Token Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/source-audio/mint":{"post":{"summary":"Publisher Source Audio Mint","description":"Mint a presigned PUT so the publisher uploads straight to storage.\n\nThe bytes never transit this API — a 400 MB WAV master through a\nrequest handler would hold a worker and a pooled connection for\nminutes. The URL is scoped to ONE object key under this publisher's\nown prefix, derived from the authenticated publisher id rather than\nanything in the request.","operationId":"publisher_source_audio_mint_v1_publisher__tenant_slug__source_audio_mint_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Source Audio Mint V1 Publisher  Tenant Slug  Source Audio Mint Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/source-audio/attach":{"post":{"summary":"Publisher Source Audio Attach","description":"Attach a finished upload to one episode and mark it live.\n\nVerifies the object EXISTS via head_object before touching the\ndatabase — an episode is never marked live on the client's word\nalone. Idempotent: re-attaching the same key to the same episode\nupdates in place rather than minting a second asset.","operationId":"publisher_source_audio_attach_v1_publisher__tenant_slug__source_audio_attach_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Source Audio Attach V1 Publisher  Tenant Slug  Source Audio Attach Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/me":{"get":{"summary":"Publisher Me","operationId":"publisher_me_v1_publisher__tenant_slug__me_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Me V1 Publisher  Tenant Slug  Me Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/distribution":{"get":{"summary":"Publisher Distribution","description":"Managed-distribution status board — which channels the author's\nshow is pending/submitted/live on. storyflo does the distribution\nwork (podcast-network model); the author just watches this board.\nPublisher-token auth. While ``MANAGED_DISTRIBUTION_ENABLED`` is off\nthe payload degrades to ``{channels: [], feed_url}`` so the FE can\nship dark.","operationId":"publisher_distribution_v1_publisher__tenant_slug__distribution_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Distribution V1 Publisher  Tenant Slug  Distribution Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/distribution/{tenant_slug}/{channel}/mark":{"post":{"summary":"Admin Distribution Mark","description":"Record a manual distribution transition — how the operator\njournals an Apple/Spotify (no submission API) submission made under\nstoryflo's platform account. Marking ``live`` triggers the one-time\n\"Your show is live on {Channel}\" author email (``notified_at`` is\nthe exactly-once gate). Gated by ``MANAGED_DISTRIBUTION_ENABLED``\nlike every other write in the distribution layer.","operationId":"admin_distribution_mark_v1_admin_distribution__tenant_slug___channel__mark_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"channel","in":"path","required":true,"schema":{"type":"string","title":"Channel"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DistributionMarkRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Distribution Mark V1 Admin Distribution  Tenant Slug   Channel  Mark Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/distribution/queue":{"get":{"summary":"Admin Distribution Queue","description":"The ops work-queue — every pending/submitted channel row grouped\nby channel, oldest first, so the operator can batch the manual\nApple/Spotify/Amazon submissions in one sitting. Read-only (works\nregardless of the master flag).","operationId":"admin_distribution_queue_v1_admin_distribution_queue_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Distribution Queue V1 Admin Distribution Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/articles":{"get":{"summary":"Publisher Articles","operationId":"publisher_articles_v1_publisher__tenant_slug__articles_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Articles V1 Publisher  Tenant Slug  Articles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/stripe-connect":{"post":{"summary":"Publisher Stripe Connect","description":"Self-serve Stripe Connect Express onboarding. Mints (or reuses) a\nconnected account for this publisher and returns a one-time\nonboarding URL the dashboard can redirect to. After onboarding the\nStripe ``account.updated`` webhook flips charges_enabled +\npayouts_enabled on the publisher row, which the dashboard reads\non next load to show the green-light state.\n\nNever 5xxs on a Stripe failure. A payout rail that isn't ready is a\n*state* of the dashboard, not a server error: the publisher gets a 200\ncarrying ``status=\"pending_setup\"``, a null ``onboarding_url`` and a\nplain-English message, so the rest of onboarding continues and the\nConnect button stays pressable. Same reasoning as ``_degraded_payload``\non the claim path — never a conversion-losing 500.\n\n``status`` is ``\"ready\"`` on the happy path and every pre-existing key\nkeeps its meaning, so older dashboard builds that only look at\n``onboarding_url`` are unaffected.","operationId":"publisher_stripe_connect_v1_publisher__tenant_slug__stripe_connect_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherStripeOnboardRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Stripe Connect V1 Publisher  Tenant Slug  Stripe Connect Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/payouts":{"get":{"summary":"Publisher Payouts","description":"Publisher-scoped payout history. Returns the latest N PayoutBatch\nrows for this tenant, newest first. Used by the dashboard payout\ntable — values in ``net_payout_micros`` (1e-6 USD).","operationId":"publisher_payouts_v1_publisher__tenant_slug__payouts_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":120,"minimum":1,"default":24,"title":"Limit"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Payouts V1 Publisher  Tenant Slug  Payouts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/webhooks":{"post":{"summary":"Publisher Webhooks Create","description":"Register a webhook receiver. Mints a per-subscription HMAC secret\nthat the publisher uses to verify inbound deliveries (Storyflo signs\nevery payload with HMAC-SHA256 and includes the digest in\nX-Storyflo-Signature).","operationId":"publisher_webhooks_create_v1_publisher__tenant_slug__webhooks_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_WebhookSubCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Webhooks Create V1 Publisher  Tenant Slug  Webhooks Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Publisher Webhooks List","description":"List webhook subscriptions for this publisher. The HMAC secret\nis NOT returned — show-once on creation. Lost secrets require\nrotation via DELETE + recreate.","operationId":"publisher_webhooks_list_v1_publisher__tenant_slug__webhooks_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Webhooks List V1 Publisher  Tenant Slug  Webhooks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/webhooks/{sub_id}/deliveries":{"get":{"summary":"Publisher Webhook Deliveries","description":"Recent deliveries for one webhook subscription. Newest first.","operationId":"publisher_webhook_deliveries_v1_publisher__tenant_slug__webhooks__sub_id__deliveries_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Webhook Deliveries V1 Publisher  Tenant Slug  Webhooks  Sub Id  Deliveries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/webhooks/{sub_id}/deliveries/{delivery_id}/retry":{"post":{"summary":"Publisher Webhook Delivery Retry","description":"Reset a failed delivery to queued so the worker re-attempts it on\nthe next tick. Returns the delivery's new status. No-op when the\ndelivery is already queued / running / completed.","operationId":"publisher_webhook_delivery_retry_v1_publisher__tenant_slug__webhooks__sub_id__deliveries__delivery_id__retry_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string","title":"Delivery Id"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Webhook Delivery Retry V1 Publisher  Tenant Slug  Webhooks  Sub Id  Deliveries  Delivery Id  Retry Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/webhooks/{sub_id}":{"delete":{"summary":"Publisher Webhooks Delete","description":"Soft-delete a webhook subscription. Sets ``deleted_at`` so\nhistorical deliveries remain visible for audit but no new events\nare dispatched.","operationId":"publisher_webhooks_delete_v1_publisher__tenant_slug__webhooks__sub_id__delete","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Webhooks Delete V1 Publisher  Tenant Slug  Webhooks  Sub Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/icon":{"patch":{"summary":"Publisher Icon Update","description":"Override the publisher's logo URL. Auto-resolved on onboard via\nfavicon scraping; this lets publishers paste their own canonical\nasset (e.g. a square brand mark from their CMS) when the auto-pick\nis wrong.","operationId":"publisher_icon_update_v1_publisher__tenant_slug__icon_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherIconUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Icon Update V1 Publisher  Tenant Slug  Icon Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/analytics":{"get":{"summary":"Publisher Analytics","description":"Daily-resolution analytics for the publisher dashboard chart.\nReturns:\n  - daily_plays: [{day: 'YYYY-MM-DD', plays: int}, ...] for the last N days\n  - vertical_breakdown: [{vertical: str, plays: int}, ...] sorted\n  - top_articles: [{slug, title, plays}, ...] top 10 by plays in window\n\nAll metrics scoped to this publisher's articles only.","operationId":"publisher_analytics_v1_publisher__tenant_slug__analytics_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Analytics V1 Publisher  Tenant Slug  Analytics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/x402-earnings":{"get":{"summary":"Publisher X402 Earnings","description":"Aggregated x402 micropayment earnings for the publisher's articles.\n\nReturns the publisher's 70% share across the lookback window plus\na per-article breakdown so the dashboard can show top earners.\nAmounts are in USDC base units (6dp) — divide by 1_000_000 for the\ndecimal display.\n\nTwo labeled buckets so briefing revenue is visible without double\ncounting:\n  - article receipts (``get_article`` / ``get_audio`` — X402Payment\n    rows with an ``article_id``) → ``total_publisher_share_base``;\n  - briefing/digest attribution (weighted ``publisher_payable``\n    LedgerEntry legs with ``source LIKE 'briefing_%'`` — multi-\n    publisher briefings can't live on the single-publisher receipt\n    column) → ``briefing_earnings_micros``.","operationId":"publisher_x402_earnings_v1_publisher__tenant_slug__x402_earnings_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher X402 Earnings V1 Publisher  Tenant Slug  X402 Earnings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publishers/{publisher_id}/earnings":{"get":{"summary":"Publisher Earnings Widget","description":"Per-vertical earnings panel for the publisher rev-share dashboard.\n\n``publisher_id`` resolves to either the Publisher PK id OR the\n``tenant_slug`` (the listener HEX-FLO referral mechanic surfaces the\ndashboard by slug; older callers may pass the id). Both work.\n\nReturns the real shape with zeros when no plays exist — never random\nor fabricated data. Per-vertical breakdown joins plays -> articles\nfor this publisher, plus a deterministic ``trending_share`` =\npublisher's vertical-listens / total-listens-in-vertical over the\nwindow. Estimated payout uses the placeholder formula\n``$0.01 / listen`` until the ledger settlement engine lands; the\nsame rate the HEX-FLO mechanic quotes externally.","operationId":"publisher_earnings_widget_v1_publishers__publisher_id__earnings_get","parameters":[{"name":"publisher_id","in":"path","required":true,"schema":{"type":"string","title":"Publisher Id"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Earnings Widget V1 Publishers  Publisher Id  Earnings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/x402/payments":{"get":{"summary":"Admin X402 Payments","description":"Operator reconciliation read for x402 receipts. Newest first.","operationId":"admin_x402_payments_v1_admin_x402_payments_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin X402 Payments V1 Admin X402 Payments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/payouts/{period}":{"get":{"summary":"Publisher Payouts For Period","description":"Per-publisher slice of /v1/admin/payouts/{period}. Returns the\npublisher's gross / platform fee / net for the YYYY-MM window.","operationId":"publisher_payouts_for_period_v1_publisher__tenant_slug__payouts__period__get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Payouts For Period V1 Publisher  Tenant Slug  Payouts  Period  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/tier":{"get":{"summary":"Listener Tier Lookup","description":"Public read of a listener's tier. Used by the frontend audio\nplayer to decide whether to show the upgrade CTA after a long\narticle and which voice options to surface.","operationId":"listener_tier_lookup_v1_listener__listener_token__tier_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Tier Lookup V1 Listener  Listener Token  Tier Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/referral-earnings":{"get":{"summary":"Listener Referral Earnings","description":"Per-listener referral rev-share dashboard.\n\nAuth: listener token is already in the URL (same model as the rest of\nthe listener self-serve surface). Returns total accrued cents,\nmonth-bucketed totals, and the most-recent 50 rows.","operationId":"listener_referral_earnings_v1_listener__listener_token__referral_earnings_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Referral Earnings V1 Listener  Listener Token  Referral Earnings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/referrals/payout-queue":{"get":{"summary":"Admin Referrals Payout Queue","description":"Operator-only payout queue.\n\nReturns rows where ``paid_out_at IS NULL`` grouped by\n``referrer_listener_token`` so the operator can batch USDC drops /\nStripe Connect transfers per referrer. Auth: ``require_api_key``\n(same surface as the rest of /v1/admin/*).","operationId":"admin_referrals_payout_queue_v1_admin_referrals_payout_queue_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Referrals Payout Queue V1 Admin Referrals Payout Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/wallet":{"post":{"summary":"Listener Bind Wallet","description":"Persist the listener's connected wallet address.\n\nCalled from the frontend after the listener completes the RainbowKit\nConnect flow on /pricing or /listen. Idempotent — repeat calls with\nthe same address are no-ops; calls with a different address replace\nthe binding (single-wallet-per-listener model).\n\nThe address itself isn't trusted as a signature — possession of the\nlistener_token is the credential. A future hardening pass can add\nSIWE (Sign-In with Ethereum) to prove control of the address before\nbinding it.","operationId":"listener_bind_wallet_v1_listener__listener_token__wallet_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerWalletBind"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Bind Wallet V1 Listener  Listener Token  Wallet Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/follow":{"post":{"tags":["listener-follows"],"summary":"Listener Follow","description":"Follow another listener's queue. Idempotent on\n(follower, followee) — replays return the existing row.\n\nThe followee shares their token via a private channel (text /\nemail); the follower pastes it. We hash the follower's token\nbefore storage so the row is anonymized at rest, but keep the\nfollowee bare so we can resolve their saves at read time.","operationId":"listener_follow_v1_listener__listener_token__follow_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SharedQueueFollowBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Follow V1 Listener  Listener Token  Follow Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/follow/{followee_token}":{"delete":{"tags":["listener-follows"],"summary":"Listener Unfollow","operationId":"listener_unfollow_v1_listener__listener_token__follow__followee_token__delete","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"followee_token","in":"path","required":true,"schema":{"type":"string","title":"Followee Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Unfollow V1 Listener  Listener Token  Follow  Followee Token  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/follows":{"get":{"tags":["listener-follows"],"summary":"Listener Follows List","description":"List the listeners I'm currently following + a redacted token\nsuffix per follow so the UI can render an unfollow row without\nexposing the full followee_token (the followee_token IS the\ncredential to read their queue, so we never echo the full string\nback to anyone but the original holder).","operationId":"listener_follows_list_v1_listener__listener_token__follows_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Follows List V1 Listener  Listener Token  Follows Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/queue/shared":{"get":{"tags":["listener-follows"],"summary":"Listener Queue Shared","description":"Articles saved by every listener I follow, deduped + dated.\nDistinct from /queue (which is my own feed). Renders on\n/listen/queue as a 'From friends' rail. Each item carries the\nfollower-attribution suffix so the UI can show 'saved by …xy123'.","operationId":"listener_queue_shared_v1_listener__listener_token__queue_shared_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Queue Shared V1 Listener  Listener Token  Queue Shared Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/me/referrals":{"post":{"tags":["listener-referrals"],"summary":"Mint Referral","description":"Mint a referral-scoped share URL. Auth via ``X-Listener-Token``.","operationId":"mint_referral_v1_listeners_me_referrals_post","parameters":[{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MintBody"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mint Referral V1 Listeners Me Referrals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["listener-referrals"],"summary":"List My Referrals","description":"List my codes + per-code lands/conversions. Backs /account/referrals.","operationId":"list_my_referrals_v1_listeners_me_referrals_get","parameters":[{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List My Referrals V1 Listeners Me Referrals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/r/listener/{code}":{"get":{"tags":["listener-referrals"],"summary":"Listener Referral Redirect","description":"Public 302 broker. Writes LAND row, sets sf_ref cookie, redirects.","operationId":"listener_referral_redirect_r_listener__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listener-referrals/leaderboard":{"get":{"tags":["listener-referrals"],"summary":"Listener Referrals Leaderboard","description":"Admin leaderboard. Auth via ``X-Storyflo-Email-Token``.","operationId":"listener_referrals_leaderboard_v1_admin_listener_referrals_leaderboard_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Referrals Leaderboard V1 Admin Listener Referrals Leaderboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/register":{"post":{"tags":["embedders"],"summary":"Embedder Register","description":"Public self-serve register endpoint.\n\nBehaviour:\n  1. Validate the site_url (https-only, sane host).\n  2. Look up existing row by contact_email. If found, return 200\n     with ``already_registered=True`` and re-send the verify-email\n     link rather than minting a duplicate. This is the\n     no-enumeration posture: we do NOT 4xx on duplicates because a\n     4xx would let an attacker probe which BD/AP contacts are\n     already in our system.\n  3. Otherwise mint a slug + ref_token, insert the row, mint a\n     verify-email magic link (24h), send via Postmark BD.\n\nRate limiting: a slowapi-style middleware sits upstream in\n``app.main`` and applies the standard public-endpoint cap. We don't\nre-implement it here.","operationId":"embedder_register_v1_embedders_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RegisterBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embedder Register V1 Embedders Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/verify-email":{"get":{"tags":["embedders"],"summary":"Embedder Verify Email","description":"Consume a ``verify_email`` magic link.\n\nSets ``embedders.email_verified_at`` and plants a fresh session\ncookie so the embedder lands on the dashboard already authenticated.\nP3a returns 200 JSON; the FE 302 redirect lives in P3c.","operationId":"embedder_verify_email_v1_embedders_verify_email_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Verify Email V1 Embedders Verify Email Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/login-request":{"post":{"tags":["embedders"],"summary":"Embedder Login Request","description":"Email-only login-request. No enumeration: always returns\n``{\"ok\": true}`` so the caller can't probe whether an email is\nregistered.","operationId":"embedder_login_request_v1_embedders_login_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_LoginRequestBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embedder Login Request V1 Embedders Login Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/login":{"get":{"tags":["embedders"],"summary":"Embedder Login","description":"Consume a ``dashboard_login`` magic link, plant the session\ncookie, return 200 JSON. FE redirect lives in P3c.","operationId":"embedder_login_v1_embedders_login_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Login V1 Embedders Login Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/admin/list":{"get":{"tags":["embedders"],"summary":"Admin Embedder List","description":"P3b · admin list of all embedders + per-row attribution stub.\n\nReturns the rows the operator dashboard renders: tenant slug, contact\nemail, revenue split bps, payout method, status, created_at. Per-row\n7d click + revenue rollup comes from the existing\n``embedder_revenue_events`` table (P3a + P3c) — single GROUP BY\nkeeps the query cheap regardless of the number of embedders.","operationId":"admin_embedder_list_v1_embedders_admin_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Admin Embedder List V1 Embedders Admin List Get"}}}}}}},"/v1/embedders/admin/{embedder_id}/resend-magic-link":{"post":{"tags":["embedders"],"summary":"Admin Embedder Resend Magic Link","description":"P3b · re-issue a 15-min login magic-link for an embedder.\n\nUseful when an embedder mislaid the original onboarding email.\nEmail goes through the Postmark BD server (signature: the storyflo\nteam, storyflo.com). NEVER references ``ali@droplinked.com`` — the\npostmark_bd module's branding tripwire enforces it.","operationId":"admin_embedder_resend_magic_link_v1_embedders_admin__embedder_id__resend_magic_link_post","parameters":[{"name":"embedder_id","in":"path","required":true,"schema":{"type":"string","title":"Embedder Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embedder Resend Magic Link V1 Embedders Admin  Embedder Id  Resend Magic Link Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/admin/{embedder_id}/attribution-rollup":{"get":{"tags":["embedders"],"summary":"Admin Embedder Attribution Rollup","description":"P3c · per-embedder rollup of revenue events over 7d / 30d / lifetime.\n\nSplits clicks (event_type='click', amount=0) from revenue events\n(qualifying_deposit / sale / refund) so the dashboard can show CTR\n+ conversion separately.","operationId":"admin_embedder_attribution_rollup_v1_embedders_admin__embedder_id__attribution_rollup_get","parameters":[{"name":"embedder_id","in":"path","required":true,"schema":{"type":"string","title":"Embedder Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embedder Attribution Rollup V1 Embedders Admin  Embedder Id  Attribution Rollup Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/webhooks/dub/conversion":{"post":{"tags":["embedders"],"summary":"Dub Conversion Webhook","description":"P3c · Dub conversion webhook receiver.\n\nDub sends a signed payload when a click → signup → qualifying-deposit\nconversion happens. The ``polymarket_subid`` carries the ``ref={embedder_slug}``\ntoken we threaded into the affiliate URL, so we look up the embedder\nby slug and persist an ``EmbedderRevenueEvent`` row.\n\nHMAC verification against ``DUB_WEBHOOK_SECRET`` (env). Idempotent via\nthe ``dub_event_id`` UNIQUE constraint — replay is a no-op. Settlement\n(rolling events into payout batches) is a SEPARATE concern handled by\nthe monthly settlement cron, NOT this webhook.","operationId":"dub_conversion_webhook_v1_embedders_webhooks_dub_conversion_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Dub Conversion Webhook V1 Embedders Webhooks Dub Conversion Post"}}}}}}},"/v1/embedders/me":{"get":{"tags":["embedders"],"summary":"Embedder Me","description":"Return the calling embedder's FE-safe profile snapshot.\n\nPowers the FE PR #275 Claim-portal sidebar identity card. Auth via\nthe `storyflo_embedder_session` cookie. 401 if the session is\nmissing / unknown / expired. Read-only.\n\nRedaction posture: allowlist-only. We NEVER surface ``ref_token``\n(Dub subId carrier), ``notes`` (operator scratch), ``disabled_reason``\n(compliance/fraud audit trail), Stripe Connect mirror flags, or\n``payout_destination`` (Stripe acct id / wallet address — the\n/balance endpoint surfaces the custody wallet address separately;\nthe embedder's PERSONAL withdrawal destination is per-withdrawal\nand lives in /withdrawals).","operationId":"embedder_me_v1_embedders_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embedder Me V1 Embedders Me Get"}}}}}}},"/v1/embedders/me/balance":{"get":{"tags":["embedders"],"summary":"Embedder Me Balance","description":"Return the calling embedder's live custody balance.\n\nPowers the FE PR #275 Claim-portal balance card.\n\nZero-state graceful: if no ``EmbedderBalance`` row exists yet (no\nrevenue has landed for this embedder), surface a fully-zero shape\ninstead of 404. The FE renders \"$0.00 claimable\" + \"wallet\nprovisioning\" copy from the same shape.\n\n``claimable = accrued - withdrawn``. Computed at read-time so a\npending withdrawal in 'pending'/'processing' status (which hasn't\nyet bumped ``withdrawn_usdc_micros``) is reflected as still-claimable.\nThe Claim button on the FE separately disables itself if there's an\nin-flight withdrawal (that check uses /withdrawals, not /balance).","operationId":"embedder_me_balance_v1_embedders_me_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embedder Me Balance V1 Embedders Me Balance Get"}}}}}}},"/v1/embedders/me/withdrawals":{"get":{"tags":["embedders"],"summary":"Embedder Me Withdrawals","description":"Return the calling embedder's withdrawal history, most-recent first.\n\nPowers the FE PR #275 Claim-portal withdrawal-history table.\n\nPagination\n----------\nOpaque ``before_id`` cursor — NOT offset. The FE passes the ``id``\nof the LAST row from the previous page; we look that withdrawal up,\npull its ``requested_at``, and filter ``requested_at < cursor_ts``\n(with ``id < cursor_id`` tiebreaker for same-second inserts). Cursor\npagination is stable under concurrent inserts; offset pagination\nwould shift rows under the FE's feet.\n\nFailed withdrawals are excluded by default — the FE shows the happy\nhistory. Operators (or curious embedders) pass ``include_failed=true``\nto see the full audit trail including terminal failures.\n\nZero-state graceful: empty embedder → empty list. NO 404.","operationId":"embedder_me_withdrawals_v1_embedders_me_withdrawals_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"before_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}],"title":"Before Id"}},{"name":"include_failed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Failed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Me Withdrawals V1 Embedders Me Withdrawals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/admin/register-direct":{"post":{"tags":["embedders-v2"],"summary":"Admin Register Direct","description":"Admin-direct create. Skips the self-serve verify-email handshake.\n\nIdempotent on ``slug``: if a row with the same slug already exists,\nreturns it with ``already_provisioned=True``. The caller (admin\nonboard endpoint in PR C) treats this as \"row already there, return\nthe existing snippet.\"","operationId":"admin_register_direct_v1_embedders_admin_register_direct_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RegisterDirectBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Admin Register Direct V1 Embedders Admin Register Direct Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/{slug}/ledger":{"get":{"tags":["embedders-v2"],"summary":"Embedder Ledger","description":"Per-embedder revenue ledger query.\n\nReturns the latest N entries from ``embedder_revenue_ledger`` for\nthe given embedder in ``settled_at DESC`` order. Powered by index\n``ix_erl_embedder_id_settled_at_desc``.","operationId":"embedder_ledger_v1_embedders__slug__ledger_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Ledger V1 Embedders  Slug  Ledger Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/{slug}/current-brief":{"get":{"tags":["embedders-v2"],"summary":"Embedder Current Brief","description":"Latest 4-8 min vertical briefing for the embedder.\n\nSoft-fail shape — never 5xx the iframe. If the embedder is unknown\nor no briefing is available, returns a placeholder shape with\nnull ``audio_url`` so the FE chrome renders a \"briefing\nunavailable\" state instead of going dark.\n\nThis endpoint is intentionally a STUB in PR B — the wiring contract\nis fixed so PR A (#279) can ship + smoke-test against it, but the\nactual brief-resolution logic (vertical-scoped PersonaBriefing\nlookup + VAST 4.0 pre-roll URL builder) lands in a follow-up PR\nthat depends on the cross-functional brief-pipeline owners.","operationId":"embedder_current_brief_v1_embedders__slug__current_brief_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Current Brief V1 Embedders  Slug  Current Brief Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/{slug}/listen-ping":{"post":{"tags":["embedders-v2"],"summary":"Embedder Listen Ping","description":"Per-quartile listen + impression + vast_skipped pings.\n\nStored as ``EmbedderRevenueEvent`` rows with\n``revenue_source='programmatic_audio'`` and\n``amount_usd_cents=0`` (zero-amount funnel events). The\nprogrammatic-CPM rollup job (separate cron, future PR) aggregates\nthese into split-resolved ``embedder_revenue_ledger`` entries.\n\nIdempotency anchor: ``dub_event_id`` is set to a deterministic\n``<embedder>:<event>:<slot>`` per-second slot so duplicate beacons\nfrom sendBeacon + pagehide collapse at the DB UNIQUE constraint\nrather than double-billing the funnel.\n\nSoft-fail: returns 200 even for unknown embedder so the FE\ntelemetry path never breaks the playback UX.","operationId":"embedder_listen_ping_v1_embedders__slug__listen_ping_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenPingBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embedder Listen Ping V1 Embedders  Slug  Listen Ping Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search-chips":{"get":{"summary":"Search Chips","description":"Return the curated chip catalog rendered on the Storyflo\n``/search`` landing page.\n\nThis is the source of truth — the BE boot-warming task reads from\nthe same module (:mod:`app.search_chips`) so chip→warming drift can\nnever reappear. A follow-up FE PR will replace the hardcoded chip\narray in ``storyflo/app/search/page.tsx`` with a fetch against this\nendpoint; until then, the FE array and this endpoint are mirrored\nverbatim in :mod:`app.search_chips.CURATED_SEARCH_CHIP_GROUPS`.\n\nResponse shape::\n\n    {\n      \"groups\": [\n        {\"group\": \"Markets\", \"topics\": [\"SpaceX\", \"Polymarket\", ...]},\n        {\"group\": \"Sports\",  \"topics\": [\"World Cup\", ...]},\n        ...\n      ],\n      \"all_queries\": [\"SpaceX\", \"Polymarket\", ..., \"news\", \"bitcoin\", ...]\n    }\n\n``all_queries`` is the deduplicated union of curated chips +\npopular-keyword watchdog set, in the same order the boot warmer\niterates so an FE caller can debug \"is my chip in the warm set?\"\nvisually.\n\nPublic read-only — no auth, no per-user gating. Safe to cache at\nthe CDN edge for hours.","operationId":"search_chips_v1_search_chips_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Search Chips V1 Search Chips Get"}}}}}}},"/v1/search":{"get":{"summary":"Search Endpoint","description":"Text search across persona briefings + articles. ILIKE-based; at\n~95 briefings + ~52K articles the seq scan + LIMIT stays fast enough on\nPostgres without a tsvector index. Swap to plainto_tsquery once article\nvolume passes ~250K; the response contract is stable.\n\nDeclassified segregation 2026-07-06: the ~198 bulk-ingested\n``briefing_kind='declassified'`` rows (Vietnam study, FBI cryptography\nfiles, …) were swamping general-search relevance — ``q=tech`` / ``ai`` /\n``news`` returned declassified case briefings instead of the matching\nvertical persona briefings, and a new self-expanding ingest cron makes\nthis strictly worse over time. General search (``kind`` unset) now\nEXCLUDES declassified via the same ``is_distinct_from('declassified')``\ngate the X-autopost rotation uses (app/daily_brief_worker.py). Declassified\nstays fully discoverable on its OWN surfaces — the declassified RSS feed,\nthe ``search_declassified`` MCP tool, ``digest_declassified`` — and, for\nparity, callers can scope this endpoint to it explicitly with\n``kind=declassified``.\n\nPool-exhaustion fix 2026-06-21 (overnight Sentry SEV — TimeoutError\nQueuePool size=5 overflow=3 reached on /v1/search): deliberately\nno ``db: AsyncSession = Depends(get_db)`` parameter. The two ILIKE\nscans below can each take 5-15s over the 52K-article body_text\nseq-scan path; with Depends(get_db) the session is held for the\nFULL request duration. Under load that triggers the same pool-\nexhaustion class as PRs #657/#658/#674/#677/#678 — on the writer\npool. Canonical fix-pattern: open a short-lived session inside\nthe handler body + release via ``_close_session_safely`` in\n``finally`` BEFORE building the response shape.","operationId":"search_endpoint_v1_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":200,"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":30,"title":"Limit"}},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"description":"Briefing-kind scope. Default (unset) is GENERAL search: the declassified archive is segregated OUT so vertical queries (tech/ai/news) surface vertical persona briefings instead of being swamped by the bulk-ingested declassified corpus. Pass ``kind=declassified`` to search ONLY the declassified archive (the same scope as /v1/podcasts/storyflo-declassified.xml and the search_declassified MCP tool).","title":"Kind"},"description":"Briefing-kind scope. Default (unset) is GENERAL search: the declassified archive is segregated OUT so vertical queries (tech/ai/news) surface vertical persona briefings instead of being swamped by the bulk-ingested declassified corpus. Pass ``kind=declassified`` to search ONLY the declassified archive (the same scope as /v1/podcasts/storyflo-declassified.xml and the search_declassified MCP tool)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Endpoint V1 Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/hybrid":{"get":{"summary":"Search Hybrid","description":"Hybrid search — keyword overlap + send_count weighting + recency.\n\nv0 is application-side scoring (no pgvector); we score every\narticle that hits at least one query token, then rank by:\n  score = 1.5 * keyword_overlap + 0.4 * log(send_count+1) + recency_bonus\n\nGraduates to pgvector once batch embeddings ship — keeping the\nsame return shape so the frontend won't churn.\n\nPerformance (SEV 2026-06-15): ``storyflo.com/search`` chip clicks\nwere 5-39s cold because this endpoint scans up to 500 rows\n(full-text contains) + Python-side scoring + a per-row\n``RenderJob.status == done`` correlated subquery. Cold path on Fly\nmeasured 39.5s for ``q=SpaceX`` before this cache. Wrapped in the\nsame ``search_cache.SearchCache`` singleton ``/v1/stories/search``\nuses (5-min TTL, single-flight, stale-while-revalidate, 200-entry\nLRU). Key disambiguator ``\"hybrid\"`` keeps hybrid + stories/search\nentries from colliding inside the shared singleton.\n\nOperator can bypass per-request with\n``X-Storyflo-Search-Cache: bypass`` (smoke probes measuring cold\nlatency) or globally via env ``SEARCH_CACHE_TTL_SEC=0``.\n\nPool-exhaustion fix (P0 2026-06-20 12:25 PDT, Sentry\n``TimeoutError: QueuePool limit of size 5 overflow 3 reached``):\ndeliberately no ``db: AsyncSession = Depends(get_db)`` parameter.\nWith basic-plan PG sized at ``DB_POOL_SIZE=5 + MAX_OVERFLOW=3``\n(total 8 conns, see\n``[[storyflo-prod-deploy-discipline-2026-06-19]]``), a request-\nscoped Depends session would be held for the full duration of\n``cache.get_or_compute`` — including for non-owners who are merely\nawaiting another coroutine's Future inside ``search_cache.py``\n(lines 246-248). With 3+ concurrent search requests that all hit\nthe single-flight wait path, the pool exhausts in seconds even\nthough only one DB query is actually running. The owner opens a\nshort-lived session inside ``_compute`` and releases it before the\nFuture resolves; non-owners hold zero connections while waiting.","operationId":"search_hybrid_v1_search_hybrid_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":200,"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Hybrid V1 Search Hybrid Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/trending":{"get":{"summary":"Trending List","description":"Cross-vertical trending: recent published persona briefings grouped\nby vertical + most-active source publishers in the last 24h + the\ntop individual articles ranked by ``trending_score`` + the most\nrecent daily-brief compilation as ``featured`` + the last 7 daily\nbriefs as ``recent_daily_briefs``.\n\nPerformance layout (post-2026-06-04):\n  Q1 — briefings/by_vertical: still computed inline (cheap, ORDER BY +\n       LIMIT over persona_briefings; sub-100ms).\n  Q2 — active_publishers_24h: reads from\n       ``trending_publisher_snapshot`` (migration 0135) which is\n       UPSERTed every ~5 min by ``TrendingSnapshotWorker``. This\n       replaces the GROUP BY over 20K+ ``is_quality=True`` articles\n       that was running ~9.9s wall-clock on cold calls.\n  Q3 — top_articles (NEW 2026-06-04; window widened 24h → 7d\n       2026-06-04 + always-10 fallback): ORDER BY ``trending_score\n       DESC NULLS LAST`` over the last 7 days of quality articles.\n       Originally mirrored ``daily_brief_worker.py:825`` (24h\n       window) but operator-flagged that the prior days' content\n       must remain discoverable — especially during LLM-provider\n       outages that strand most of the day's articles without a\n       summary. A relaxed fallback pass fires when the strict list\n       is shorter than 10, dropping the ``trending_score IS NOT\n       NULL`` filter and ordering by ``created_at DESC`` to fill\n       the gap, so the surface is always populated when any\n       quality article exists in the window. Excludes\n       ``slug LIKE 'daily-brief-%'`` in both passes so the\n       compilation is featured separately and not double-billed.\n  Q4 — featured (NEW 2026-06-04): the latest rendered\n       ``daily-brief-YYYY-MM-DD`` article — the same article whose\n       audio is published to the storyflo · trending Spotify show.\n       Listeners now see it inside the in-app feed too.\n  Q5 — recent_daily_briefs (NEW 2026-06-04): the last 7 rendered\n       daily-brief compilations ordered by ``created_at DESC``.\n       Acts as an always-populated anchor section for the trending\n       UI even when ``top_articles`` is sparse — listeners can\n       always scroll back to \"yesterday's top 10\". Only daily\n       briefs with a done RenderJob (rendered audio available)\n       appear; an unrendered brief is a dead row to a listener\n       expecting playback to start on click.\n\nThe in-process 60s TTL cache (``app/trending_cache.py``) stays as a\nsecond-line defence — cache key now includes ``top`` so different\ncallers share rows but don't collide.\n\nSnapshot freshness contract: ``TrendingSnapshotWorker`` is the\nprimary driver (5-min loop); ``cron_extra_ticks.trending_snapshot_tick``\non the daily-brief loop is the safety-net. The worker fires its first\ntick immediately on start so the snapshot row exists within seconds\nof deploy. If the row is somehow missing, this endpoint returns\n``[]`` for ``active_publishers_24h`` rather than falling back to the\n9.9s aggregation.","operationId":"trending_list_v1_trending_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"top","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Top"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Trending List V1 Trending Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/trending/search":{"get":{"summary":"Trending Search","description":"Discovery + shareability surface — keyword search across the\nsame 7d quality-article corpus that powers ``/v1/trending``'s\n``top_articles`` list.\n\nOperator directive 2026-06-04: listeners should \"always see the\ntrending 10 stories in order on trending and initially with search\nto drive discovery and shareability.\" ``/v1/trending`` shipped the\nordered list (PR #451 + #461 + the 7d-window widening); this is the\nmissing search surface. The response per-item shape MIRRORS\n``top_articles`` (slug + title + publisher_slug + publisher_name +\nvertical + trending_score + summary + created_at) so the frontend\ncan render search hits with the exact same card component — only a\nnew ``match_rank`` field (0-1 relevance score) is added.\n\nImplementation: Postgres full-text search against\n``articles.search_vector`` (the generated tsvector column codified\nby migration 0124, indexed by the partial GIN\n``ix_articles_search_vector_recent`` from migration 0136).\n``plainto_tsquery('english', :q)`` parses user input safely (no\ntsquery operator injection); the match predicate is\n``search_vector @@ tsq`` so the GIN bitmap scan drives candidate\nselection.\n\nRanking blend (operator-mandated):\n\n    score = trending_score * 0.7\n          + ts_rank(search_vector, tsq) * 0.3 * 5.0\n\n``trending_score`` is the prebuilt freshness/landing signal\n(typical range 0-5) and ``ts_rank`` returns 0-1. The 5.0 multiplier\nrescales ts_rank into the same magnitude bracket so the 70/30\nweighting is meaningful in practice — without it the trending term\nwould always dominate and search-relevance would be cosmetic.\n\nEligibility filter mirrors ``top_articles`` so the search surface\nonly returns the same rows the trending list could surface:\n  - created_at >= now - ``window_days``  (default 7d, max 30d)\n  - is_quality = True\n  - summary is not null + not empty\n  - slug NOT LIKE 'daily-brief-%'  (compilation is featured separately)\n  - vertical = :vertical when filter supplied (must be one of the\n    7 priority verticals per ``app.vertical_classifier.PRIORITY_VERTICALS``)\n\nValidation:\n  - q          2-100 chars (FastAPI Query enforces, returns 422)\n  - limit      1-50      (FastAPI Query enforces)\n  - window_days 1-30     (matches PR #457's expanded default)\n  - vertical   must be in PRIORITY_VERTICALS or 400\n\nEmpty result is a 200 with ``count: 0`` + ``results: []`` — NOT\n404, because the empty case is a normal search outcome (typo, term\nwith no hits this week) and the listener UI shouldn't treat it as\nan error state.\n\nSqlite fallback (tests): the ``search_vector`` column + tsvector\noperators are Postgres-only. On sqlite the implementation switches\nto an ILIKE-based match (title OR summary) with a token-overlap\n``match_rank`` proxy. Same response contract, same eligibility\nfilter, same ranking blend (with ``match_rank`` substituting for\nts_rank). Used only by the test suite — production is always\nPostgres.\n\nPool-exhaustion fix 2026-06-21 (follow-up to PRs #657 + #658 +\n#674 + #677): deliberately no ``db: AsyncSession = Depends(get_db)``\nparameter. ``/v1/trending/search`` shares the trending-cache\nsingleton with ``/v1/trending`` AND is wrapped by the same\nsingle-flight ``cache.get_or_compute`` primitive — so the same\npool-pressure anti-pattern applies. With basic-plan PG sized at\n``DB_POOL_SIZE=5 + MAX_OVERFLOW=3`` (total 8 conns), a request-\nscoped Depends session held across ``get_or_compute`` exhausts the\npool on a concurrent-burst storm even when only one DB query is\nactually running (every non-owner is parked on the shared Future\nwhile still holding its Depends-session checkout).\n\nSame fix shape as the four previous endpoints: only the single-\nflight OWNER opens a short-lived session inside ``_compute_search``\nand releases it via ``_close_session_safely`` BEFORE the function\nreturns. Non-owners hold zero connections while awaiting the\nshared Future inside ``trending_cache.get_or_compute``.","operationId":"trending_search_v1_trending_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":100,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"default":7,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Trending Search V1 Trending Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefings/index":{"get":{"summary":"Briefings Index","description":"Free, unauthenticated catalog of available premium briefings.\n\nUsed by the storyflo-mcp server + the frontend /agents page to\nadvertise which audio briefings an agent can pay to retrieve. The\nLIST is free; only the audio access behind each url is x402-gated.\n\n``lang`` (optional) — ISO 639-1 listener-preference filter. ``en``\n(default) returns the legacy English catalog. ``es`` returns the\nSpanish persona-briefing slate (synthesised by the daily-es cron;\nsee ``.github/workflows/daily-briefings-es.yml``). Each item carries\na ``language`` field so the frontend can pre-flight the audio voice\n(Piper Spanish vs Kokoro English).","operationId":"briefings_index_v1_briefings_index_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Briefings Index V1 Briefings Index Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefings/recent":{"get":{"summary":"Briefings Recent","description":"Recent published persona-briefings, optionally filtered by ``lang``.\n\nConvention: non-EN briefings use slug prefix ``daily-<lang>-*`` (see\nseed_es_personas_2026_05_20.py). EN runs match every other persona.\nReturns most-recent first. Public endpoint — briefings are our own\neditorial commentary, not gated by Publisher.opt_in_at.\n\nCap is 1000 (was 50) because the frontend sitemap.xml + news-sitemap.xml\nuse this endpoint to enumerate URLs for SEO discoverability; 50 was\nleaving 100s of published briefings invisible to Google. Postgres\nhandles 1k LIMIT trivially on the (status, published_at DESC) index.","operationId":"briefings_recent_v1_briefings_recent_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Lang"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Briefings Recent V1 Briefings Recent Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefings/events":{"get":{"summary":"Briefings Events Stream","description":"text/event-stream channel pushing briefing-render events.","operationId":"briefings_events_stream_v1_briefings_events_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Event Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"summary":"Briefings Events Cors Preflight","description":"CORS preflight for the briefings events SSE channel.","operationId":"briefings_events_cors_preflight_v1_briefings_events_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/briefings/{persona_slug}":{"get":{"summary":"Get Latest Briefing","description":"Latest published briefing for a persona. 404 if none.","operationId":"get_latest_briefing_v1_briefings__persona_slug__get","parameters":[{"name":"persona_slug","in":"path","required":true,"schema":{"type":"string","title":"Persona Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Latest Briefing V1 Briefings  Persona Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefings/{persona_slug}/{briefing_date}":{"get":{"summary":"Get Briefing By Date","description":"Specific published briefing for a (persona, date). 404 if none.","operationId":"get_briefing_by_date_v1_briefings__persona_slug___briefing_date__get","parameters":[{"name":"persona_slug","in":"path","required":true,"schema":{"type":"string","title":"Persona Slug"}},{"name":"briefing_date","in":"path","required":true,"schema":{"type":"string","title":"Briefing Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Briefing By Date V1 Briefings  Persona Slug   Briefing Date  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/discover/newsletters":{"get":{"summary":"Discover Newsletters","description":"Public listing of claimed publishers with subscription counts +\nmost-recent article. Drives the /discover/newsletters listener-\nacquisition page. Excludes publishers who haven't claimed (no\nsubscriptions enabled / no payout configured) so the directory\nonly surfaces real wired-up authors.\n\nOptional ``vertical`` filter (tech | health | young_moms | yoga |\nscience | finance | news) restricts to publishers tagged with that\ntaxonomy slug. See app/vertical_classifier.py for the canonical\nlist.\n\n2026-06-14 · architectural rewrite (closes #562). Reads from the\n``mv_discover_newsletters_snapshot`` materialized view (migration\n0153) — a single indexed SELECT replaces the four queries +\nPython-side fan-out that took 50+s on cold cache. The MV is\nrefreshed every 5 minutes by the separate worker (separate PR);\nstale data is fine because publisher claims are rare events.\n\nSQLite path (tests/dev): the materialized view doesn't exist —\nfall back to the legacy Python fan-out so tests keep working.","operationId":"discover_newsletters_v1_discover_newsletters_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Discover Newsletters V1 Discover Newsletters Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/podcasts/dl":{"get":{"summary":"Podcast Download Redirect","description":"Download-tracking redirect for podcast ``<enclosure>`` URLs — the\nattribution seam for Apple/Spotify/Overcast plays.\n\nPodcast apps GET this endpoint (it's the enclosure URL), we record the\ndownload, then 302 to the real audio host (the app follows it and still\ncounts the download). Emits a structured ``podcast_download`` log line\nkeyed by episode + privacy-safe IP hash so listens are countable (dedupe\nby (key, ip) for IAB-style unique downloads — analytics fast-follow).\n\nHEAD is registered alongside GET (2026-07-15 incident): YouTube/Apple\nHEAD-probe enclosure URLs before ingesting; a 405 reads as \"could not\nfetch the audio file\" and the episode is rejected. HEAD returns the\nidentical 302 + Location (the redirect target — audio.storyflo.com\nCloudFront — itself serves HEAD, verified 200 audio/mpeg) but does NOT\nemit the ``podcast_download`` line: a probe is not a listen.\n\nOpen-redirect-guarded: only ``*.r2.dev`` / ``*.storyflo.com`` audio\nhosts are honored; anything else is a 400.","operationId":"podcast_download_redirect_v1_podcasts_dl_get","parameters":[{"name":"u","in":"query","required":true,"schema":{"type":"string","maxLength":2048,"title":"U"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Podcast Download Redirect","description":"Download-tracking redirect for podcast ``<enclosure>`` URLs — the\nattribution seam for Apple/Spotify/Overcast plays.\n\nPodcast apps GET this endpoint (it's the enclosure URL), we record the\ndownload, then 302 to the real audio host (the app follows it and still\ncounts the download). Emits a structured ``podcast_download`` log line\nkeyed by episode + privacy-safe IP hash so listens are countable (dedupe\nby (key, ip) for IAB-style unique downloads — analytics fast-follow).\n\nHEAD is registered alongside GET (2026-07-15 incident): YouTube/Apple\nHEAD-probe enclosure URLs before ingesting; a 405 reads as \"could not\nfetch the audio file\" and the episode is rejected. HEAD returns the\nidentical 302 + Location (the redirect target — audio.storyflo.com\nCloudFront — itself serves HEAD, verified 200 audio/mpeg) but does NOT\nemit the ``podcast_download`` line: a probe is not a listen.\n\nOpen-redirect-guarded: only ``*.r2.dev`` / ``*.storyflo.com`` audio\nhosts are honored; anything else is a 400.","operationId":"podcast_download_redirect_v1_podcasts_dl_get","parameters":[{"name":"u","in":"query","required":true,"schema":{"type":"string","maxLength":2048,"title":"U"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/daily-signal/subscribe":{"post":{"tags":["daily-signal"],"summary":"Daily Signal Subscribe","description":"Public double-opt-in subscribe.\n\nUpserts a ``pending`` row with a fresh opt-in token (only its SHA-256\nhash is stored) and emails a confirm link. No enumeration: any\nwell-formed address returns 200 regardless of whether it already\nexisted; only a malformed address is a 400.","operationId":"daily_signal_subscribe_v1_daily_signal_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubscribeBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/daily-signal/confirm":{"get":{"tags":["daily-signal"],"summary":"Daily Signal Confirm","description":"Consume a double-opt-in confirm link.\n\nHashes the token, matches a ``pending`` row, flips it to ``confirmed``,\nthen 302-redirects to the signal page with ``?confirmed=1``. An\ninvalid/expired/consumed token redirects with ``?confirmed=0`` (no\nenumeration, no error page).","operationId":"daily_signal_confirm_v1_daily_signal_confirm_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/daily-signal/unsubscribe":{"get":{"tags":["daily-signal"],"summary":"Daily Signal Unsubscribe","description":"One-click unsubscribe.\n\nVerifies the stateless HMAC token (``app.email_guardrails``), marks the\nmatching subscriber ``unsubscribed`` (idempotent — a row already\nunsubscribed or an address that was never a row still returns the\nfriendly 200 confirmation), and returns a simple HTML page.","operationId":"daily_signal_unsubscribe_v1_daily_signal_unsubscribe_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/atlas/positions":{"get":{"tags":["atlas"],"summary":"Atlas Positions","description":"Public: Atlas's illustrative position BOOK, most-recently-active\nfirst. Filter by ``status`` (``open`` | ``closed`` | ``all``; default\n``all``). Read-only, no auth — the citeable showcase. ZERO raw odds.","operationId":"atlas_positions_v1_atlas_positions_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","maxLength":16,"default":"all","title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Atlas Positions V1 Atlas Positions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/atlas/positions/summary":{"get":{"tags":["atlas"],"summary":"Atlas Positions Summary","description":"Public: showcase header counts for Atlas's illustrative book —\n``{open, closed, repositioned_last_7d, total}``. ZERO raw odds.","operationId":"atlas_positions_summary_v1_atlas_positions_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Atlas Positions Summary V1 Atlas Positions Summary Get"}}}}}}},"/v1/search/unified":{"get":{"tags":["search"],"summary":"Unified Search Endpoint","description":"Search every requested corpus and return per-corpus buckets.\n\nNote the deliberate asymmetry with ``/v1/search/hybrid``: that endpoint\ntakes NO db session (a request-scoped session held across its\nsingle-flight wait exhausted the 8-connection pool in a 2026-06-20\nP0). Here a session IS needed for auth + metering, but it is used only\nfor those short writes — the SEARCH itself runs inside\n``search_all``'s own short-lived read session, so nothing is held\nacross the scan.","operationId":"unified_search_endpoint_v1_search_unified_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":200,"title":"Q"}},{"name":"corpora","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated corpora to search: news, declassified, signals. Omit (or '*') for all. Unknown names are ignored rather than rejected so a pinned client keeps working as corpora are added.","title":"Corpora"},"description":"Comma-separated corpora to search: news, declassified, signals. Omit (or '*') for all. Unknown names are ignored rather than rejected so a pinned client keeps working as corpora are added."},{"name":"profile","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Ranking profile: 'desk' (signals lead, news/declassified are supporting evidence) or 'publisher' (news/declassified lead with attribution, signals are context). Defaults to the client's configured profile, else 'publisher'. Both profiles return the SAME buckets — only order and enrichment differ.","title":"Profile"},"description":"Ranking profile: 'desk' (signals lead, news/declassified are supporting evidence) or 'publisher' (news/declassified lead with attribution, signals are context). Defaults to the client's configured profile, else 'publisher'. Both profiles return the SAME buckets — only order and enrichment differ."},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/access":{"post":{"tags":["publisher"],"summary":"Publisher Access By Email","description":"Email in, sign-in link out.\n\nAlways returns the same body. A caller cannot tell a hit from a miss,\nso the endpoint never answers \"is this author on storyflo?\" for a\nstranger.\n\nSend failures are logged and swallowed for the same reason: a\ndifferent response on a bounce would leak the same fact.","operationId":"publisher_access_by_email_v1_publisher_access_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Publisher Access By Email V1 Publisher Access Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/legal/publisher-authorization":{"get":{"tags":["legal"],"summary":"Publisher Authorization","description":"The Publisher Audio Authorization Agreement text on file.\n\nPublic, no auth — the FE authorization modal fetches this so it renders the\nEXACT bytes an acceptance is recorded against (``content_hash`` ties the two\ntogether). Returns the CURRENT version by default; ``?version=`` fetches a\nhistorical version (404 if unknown).","operationId":"publisher_authorization_v1_legal_publisher_authorization_get","parameters":[{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"description":"Fetch a specific historical version; omit for current.","title":"Version"},"description":"Fetch a specific historical version; omit for current."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Authorization V1 Legal Publisher Authorization Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/authorization":{"get":{"tags":["publisher-authorization"],"summary":"Get Publisher Authorization","description":"The publisher's on-file Audio Authorization acceptance + revocation state.\n\n``revoked`` is the effective flag — ``takedown_at`` (the always-wins\nrevocation) OR a ledger revocation row. The dashboard Agreement card renders\n``on_file`` (the receipt) and toggles the \"Cancel authorization\" affordance\non ``revoked``.","operationId":"get_publisher_authorization_v1_publisher__tenant_slug__authorization_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Publisher Authorization V1 Publisher  Tenant Slug  Authorization Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/authorization/revoke":{"post":{"tags":["publisher-authorization"],"summary":"Revoke Publisher Authorization","description":"Self-serve cancel-anytime. Idempotent + never flag-gated — revocation is\na safety operation that must never be stranded.\n\nEffect order (the effect first, the audit row second — a dropped audit row\nmust never leave a publisher un-revoked): stamp ``takedown_at`` +\npause ACTIVE intake sources + commit, THEN append the ledger revocation row,\nTHEN notify ops (best-effort). ``distribution_authorized`` honors\n``takedown_at`` as the always-wins revocation, so narration + managed\ndistribution stop immediately for this publisher.","operationId":"revoke_publisher_authorization_v1_publisher__tenant_slug__authorization_revoke_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Publisher Authorization V1 Publisher  Tenant Slug  Authorization Revoke Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voice/pause":{"post":{"tags":["voice"],"summary":"Voice Pause","operationId":"voice_pause_v1_voice_pause_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceCommandBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Voice Pause V1 Voice Pause Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voice/go":{"post":{"tags":["voice"],"summary":"Voice Go","description":"Alias-resume. ``/resume`` would be more REST-y but the voice\nintent dictionary prefers the snappier \"go\" verb (matches the Siri\nShortcut name).","operationId":"voice_go_v1_voice_go_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceCommandBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Voice Go V1 Voice Go Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voice/next":{"post":{"tags":["voice"],"summary":"Voice Next","description":"Skip to the next article in the listener's queue.\n\nResolves the queue the same way the dashboard /queue endpoint + RSS\nfeed do — recent articles in the listener's subscribed verticals,\nnewest first — finds where they are now (``body.article_slug`` if the\ncaller passed it, else the persisted playback state), and advances to\nthe following item, wrapping to the top at the end. Publishes the new\n``article_slug`` over SSE so a connected player jumps to it.","operationId":"voice_next_v1_voice_next_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceCommandBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Voice Next V1 Voice Next Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voice/state/{listener_token}":{"get":{"tags":["voice"],"summary":"Voice State","operationId":"voice_state_v1_voice_state__listener_token__get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Voice State V1 Voice State  Listener Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voice/stream/{listener_token}":{"get":{"tags":["voice"],"summary":"Voice Stream","description":"SSE stream of state-change events for this listener.\n\nLong-lived ``text/event-stream`` connection. Heartbeat comments\nevery 15s prevent intermediary timeouts. Disconnects clean up the\nsubscriber queue automatically (see ``sse.event_stream``).\n\nPool-safety (2026-08-11 SEV)\n============================\n\nThis handler MUST NOT take ``db: AsyncSession = Depends(get_db)``.\n\nFastAPI tears yield-scoped dependencies down on the request's\n``AsyncExitStack``, and for a ``StreamingResponse`` that stack does\nnot unwind until the response BODY has finished sending. The body\nhere is ``sse.event_stream`` — a ``while True`` loop that only ends\nwhen the client disconnects. So a signature-bound session would pin\none primary-pool connection for the ENTIRE SSE session (minutes to\nhours) while issuing exactly one query at the start.\n\nWith ``DB_POOL_SIZE=15 + DB_MAX_OVERFLOW=5``, twenty concurrent\nlisteners on this route would exhaust the writer pool platform-wide\nand take down every other endpoint — the shape behind the\n2026-08-11 ``QueuePool limit of size 15 overflow 5 reached`` SEV.\n\nInstead: authenticate on a short-lived session, release it in a\n``finally`` BEFORE constructing the streaming response, and let the\nstream run holding zero connections.","operationId":"voice_stream_v1_voice_stream__listener_token__get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/detect-test":{"post":{"tags":["admin","ads"],"summary":"Admin Ads Detect Test","description":"Run the detector on arbitrary input and echo the result.\n\nNo DB write. Useful for tuning thresholds + iterating on the\nsignal lexicon without re-ingesting an article.","operationId":"admin_ads_detect_test_v1_admin_ads_detect_test_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__intake__ad_router__DetectTestBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ads Detect Test V1 Admin Ads Detect Test Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/detected":{"get":{"tags":["admin","ads"],"summary":"Admin Ads Detected","description":"Paginated list of detected sponsor blocks.","operationId":"admin_ads_detected_v1_admin_ads_detected_get","parameters":[{"name":"publisher_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ads Detected V1 Admin Ads Detected Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/claims/{publisher_id}":{"get":{"tags":["admin","ads"],"summary":"Admin Ads Claims For Publisher","description":"Return every PublisherAdClaim row for one publisher.","operationId":"admin_ads_claims_for_publisher_v1_admin_ads_claims__publisher_id__get","parameters":[{"name":"publisher_id","in":"path","required":true,"schema":{"type":"string","title":"Publisher Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ads Claims For Publisher V1 Admin Ads Claims  Publisher Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/claims/{publisher_id}/refresh":{"post":{"tags":["admin","ads"],"summary":"Admin Ads Claims Refresh","description":"Recompute ``claimed_blocks`` + ``claimed_reach`` for one publisher.\n\nFor every calendar month with at least one DetectedSponsorBlock,\nproduce a ``PublisherAdClaim`` row holding:\n\n  * ``claimed_blocks``  count of distinct detected blocks for the\n                        publisher in that month.\n  * ``claimed_reach``   total ``plays`` rows attached to the\n                        distinct articles those blocks belong to.\n\nIdempotent — re-running on the same data yields identical rows.\nThe join is done at the slug level (DetectedSponsorBlock.article_slug\n== Article.slug AND Article.publisher_id == publisher_id), so a\npublisher only ever counts plays of its own articles.","operationId":"admin_ads_claims_refresh_v1_admin_ads_claims__publisher_id__refresh_post","parameters":[{"name":"publisher_id","in":"path","required":true,"schema":{"type":"string","title":"Publisher Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ads Claims Refresh V1 Admin Ads Claims  Publisher Id  Refresh Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/classify-test":{"post":{"tags":["admin-intake"],"summary":"Classify Test","description":"Run the template classifier against an arbitrary sample.\n\nAdmin-token-gated. Read-only — does not touch the DB and does\nnot invoke the bundler. Useful for tuning the keyword/regex\ntables in ``template_signals`` against real-world misfires.","operationId":"classify_test_v1_admin_intake_classify_test_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ClassifyTestBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ClassifyTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/health":{"get":{"tags":["admin-intake"],"summary":"Admin Intake Health","description":"Operator dashboard endpoint.\n\nReturns:\n\n  * ``ingest_per_vertical_1h`` / ``ingest_per_vertical_24h`` —\n    Article inserts grouped by vertical over the trailing window.\n    Zero counts are omitted to keep the payload tight.\n  * ``sources`` — per-IntakeSource summary, errored ones first.\n  * ``stage_counters`` — snapshot of the in-process counters from\n    ``app/intake/observability.py``. ``by_stage`` is global,\n    ``by_stage_vertical`` is the per-vertical breakdown.\n  * ``generated_at`` — RFC 3339 timestamp.","operationId":"admin_intake_health_v1_admin_intake_health_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Health V1 Admin Intake Health Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio-qa/render-check":{"post":{"tags":["admin-audio-qa"],"summary":"Render Check","description":"Probe a rendered MP3 for silence/clipping/duration pathologies.\n\nAdmin-token-gated. Runs ffprobe + ffmpeg (no ML model, no Whisper),\nso this endpoint is cheap and safe to call on every render.\nComplementary to ``/verify`` (voiced-PII check) — both should pass\nbefore promoting a render.","operationId":"render_check_v1_admin_audio_qa_render_check_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RenderCheckBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RenderCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio-qa/verify":{"post":{"tags":["admin-audio-qa"],"summary":"Verify","description":"Transcribe an audio file with Whisper and scan for spoken PII.\n\nAdmin-token-gated. The audio is downloaded (URL) or read (path),\ntranscribed with the smallest appropriate Whisper variant, and the\ntranscript is matched against\n:data:`app.audio_qa.whisper_check.SPOKEN_PII_PATTERNS`.\n\nReturns the verdict verbatim. The route does NOT publish, alert, or\npersist — it's a tuning / re-check surface, not a worker hook.","operationId":"verify_v1_admin_audio_qa_verify_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VerifyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/history":{"post":{"summary":"Listener History Record","description":"Record a play in the listener's personal history. Idempotent on\n(listener, article, day) — re-firing the same article on the same\nUTC day updates listened_seconds rather than appending a duplicate.\nBest-effort: returns 200 even when the article is unknown.","operationId":"listener_history_record_v1_listener__listener_token__history_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_HistoryRecord"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener History Record V1 Listener  Listener Token  History Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Listener History List","description":"Paginated personal history. Saves come first (saved=True), then\neverything else newest-first. Excludes dismissed rows.","operationId":"listener_history_list_v1_listener__listener_token__history_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener History List V1 Listener  Listener Token  History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/feedback":{"post":{"summary":"Listener Feedback","description":"Save / dismiss / skip an article from the listener's queue. Writes\nto ListenerHistory so the queue and history pages can both filter\non it. ``skip`` is logged as a same-day history row with\nlistened_seconds=0 and dismissed=True so the queue suppresses it\nwithout persisting a strong negative signal.","operationId":"listener_feedback_v1_listener__listener_token__feedback_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerFeedback"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Feedback V1 Listener  Listener Token  Feedback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/experiments/assign":{"post":{"summary":"Experiment Assign","description":"Return the sticky variant assignment for a subject. Creates one\non first call (deterministic hash → variant index), reads the\nexisting row on subsequent calls so the listener never flickers\nbetween variants.\n\nSubject hashing:\n  - listener_token (when present) → primary key\n  - request IP hash (otherwise) → anonymous fallback\n\nVariants are caller-supplied so the same endpoint serves any\nexperiment without server-side config — keeps the framework\nhomegrown and dead-simple.","operationId":"experiment_assign_v1_experiments_assign_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AssignBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Experiment Assign V1 Experiments Assign Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/experiments/convert":{"post":{"summary":"Experiment Convert","description":"Mark a subject's assignment as converted. Idempotent — repeat\ncalls don't increment a count; the per-variant conversion rate\non the dashboard is rows-converted / rows-total.","operationId":"experiment_convert_v1_experiments_convert_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ConvertBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Experiment Convert V1 Experiments Convert Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/experiments":{"get":{"summary":"Admin Experiments Summary","description":"Per-experiment + per-variant counts + conversion rates.","operationId":"admin_experiments_summary_v1_admin_experiments_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Experiments Summary V1 Admin Experiments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs/dead-letter":{"get":{"summary":"Admin Jobs Dead Letter","description":"Failed jobs grouped by error pattern. Lets the operator see\n'we have 47 jobs failing with the same provider timeout' at a\nglance instead of paging through individual rows.\n\nPattern key = first 80 chars of the error message after a normaliser\n(lowercase + strip numeric IDs + whitespace) so functionally\nidentical errors collapse into one bucket.","operationId":"admin_jobs_dead_letter_v1_admin_jobs_dead_letter_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs Dead Letter V1 Admin Jobs Dead Letter Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs/retry-by-pattern":{"post":{"summary":"Admin Jobs Retry By Pattern","description":"Bulk retry every failed job whose normalised error matches\n``pattern``. The operator picks a pattern from the dead-letter\ngrouping endpoint; we re-classify those jobs back to ``queued``\nand clear ``attempts`` so the worker reprocesses them.","operationId":"admin_jobs_retry_by_pattern_v1_admin_jobs_retry_by_pattern_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RetryByPatternBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs Retry By Pattern V1 Admin Jobs Retry By Pattern Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/full":{"get":{"summary":"Admin Publishers Full","description":"Email-token-gated publisher list with the rich projection the\n/admin/publishers page needs (icon, intake source count, recent\narticle count). Distinct from /v1/admin/publishers (api-key\ngated, used for tenant management) — this one is operator UX.","operationId":"admin_publishers_full_v1_admin_publishers_full_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publishers Full V1 Admin Publishers Full Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declined":{"get":{"summary":"Admin Declined List","description":"Paginated list of publishers in the decline / copyright bucket.\n\nFilters:\n  - ``class`` (REMOVE|DECLINED|UNSUBSCRIBE|COPYRIGHT_QUESTION) —\n    narrow by the watcher's classification. Omit for all classes.\n  - ``q`` — case-insensitive substring search over the publisher's\n    name + contact_email + decline_reason snippet.\n\nThe query includes BOTH do_not_contact=true rows (auto-suppressed)\nAND rows where decline_class is non-null but do_not_contact is\nfalse (COPYRIGHT_QUESTION — operator review pending). This keeps\nthe master-list a single screen for everything the operator needs\nto know about decline-class replies.\n\nReturns the per-publisher row + a count of prior outreach attempts\n(so the operator sees \"how many times did we touch them before\nthey replied stop\"). The count is a small per-publisher query — fine\nat 50/200 pagination, would need a join optimisation at higher\npage sizes.","operationId":"admin_declined_list_v1_admin_declined_get","parameters":[{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Class"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Declined List V1 Admin Declined Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declined/stats":{"get":{"summary":"Admin Declined Stats","description":"Headline counters for the /admin/declined dashboard. Mirrors the\n/v1/admin/listeners/stats shape so the frontend can reuse the same\nStat card pattern.\n\nCounts:\n  - ``total`` — every publisher with decline_class non-null OR\n    do_not_contact=true. Same predicate as the list endpoint above.\n  - ``by_class`` — per-class breakdown ({REMOVE, DECLINED,\n    UNSUBSCRIBE, COPYRIGHT_QUESTION, OTHER}).\n  - ``copyright_review_pending`` — COPYRIGHT_QUESTION rows that\n    still have do_not_contact=false (the operator hasn't manually\n    decided yet).","operationId":"admin_declined_stats_v1_admin_declined_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Declined Stats V1 Admin Declined Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declined/{tenant_slug}/un-suppress":{"post":{"summary":"Admin Declined Un Suppress","description":"Manually reverse the watcher's auto-suppression decision.\n\nFlips ``do_not_contact`` back to false + clears ``declined_at``,\n``decline_reason``, ``decline_class``. Use sparingly — this is the\noperator's explicit override when an auto-suppress was wrong (e.g.\na publisher said \"remove me from this list\" meaning \"remove from\nTHIS cohort, send me the right one\" rather than a hard out).\n\nThe reason is logged for audit (server log only — not persisted on\nthe row, intentionally, so the next auto-suppress can stamp fresh\nwithout the operator manually clearing audit text).","operationId":"admin_declined_un_suppress_v1_admin_declined__tenant_slug__un_suppress_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_UnSuppressRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Declined Un Suppress V1 Admin Declined  Tenant Slug  Un Suppress Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/opted-in":{"get":{"summary":"Admin Opted In List","description":"Paginated list of publishers who consented (opted in).\n\nFilters:\n  - ``vertical`` — exact match on ``Publisher.vertical``. Omit for all.\n  - ``q`` — case-insensitive substring search over name + contact_email\n    + tenant_slug.\n\nReturns publisher rows with monetization-status flags (has_payout_wallet,\nhas_stripe_account) so the operator can spot opted-in publishers who\nstill need to wire up payout. Includes the optional latest article\ntitle via a per-row lookup — same per-row-query pattern as the\noutreach_attempts join in the declined endpoint; fine at 50/200\npagination ceilings.\n\nSort: ``opt_in_at DESC`` (newest claim first).","operationId":"admin_opted_in_list_v1_admin_opted_in_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Opted In List V1 Admin Opted In Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/opted-in/stats":{"get":{"summary":"Admin Opted In Stats","description":"Headline counters for the /admin/opted-in dashboard. Mirrors the\n/v1/admin/declined/stats shape so the frontend can reuse the same\nStat card pattern.\n\nCounts:\n  - ``total`` — every publisher with opt_in_at non-null AND\n    do_not_contact not true (suppression-overrides-opt-in rule).\n  - ``by_vertical`` — per-vertical breakdown. Buckets a NULL\n    vertical under ``\"unclassified\"``.\n  - ``last_7d_count`` — rows whose opt_in_at falls within the last\n    7 days. Useful for \"weekly claim velocity\" at-a-glance.\n  - ``last_30d_count`` — rows whose opt_in_at falls within the last\n    30 days. Useful for monthly retention reporting.\n  - ``with_payout_wallet`` — opted-in rows that have a non-empty\n    payout_wallet_address (USDC-on-Base rail wired).\n  - ``with_stripe_account`` — opted-in rows that have a non-empty\n    stripe_account_id (Stripe Connect onboarding initiated; may\n    not yet be ``stripe_payouts_enabled``).","operationId":"admin_opted_in_stats_v1_admin_opted_in_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Opted In Stats V1 Admin Opted In Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox":{"get":{"summary":"Admin Inbox List","description":"Browse inbound messages captured by the Path A merged email-intake\npipeline. Filter by classification (`reply` / `bounce` /\n`auto-response` / `abuse-report` / `unknown`), reply_intent\n(`take_it_down` / `yes_keep_going` / `payout_question` /\n`tell_me_more` / `who_are_you`), status\n(`awaiting` / `replied` / `auto_handled` / `stale` / `new`) and\nfree-text search over from/subject/body.\n\n``awaiting_action=true`` is a compatibility shortcut equivalent to\n``status=awaiting`` — keeps the spec'd URL\n(``/admin/inbox?awaiting_action=true``) working as the stale-reply\nTG alert link target.\n\n``reply_intent`` (2026-08-08) matches the column the inbound-reply\nbridge and its backlog sweep stamp. Exact match on the enum value,\nsame shape as ``classification`` — no normalization, so the filter\nmeans exactly what ``by_reply_intent`` on\n``GET /v1/admin/inbox/stats`` counts. It exists because there was no\nserver-side way to ask \"which rows are take_it_down?\": the\n2026-08-07 backfill stamped 26 of them and finding those 26 meant\npaginating 800 rows client-side. Rows the bridge leaves NULL\n(UNRELATED / machine-notice) are outside every value of this filter\nby design.","operationId":"admin_inbox_list_v1_admin_inbox_get","parameters":[{"name":"classification","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Classification"}},{"name":"reply_intent","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Reply Intent"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":24},{"type":"null"}],"title":"Status"}},{"name":"awaiting_action","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Awaiting Action"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox List V1 Admin Inbox Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/sent-poll":{"post":{"summary":"Admin Sent Poll","description":"One-shot Sent-folder backfill — operator-triggered.\n\nWalks ``since_hours`` of M365 Sent messages, matches each back to\nthe inbound row via conversationId / In-Reply-To, stamps\n``operator_actioned_at`` on matches. Idempotent (each UPDATE is\nguarded by ``operator_actioned_at IS NULL``) so re-running is safe.\n\nDefault 30-day look-back matches the spec's \"historical backfill\"\nstep; max 90d covers any plausible operator catch-up window.","operationId":"admin_sent_poll_v1_admin_inbox_sent_poll_post","parameters":[{"name":"since_hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":1,"default":720,"title":"Since Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":500,"title":"Limit"}},{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Dry Run"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Sent Poll V1 Admin Inbox Sent Poll Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/{msg_id}/draft-reply":{"post":{"summary":"Admin Inbox Draft Reply","description":"LLM-draft a reply to an inbound publisher email. Returns the\ndraft text — operator reviews, edits, then POSTs to /send-reply.\nNo email is sent on this call.","operationId":"admin_inbox_draft_reply_v1_admin_inbox__msg_id__draft_reply_post","parameters":[{"name":"msg_id","in":"path","required":true,"schema":{"type":"integer","title":"Msg Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_InboxReplyDraftRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Draft Reply V1 Admin Inbox  Msg Id  Draft Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/{msg_id}/send-reply":{"post":{"summary":"Admin Inbox Send Reply","description":"Send the operator-approved reply via Postmark. Records the\noutbound on the InboundMessage row so the thread is auditable.","operationId":"admin_inbox_send_reply_v1_admin_inbox__msg_id__send_reply_post","parameters":[{"name":"msg_id","in":"path","required":true,"schema":{"type":"integer","title":"Msg Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_InboxReplySendRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Send Reply V1 Admin Inbox  Msg Id  Send Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/{msg_id}/rescue-as-article":{"post":{"summary":"Admin Inbox Rescue As Article","description":"Convert a triaged InboundMessage into an actual Article tagged\nto the named listener. Use when a forward landed in the operator\ninbox (e.g. sender wasn't a known listener yet, or path-A bailed\nsilently because no out-link was extractable). Runs the same\ningest_email pipeline so all the slug + dedupe + render-job logic\nis preserved — including the newsletter-as-article fallback added\nin 0058.","operationId":"admin_inbox_rescue_as_article_v1_admin_inbox__msg_id__rescue_as_article_post","parameters":[{"name":"msg_id","in":"path","required":true,"schema":{"type":"integer","title":"Msg Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RescueAsArticleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Rescue As Article V1 Admin Inbox  Msg Id  Rescue As Article Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/newsletter-discovery":{"get":{"summary":"Admin Newsletter Discovery","description":"Aggregate forward signal by newsletter author. Returns the list\nof unique sender_email + count + listeners-who-forwarded so the\noperator can prioritize newsletter author outreach. Newsletter\nauthors that show up here have 100% verified product-market fit\nwith our listener base — they're the ones our users are ALREADY\nforwarding without prompting.","operationId":"admin_newsletter_discovery_v1_admin_newsletter_discovery_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"min_forwards","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":2,"title":"Min Forwards"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Newsletter Discovery V1 Admin Newsletter Discovery Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/spotify/push-briefing":{"post":{"summary":"Listener Spotify Push Briefing","description":"Create or update the listener's 'Storyflo · Daily Flow' Spotify\nplaylist with today's queued episodes. Requires the listener to\nhave connected Spotify with `playlist-modify-private` scope.\n\nOperator note: scope expansion deployed in sprint π already\nincluded `user-top-read` + `user-read-recently-played`. To enable\npush-briefing for existing listeners we'd need them to re-OAuth\nwith the playlist-modify scope added — done implicitly when they\nnext connect/reconnect. This endpoint surfaces a clear 412 with a\nre-auth hint when the scope is missing.","operationId":"listener_spotify_push_briefing_v1_listener__listener_token__spotify_push_briefing_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Spotify Push Briefing V1 Listener  Listener Token  Spotify Push Briefing Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/{msg_id}":{"get":{"summary":"Admin Inbox Get","description":"Full body of a single inbound message — including the original\nPostmark payload — for triage drill-down.\n\n2026-08-08 · the action stamps plus the derived ``status`` /\n``answered`` / ``handled`` fields are surfaced here too. They were\nlist-only, so the drill-down — the view an operator opens precisely\nwhen asking \"did anyone actually reply to this one?\" — was the single\nplace that could not answer it. All additive.","operationId":"admin_inbox_get_v1_admin_inbox__msg_id__get","parameters":[{"name":"msg_id","in":"path","required":true,"schema":{"type":"integer","title":"Msg Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Get V1 Admin Inbox  Msg Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/stats":{"get":{"summary":"Admin Inbox Stats","description":"Classification mix + 24h volume for the admin inbox tile.\n\nSynthetic / smoke senders are excluded via the same\n``app.synthetic_accounts.not_synthetic_email`` predicate the\nlistener KPIs use (2026-07-16 revenue-accuracy fix — stability-cron\nand smoke-test mail previously inflated every count here).\n\n2026-08-06 additions (both purely additive — no existing key changed):\n\n* ``by_reply_intent`` / ``unclassified_replies`` — the 5-bucket\n  intent mix from :mod:`app.outreach.reply_classifier`. This is the\n  read that makes the \"replies never reach the classifier\" gap\n  VISIBLE: a large ``unclassified_replies`` next to a small\n  ``by_reply_intent`` means ingestion is not feeding classification.\n* ``monday_crm_sync`` — the nightly CRM sweep heartbeat\n  (``last_run_at`` + ``last_counts`` + the dispatcher's per-reason\n  failure tally), read from the generic ``kv_store`` row the tick\n  writes. Before this, a sweep that ran and had every monday write\n  rejected looked identical to a sweep that never ran. Extended here\n  rather than as a new page — this endpoint is already the inbox /\n  correspondence-pipeline tile.","operationId":"admin_inbox_stats_v1_admin_inbox_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Stats V1 Admin Inbox Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listeners":{"get":{"summary":"Admin Listeners List","description":"Operator support / abuse-triage view of listeners. Returns\nobfuscated tokens (last 8 chars only) so support can identify a\nlistener by their feed URL without surfacing the full credential\nif a screen is shared in a debug session.","operationId":"admin_listeners_list_v1_admin_listeners_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"tier","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Tier"}},{"name":"paused","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Paused"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Listeners List V1 Admin Listeners Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listeners/stats":{"get":{"summary":"Admin Listeners Stats","description":"Headline counters for the /admin/listeners dashboard. Cheap\naggregation queries — runs in <50ms on Postgres up to ~100k rows.","operationId":"admin_listeners_stats_v1_admin_listeners_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Listeners Stats V1 Admin Listeners Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/status":{"get":{"summary":"Admin Webauthn Status","operationId":"admin_webauthn_status_v1_admin_webauthn_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Status V1 Admin Webauthn Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/register/start":{"post":{"summary":"Admin Webauthn Register Start","operationId":"admin_webauthn_register_start_v1_admin_webauthn_register_start_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Register Start V1 Admin Webauthn Register Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/register/finish":{"post":{"summary":"Admin Webauthn Register Finish","operationId":"admin_webauthn_register_finish_v1_admin_webauthn_register_finish_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Register Finish V1 Admin Webauthn Register Finish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/auth/start":{"post":{"summary":"Admin Webauthn Auth Start","operationId":"admin_webauthn_auth_start_v1_admin_webauthn_auth_start_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Auth Start V1 Admin Webauthn Auth Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/auth/finish":{"post":{"summary":"Admin Webauthn Auth Finish","operationId":"admin_webauthn_auth_finish_v1_admin_webauthn_auth_finish_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Auth Finish V1 Admin Webauthn Auth Finish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/webauthn/credentials/{credential_id}":{"delete":{"summary":"Admin Webauthn Remove","operationId":"admin_webauthn_remove_v1_admin_webauthn_credentials__credential_id__delete","parameters":[{"name":"credential_id","in":"path","required":true,"schema":{"type":"string","title":"Credential Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Webauthn Remove V1 Admin Webauthn Credentials  Credential Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/agent-referrals":{"get":{"summary":"Listener Agent Referrals","description":"List agent installs that this listener referred + the running\ncredit total. Token-as-credential (same pattern as the other\nlistener endpoints).","operationId":"listener_agent_referrals_v1_listeners__token__agent_referrals_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Agent Referrals V1 Listeners  Token  Agent Referrals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/access-token/rotate":{"post":{"summary":"Publisher Rotate Access Token","description":"Publisher-initiated rotation of their dashboard access token.\nAuth is the EXISTING token — possessing it is the credential, same\ntrust model as listener tokens. After rotation the old token is\nimmediately invalid. Returns the new token once; we never show\nplaintext again.","operationId":"publisher_rotate_access_token_v1_publisher__tenant_slug__access_token_rotate_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherKeyRotate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Rotate Access Token V1 Publisher  Tenant Slug  Access Token Rotate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/access-token/status":{"get":{"summary":"Publisher Access Token Status","operationId":"publisher_access_token_status_v1_publisher__tenant_slug__access_token_status_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Access Token Status V1 Publisher  Tenant Slug  Access Token Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/push/subscribe":{"post":{"summary":"Listener Push Subscribe","description":"D-5.8 · register a Web Push PushSubscription for the listener.\n\nBody shape (matches W3C PushSubscription.toJSON()):\n  {\n    \"endpoint\": \"https://fcm.googleapis.com/...\",\n    \"keys\": {\"p256dh\": \"...\", \"auth\": \"...\"}\n  }\n\nIdempotent on (endpoint) — re-subscribing from the same browser\njust refreshes last_seen_at instead of creating a duplicate row.","operationId":"listener_push_subscribe_v1_listeners__token__push_subscribe_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Push Subscribe V1 Listeners  Token  Push Subscribe Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Listener Push Unsubscribe","description":"D-5.8 · soft-disable a push subscription. Body: {\"endpoint\": \"...\"}.\nMarks disabled_at; row stays for analytics.","operationId":"listener_push_unsubscribe_v1_listeners__token__push_subscribe_delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Push Unsubscribe V1 Listeners  Token  Push Subscribe Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/push/public-key":{"get":{"summary":"Push Vapid Public Key Legacy","description":"Returns the VAPID public key for the frontend to pass into\npushManager.subscribe({ applicationServerKey }).\n\nPreviously read STORYFLO_VAPID_PUBLIC_KEY via os.environ, which\nnever matched the actual Fly secret name (VAPID_PUBLIC_KEY) — so\nthis endpoint silently returned \"\" and the subscribe UI passed an\nempty applicationServerKey to pushManager.subscribe(). Now reads\nsettings.vapid_public_key (the pydantic field that DOES match the\nFly secret) so this legacy path returns the same value as\n/v1/push/vapid-public-key. Frontend caller at\napp/listen/share/page.tsx hits this URL.","operationId":"push_vapid_public_key_legacy_v1_push_public_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Push Vapid Public Key Legacy V1 Push Public Key Get"}}}}}}},"/v1/admin/overview":{"get":{"summary":"Admin Overview","description":"Master operator dashboard payload — one round-trip surfaces\nevery KPI the /admin landing page renders. Categories:\n  - listeners (total / last 24h / last 7d / by tier)\n  - publishers (total / last 24h / Stripe-connected count)\n  - articles (total / last 24h / top 5 by send_count)\n  - monetization (play value 30d, x402 30d, rewards pending vs disbursed)\n  - render queue (queued / running / done 24h / failed 24h)\n  - ssp fill rate 24h per ssp\nCheap aggregation queries — runs in <100ms on Postgres up to\nmillions of rows because each filter hits an index.","operationId":"admin_overview_v1_admin_overview_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Overview V1 Admin Overview Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/funnel":{"get":{"summary":"Admin Alpha Funnel","description":"Alpha-cohort retention funnel.\n\nSix steps over the last N days:\n  1. subscribed     — listeners with a row in listener_subscriptions\n  2. queued         — at least one Article matched their verticals\n  3. played         — at least one ListenerHistory row\n  4. completed      — at least one ListenerHistory.completed=true\n  5. returned       — listened on >=2 distinct UTC days\n  6. upgraded       — flipped free → plus / pro inside the\n                      window (monetization audit Action #5,\n                      2026-05-31). Reads\n                      ``listener_subscriptions.tier_upgraded_at``\n                      (added in migration 0143; backfilled from\n                      ``plus_since`` so historical paid listeners\n                      aren't blanked-out).\n\nThe point is signal: how many invitees actually came back, and how\nmany of those CONVERTED? The drop-off between stages tells you\nwhich gate to invest in next (intake quality, queue UX, audio\nquality, repeat-listening hooks, monetization wedge).","operationId":"admin_alpha_funnel_v1_admin_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":14,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Alpha Funnel V1 Admin Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/content-freshness":{"get":{"summary":"Admin Content Freshness","description":"Content freshness — what share of the live article corpus is fresh?\n\nBuckets quality-passing articles (``is_quality=True`` — the feed-\neligible corpus) by the age of their ``created_at`` timestamp:\n\n  * ``fresh_24h`` — published in the last 24 hours\n  * ``fresh_7d``  — published in the last 7 days (cumulative, so it\n                    includes ``fresh_24h``)\n  * ``stale``     — older than 7 days but still inside the window\n\nThe denominator (``total``) is every quality article created inside\nthe requested ``window`` (default 30d; the window floor is 7d so the\n24h/7d buckets always nest). Percentages are relative to ``total``.\n\nAnswers the operator question \"is the feed going stale?\" at a glance:\na falling fresh-share means intake has stalled or the quality gate is\nrejecting too much of what came in.","operationId":"admin_content_freshness_v1_admin_content_freshness_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","maxLength":8,"default":"30d","title":"Window"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Content Freshness V1 Admin Content Freshness Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/monetization/trial-funnel":{"get":{"summary":"Admin Trial Funnel","description":"Trial → paid conversion funnel.\n\nOf the listeners who STARTED a trial inside the last ``days``, what\nshare upgraded to a paid tier (plus / pro) — within 7 days, within\n30 days, or ever (lifetime)? Reads the append-only\n``listener_subscription_events`` log (migration 0143): a\n``trial_started`` row marks the cohort entry, a subsequent\n``upgraded`` row (to a paid tier) marks conversion.\n\nConversion is measured relative to EACH listener's own trial-start\ntime, not a fixed calendar window, so the 7d / 30d buckets are true\ntime-to-convert cohorts. ``lifetime`` counts any paid upgrade after\nthe trial started, regardless of elapsed time.\n\nThe point is signal on the trial wedge: a healthy 7d conversion\nmeans the trial is doing its job; a big gap between 7d and lifetime\nmeans listeners convert late and a nudge campaign could pull it\nforward.","operationId":"admin_trial_funnel_v1_admin_monetization_trial_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Trial Funnel V1 Admin Monetization Trial Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embedders/{slug}/funnel-detailed":{"get":{"summary":"Admin Embedder Funnel Detailed","description":"Per-embedder attribution funnel: story → signup → paid.\n\nFor a single partner embedder, reads the full pull-through ladder\ninside the last ``days`` window:\n\n  1. ``stories_served`` — plays carrying this embedder's\n     ``source_embedder_slug`` (the agent surface actually served a\n     story that got listened to)\n  2. ``signups``        — listeners whose\n     ``referred_by_embedder_slug`` is this embedder (a served story\n     converted to a private-feed subscription)\n  3. ``paid``           — of those signups, how many are on a paid\n     tier (plus / pro) now\n\nThe flywheel snapshot aggregates these across ALL embedders; this\nendpoint breaks them out per-partner so the operator can see which\nsurfaces convert (and which just drive vanity plays). Conversion\nrates are stage-to-stage: ``signup_rate`` = signups / stories,\n``paid_rate`` = paid / signups.\n\n404 if the slug is not a registered embedder.","operationId":"admin_embedder_funnel_detailed_v1_admin_embedders__slug__funnel_detailed_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embedder Funnel Detailed V1 Admin Embedders  Slug  Funnel Detailed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/anf/{slug}":{"get":{"summary":"Article Anf","description":"Apple News Format JSON for a single article.\n\nPowers the Apple News Publisher submission flow — operators can\ndrop the response into News Publisher's article editor or POST it\nvia the official Apple News CLI / Channel API. Public read; the\ncanonical URL is the article's own /story/{slug}, so distributing\nthe ANF further isn't a leak.\n\nAudio callout component points back to storyflo.com/story/{slug}\nsince Apple News doesn't host our TTS audio directly.","operationId":"article_anf_v1_anf__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Anf V1 Anf  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/unlock":{"post":{"summary":"Article Unlock","description":"Verify an on-chain USDC transfer to STORYFLO_PROTOCOL_WALLET and\ngrant a 24h pay-per-article unlock.\n\nThe unlock is keyed on the payer wallet address (from the verified\ntransfer's `from` topic) — same wallet replays the unlock without\nre-paying within the entitlement window. Cross-references the\nexisting X402Payment table so the unlock state is queryable from\none place. Reuses the same on-chain receipt-walking logic as the\ntier upgrade endpoint to keep verification consistent.","operationId":"article_unlock_v1_articles__slug__unlock_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ArticleUnlockBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Unlock V1 Articles  Slug  Unlock Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/unlock-status":{"get":{"summary":"Article Unlock Status","description":"Check whether a wallet has unlocked this article in the last 24h.\nPublic read — wallet is the entitlement key (no token needed).","operationId":"article_unlock_status_v1_articles__slug__unlock_status_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"wallet","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Wallet"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Unlock Status V1 Articles  Slug  Unlock Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/reward-claim":{"post":{"summary":"Listener Reward Claim","description":"Record a PYUSD/USDC reward owed to a listener for completing a\nsponsored creative end-to-end. Idempotent at the\n(listener, creative, day) granularity — replays return the existing\nrow without crediting twice.\n\nValidates the play actually happened by cross-referencing\nSspBidLog (for SSP fills) or AdImpression (for direct creatives) on\nthe same UTC day. Spoofed claims (no matching impression) return\n422 — listener got nothing to credit.\n\nDisbursement is NOT done in-line; the reward row lands in\nstatus=pending and a separate worker (``listener_reward_worker``,\nlanding in a follow-up sprint) sweeps pending rows + broadcasts on\nthe configured rail.","operationId":"listener_reward_claim_v1_listener__listener_token__reward_claim_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RewardClaimBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Reward Claim V1 Listener  Listener Token  Reward Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/rewards":{"get":{"summary":"Listener Rewards List","description":"Listener-facing read of accrued + redeemed rewards. Drives the\n'you've earned $X.XX so far' card on /listen/preferences.","operationId":"listener_rewards_list_v1_listener__listener_token__rewards_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Rewards List V1 Listener  Listener Token  Rewards Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/upgrade":{"post":{"summary":"Listener Tier Upgrade","description":"Verify an on-chain USDC transfer to the protocol wallet and flip\nthe listener's tier. Listener signs the transfer from their connected\nwallet (any Base-compatible wallet — Coinbase Smart Wallet, MetaMask,\nRainbow, etc.) and posts the tx_hash here. Idempotent on tx_hash via\nthe X402Payment receipt table.","operationId":"listener_tier_upgrade_v1_listener__listener_token__upgrade_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TierUpgradeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Tier Upgrade V1 Listener  Listener Token  Upgrade Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/referrals":{"get":{"summary":"Listener Referrals","description":"List referral credits accrued by this listener and the redemption\nstate. Returns total redeemable days + share-link the listener can\npass to friends.","operationId":"listener_referrals_v1_listener__listener_token__referrals_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Referrals V1 Listener  Listener Token  Referrals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/referrals/redeem":{"post":{"summary":"Listener Referrals Redeem","description":"Redeem all unredeemed referral credits — extends tier_expires_at\nby the sum of unredeemed days_credit and flips each Referral row\nto redeemed. Atomic: either all credits redeem or none do.","operationId":"listener_referrals_redeem_v1_listener__listener_token__referrals_redeem_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Referrals Redeem V1 Listener  Listener Token  Referrals Redeem Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/ssp-summary":{"get":{"summary":"Admin Ssp Summary","description":"Aggregated fill-rate + eCPM by SSP for the lookback window.\nPowers the /admin/ads dashboard. Returns one row per SSP with\nrequests / fills / fill_rate / eCPM. Drops SSPs that haven't seen\ntraffic in the window so the table stays tight.","operationId":"admin_ssp_summary_v1_admin_ads_ssp_summary_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ssp Summary V1 Admin Ads Ssp Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/wipe":{"delete":{"summary":"Admin Wipe Publisher","description":"Cascade-delete every row tied to ``tenant_slug``: publisher,\nintake sources, articles, render jobs, audio cache keys (left in\nR2 — Cloudflare lifecycle handles eviction), webhooks. Used to\nclear test publishers (e.g. FLATLAY Inc) after end-to-end loop\nvalidation so they don't pollute the alpha.\n\nDESTRUCTIVE — there is no undo. Cascade order matches FK\nconstraints: jobs → articles → intake sources → webhooks →\npublisher row.","operationId":"admin_wipe_publisher_v1_admin_publishers__tenant_slug__wipe_delete","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Wipe Publisher V1 Admin Publishers  Tenant Slug  Wipe Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/live":{"get":{"summary":"Admin Live Stream","description":"Server-Sent Events stream of platform activity for the\nreal-time admin dashboard. Pushes a JSON event every 2 seconds\nwith a snapshot of: pending render jobs, done-this-hour count,\nlistener subscriptions in last 5 min, sent rewards count,\narticle-summary backlog, and the cost-guardrail headline number.\n\nCheap by design — each tick is a handful of COUNT() queries on\nindexed columns, ~5ms each. Browser clients hold an EventSource\nopen; we close after 60s so a forgotten tab doesn't pin a\nPostgres connection forever (client auto-reconnects).\n\nEventSource can't set custom headers, so the admin token is\naccepted from either the X-Storyflo-Email-Token header (canonical)\nor the ?token= query string (browser SSE clients).","operationId":"admin_live_stream_v1_admin_live_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/regression/run":{"post":{"summary":"Admin Regression Run","description":"Run the regression smoke battery once and return per-check\npass/fail. Same checks the background worker runs every 10 min;\nthis is the on-demand version for post-deploy verification.","operationId":"admin_regression_run_v1_admin_regression_run_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Regression Run V1 Admin Regression Run Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/regression/status":{"get":{"summary":"Admin Regression Status","description":"Last regression-tick snapshot + when it ran. No fresh execution;\ncheap read for the live-dashboard SSE to surface.","operationId":"admin_regression_status_v1_admin_regression_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Regression Status V1 Admin Regression Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/detail":{"get":{"summary":"Admin Publisher Detail","description":"Per-publisher drill-down for the admin panel. Returns:\n\n  * publisher metadata (name, slug, contact_email, stripe state)\n  * intake source list with status + last_polled_at\n  * recent articles (newest first) with: title, slug, vertical,\n    send_count, trending_score, render-job status, listen_seconds\n  * render queue depth for this publisher\n  * 24h play volume + revenue micros\n\nOne round-trip surfaces everything the per-publisher /admin\ndetail page needs.","operationId":"admin_publisher_detail_v1_admin_publishers__tenant_slug__detail_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"article_limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Article Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publisher Detail V1 Admin Publishers  Tenant Slug  Detail Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/spam/scrub":{"delete":{"summary":"Admin Articles Scrub Spam","description":"Hard-delete articles that are obviously scrape-failure /\nspam noise. Targets:\n\n  * Title is a URL (http://… or https://…) — happens when an\n    intake fetcher gets only a redirect with no extracted body.\n  * Title or url contains image-CDN domains (m.media-amazon.com\n    images, etc.) — happens when an Amazon Music link gets sent\n    to the email-intake instead of an article.\n  * Body contains the placeholder 'Storyflo couldn't fetch' or\n    is shorter than 200 chars (real articles are always longer).\n  * Publisher domains we never want surfaced (postmarkapp.com,\n    media-amazon.com, amazon.com/podcasters).\n\nCascade-deletes related render_jobs. Returns counts. Pass\n``dry_run=true`` to preview without deleting.","operationId":"admin_articles_scrub_spam_v1_admin_spam_scrub_delete","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Dry Run"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Scrub Spam V1 Admin Spam Scrub Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/trending/recompute":{"post":{"summary":"Admin Trending Recompute","description":"Manually trigger one tick of the trending-score worker. Useful\nafter tuning the TRENDING_*_WEIGHT env vars to see the new ranking\nimmediately rather than waiting for the next hourly tick.","operationId":"admin_trending_recompute_v1_admin_trending_recompute_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Trending Recompute V1 Admin Trending Recompute Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-duration":{"post":{"summary":"Admin Backfill Duration","description":"Backfill ``Article.listen_seconds`` from the actual rendered\naudio file size. Apple/Spotify/Amazon all surface itunes:duration\nas the visible episode length; articles ingested before the\nrender worker persisted duration_sec show as ``0 seconds`` in\nevery podcast app — this endpoint walks rendered articles and\nestimates duration from the wav byte length.","operationId":"admin_backfill_duration_v1_admin_articles_backfill_duration_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":500,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Duration V1 Admin Articles Backfill Duration Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-waveform-peaks":{"post":{"summary":"Admin Backfill Waveform Peaks","description":"Backfill ``Article.waveform_peaks`` for rows rendered before the\nreal-peaks worker shipped. Walks articles with NULL waveform_peaks\nthat have a done RenderJob, fetches the rendered wav via the cache\nbackend, decodes 256 normalized amplitude peaks with ffmpeg, and\npersists them. ``dry_run=true`` (default) reports counts only.","operationId":"admin_backfill_waveform_peaks_v1_admin_articles_backfill_waveform_peaks_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":200,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Waveform Peaks V1 Admin Articles Backfill Waveform Peaks Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-opus":{"post":{"summary":"Admin Backfill Opus","description":"Walk every done render and produce the ``.opus`` sibling in R2\nfor any key that doesn't have one. Returns immediately after\nspawning the background task — poll\n``/v1/admin/articles/backfill-opus/status`` for progress.\n\nEarlier revisions enqueued through ``audio_normalize_queue`` (an\nin-process asyncio.Queue). That worked for new renders because\nthe render worker and normalize worker share a process, but not\nfor this admin endpoint — the API and worker are separate Fly\nmachines, so keys queued on the API never reach the worker's\nconsumer. We now do the work directly on whichever machine\nreceives the call (Docker image is identical so ffmpeg is there\ntoo).","operationId":"admin_backfill_opus_v1_admin_articles_backfill_opus_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":200,"title":"Limit"}},{"name":"sleep_between","in":"query","required":false,"schema":{"type":"number","maximum":60.0,"minimum":0.0,"default":5.0,"title":"Sleep Between"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Opus V1 Admin Articles Backfill Opus Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-opus/status":{"get":{"summary":"Admin Backfill Opus Status","operationId":"admin_backfill_opus_status_v1_admin_articles_backfill_opus_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Opus Status V1 Admin Articles Backfill Opus Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio/opus-encode-key":{"post":{"summary":"Admin Opus Encode Key","description":"Produce the .opus sibling for a specific cache key. Used for\nkeys that were rendered via direct /v1/render and have no\nRenderJob row the backfill could walk.","operationId":"admin_opus_encode_key_v1_admin_audio_opus_encode_key_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Opus Encode Key V1 Admin Audio Opus Encode Key Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio/quality-audit":{"post":{"summary":"Admin Audio Quality Audit","description":"Audit recent done renders for broken audio.\n\nPulls the latest done RenderJob per article whose ``finished_at`` is\nafter ``since`` (default: 1 hour ago), HEADs each ``audio_url``, and\nflags failures + duration-suspicious shorts.","operationId":"admin_audio_quality_audit_v1_admin_audio_quality_audit_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AudioQualityAuditBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Audio Quality Audit V1 Admin Audio Quality Audit Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio-cache/sweep-empty":{"post":{"summary":"Admin Audio Cache Sweep Empty","description":"Sweep R2 audio-cache for poisoned (sub-``min_bytes``) WAVs.\n\nBackground: a concurrency=8 era produced thousands of 0-byte / sub-1s\nWAVs that now cause workers to claim a job, hit cache, fail audio_qa\nwith ``empty_or_unreadable_audio``, and stall the render queue. This\nendpoint enumerates audio cache entries and deletes the poisoned ones\nso the next render call re-runs Piper end-to-end. Scoped strictly to\n``.wav`` objects via :meth:`R2Backend.list_audio_objects`; non-audio\ncaches (images at ``img/``, etc.) are never touched.\n\nAuth: admin token (X-Storyflo-Email-Token). DEFAULT is dry-run; set\n``dry_run=false`` to actually delete.","operationId":"admin_audio_cache_sweep_empty_v1_admin_audio_cache_sweep_empty_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AudioCacheSweepBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Audio Cache Sweep Empty V1 Admin Audio Cache Sweep Empty Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/contact-email-backfill-bulk":{"post":{"summary":"Admin Contact Email Backfill Bulk","description":"Fire-and-forget bulk fill of ``Publisher.contact_email`` across\nevery shadow publisher that has none set. The cold-outreach\neligibility filter requires this field; on a fresh deploy almost\nevery shadow row is missing it, which is why\n``/v1/admin/outreach/cold-eligible`` returns 0.\n\nWalks the catalogue in 50-row chunks. Poll\n``/v1/admin/publishers/contact-email-backfill-bulk/status`` for\nprogress. ``target`` caps the total publishers attempted in one\nrun — the default 2000 covers the current shadow-publisher\ncatalogue with headroom.","operationId":"admin_contact_email_backfill_bulk_v1_admin_publishers_contact_email_backfill_bulk_post","parameters":[{"name":"target","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"default":2000,"title":"Target"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Contact Email Backfill Bulk V1 Admin Publishers Contact Email Backfill Bulk Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/contact-email-backfill-bulk/status":{"get":{"summary":"Admin Contact Email Backfill Bulk Status","operationId":"admin_contact_email_backfill_bulk_status_v1_admin_publishers_contact_email_backfill_bulk_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Contact Email Backfill Bulk Status V1 Admin Publishers Contact Email Backfill Bulk Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/parser-backfill":{"post":{"summary":"Admin Articles Parser Backfill","description":"Re-run the Anthropic Haiku newsletter parser on legacy articles\nthat predate the parser (parsed_json IS NULL) so the new story-page\nrender path lights up retroactively. Returns immediately after\nspawning the background task — poll\n``/v1/admin/articles/parser-backfill/status`` for progress.","operationId":"admin_articles_parser_backfill_v1_admin_articles_parser_backfill_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Parser Backfill V1 Admin Articles Parser Backfill Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/parser-backfill/status":{"get":{"summary":"Admin Articles Parser Backfill Status","operationId":"admin_articles_parser_backfill_status_v1_admin_articles_parser_backfill_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Parser Backfill Status V1 Admin Articles Parser Backfill Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/daily-brief/run":{"post":{"summary":"Admin Daily Brief Run","description":"Manually trigger one tick of the daily-brief worker. Useful\non launch day before the scheduled hour (default 6am UTC) hits,\nor to regenerate after content updates. Idempotent: if today's\nbrief already exists, returns ``created=False``.\n\n2026-06-08 · ``force=true`` query param bypasses the hour gate\nso the smoke-e2e workflow can fire the full 9-brief pipeline\non demand. Production cron path (6 UTC) leaves force unset.","operationId":"admin_daily_brief_run_v1_admin_daily_brief_run_post","parameters":[{"name":"force","in":"query","required":false,"schema":{"type":"boolean","description":"When True, bypasses the hour gate (DAILY_BRIEF_UTC_HOUR) so the tick can fire pre-6am UTC. Idempotency still holds via the slug-exists check, so a re-run on the same day is still a noop for the cross-vertical brief. Used by the smoke-e2e verifier to exercise the full pipeline in under 10 min instead of waiting 24h for the next 6 UTC tick.","default":false,"title":"Force"},"description":"When True, bypasses the hour gate (DAILY_BRIEF_UTC_HOUR) so the tick can fire pre-6am UTC. Idempotency still holds via the slug-exists check, so a re-run on the same day is still a noop for the cross-vertical brief. Used by the smoke-e2e verifier to exercise the full pipeline in under 10 min instead of waiting 24h for the next 6 UTC tick."},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Daily Brief Run V1 Admin Daily Brief Run Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/r2/promote-all":{"post":{"summary":"Admin R2 Promote All","description":"Kick off a background backfill of every cache object into the\nconfigured promotion buckets. Returns immediately; the actual\ncopy continues asynchronously. Poll GET /v1/admin/r2/promote-status\nfor progress.\n\nConcurrency: only one backfill at a time per app instance. Calling\nagain while one is running returns 409 with the current state.","operationId":"admin_r2_promote_all_v1_admin_r2_promote_all_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50000,"minimum":1,"default":2000,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin R2 Promote All V1 Admin R2 Promote All Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/r2/promote-status":{"get":{"summary":"Admin R2 Promote Status","description":"Return the in-flight (or last-completed) R2 backfill state.","operationId":"admin_r2_promote_status_v1_admin_r2_promote_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin R2 Promote Status V1 Admin R2 Promote Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/circle/platform-wallet":{"post":{"summary":"Admin Circle Platform Wallet","description":"Mint or fetch the Circle platform wallet that funds listener\nreward disbursements. Idempotent — Circle returns the same wallet\non re-call. Operator copies ``wallet_id`` into the Fly secret\n``CIRCLE_PLATFORM_WALLET_ID`` to enable the listener-reward rail.\n\nTop up that wallet with the existing private-alpha USDC balance\n(sandbox.circle.com → wallets → fund) and rewards start flowing\non the next worker tick.","operationId":"admin_circle_platform_wallet_v1_admin_circle_platform_wallet_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Circle Platform Wallet V1 Admin Circle Platform Wallet Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/push/vapid-public-key":{"get":{"summary":"Push Vapid Public Key","description":"D-5.8 · expose the VAPID public key so the browser can call\npushManager.subscribe({applicationServerKey}). Public — the\npublic key is not a secret. When VAPID_PUBLIC_KEY is unset\n(local dev), returns an empty value so the frontend can\nsuppress the subscribe UI.","operationId":"push_vapid_public_key_v1_push_vapid_public_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Push Vapid Public Key V1 Push Vapid Public Key Get"}}}}}}},"/v1/admin/push/test-send":{"post":{"summary":"Admin Push Test Send","description":"D-5.8 · fire ONE web-push notification at a listener for live\nend-to-end verification. Operator hits this after subscribing in a\nbrowser to confirm VAPID signing + the SW push handler are wired.","operationId":"admin_push_test_send_v1_admin_push_test_send_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Push Test Send V1 Admin Push Test Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/recommend":{"get":{"summary":"Listener Vertical Recommend","description":"D-5.3 · vertical recommendations for the queue sidebar.\n\nLooks at what the listener has actually listened to / saved\nrecently and returns up to 3 verticals they DON'T currently\nfollow but co-occur strongly in similar listeners' feeds. Used\nby /listen/queue to render a 'Try also: tech, finance' widget.\n\nHeuristic v0: rank by total send_count of articles in each\nvertical NOT in the listener's vertical set, weighted by how\noften listeners with overlapping verticals also saved articles\nin those verticals. Caches per-listener for 1 hour.","operationId":"listener_vertical_recommend_v1_listeners__token__recommend_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Vertical Recommend V1 Listeners  Token  Recommend Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voices/{voice_id}/sample":{"get":{"summary":"Voice Sample","description":"D-5.10 · listener voice preview.\n\nReturns a 6-second WAV of the requested voice saying a fixed line\nso visitors on /listen/preferences can audition before locking\nin. The line is intentionally short and constant — Kokoro caches\non (voice, text) so the first hit pays the render cost and every\nsubsequent hit is a free cache lookup.\n\nRate-limited 60/min/IP. No API key, no auth — voice quality IS\nthe marketing surface. Public CDN-cached for 24 hours.","operationId":"voice_sample_v1_voices__voice_id__sample_get","parameters":[{"name":"voice_id","in":"path","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publishers/validate-rss":{"post":{"summary":"Publishers Validate Rss","description":"D-5.4 · public RSS validator for the publisher onboarding form.\n\nTakes ``{\"url\": \"https://example.com/feed\"}`` and returns a tiny\nsummary so the publisher can verify their feed parses correctly\nBEFORE submitting it to the intake queue. Avoids the most common\nonboarding fail (typo'd URL, blog returns HTML instead of RSS,\nfeed locked behind Cloudflare).\n\nReturns:\n  - ok: bool\n  - title, description, link, language\n  - items_count, latest_pub\n  - reason (only on failure)\n\nRate-limited 20/min/IP — generous enough for a real publisher\niterating, tight enough to block scraper enumeration.","operationId":"publishers_validate_rss_v1_publishers_validate_rss_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Publishers Validate Rss V1 Publishers Validate Rss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/probe-fetcher":{"post":{"summary":"Admin Intake Probe Fetcher","description":"Probe the per-publisher fetcher policy chain against a URL.\n\nBody: ``{\"url\": \"https://www.bloomberg.com/...\"}``. We attempt the\nstandard fetch first; if it 4xx's we walk the policy chain\n(AMP → Google AMP cache → Wayback Machine → Googlebot UA → ...)\nand report which strategy succeeded. Used to diagnose new\nbot-block surprises without redeploying.","operationId":"admin_intake_probe_fetcher_v1_admin_intake_probe_fetcher_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Probe Fetcher V1 Admin Intake Probe Fetcher Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/seed":{"post":{"summary":"Admin Intake Seed","description":"Bulk-create Publisher + IntakeSource rows from a curated seed\nlist. Idempotent on tenant_slug — re-running with the same feeds\nis a no-op (existing publishers + sources are returned unchanged).\n\nSources land in ``status=pending`` so a human still flips activation;\nwe don't auto-poll feeds that haven't been operator-vetted (anti-\nspam + anti-low-quality).","operationId":"admin_intake_seed_v1_admin_intake_seed_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_IntakeSeedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Seed V1 Admin Intake Seed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/seed-demo-agents":{"post":{"summary":"Admin Seed Demo Agents","description":"Seed two recognizable test accounts so the operator console has\nbaseline data on a fresh DB:\n\n  • DEMO LISTENER · agent.listener@storyflo.com — Plus tier, broad\n    vertical interests, plus 5 ListenerHistory rows seeded against\n    the most recent articles so the funnel + admin/listeners pages\n    show real numbers.\n\n  • DEMO PUBLISHER · tenant_slug=demo-publisher — has a stable\n    access_token and 3 sample articles with body_text + summaries\n    so /admin/publishers and /admin/queue show meaningful rows.\n    Render jobs are auto-enqueued via the existing intake path.\n\nIdempotent: re-running with the same identities is a no-op for the\nlistener subscription (returns the existing row) and the publisher\n(matched by tenant_slug). Article slugs include 'demo-' prefix so\nthey never collide with real intake.","operationId":"admin_seed_demo_agents_v1_admin_seed_demo_agents_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Seed Demo Agents V1 Admin Seed Demo Agents Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/backfill-all":{"post":{"summary":"Admin Render Backfill All","description":"Cache pre-render strategy — enqueue a render job for every\narticle that does NOT already have a done job. Idempotent: re-\nrunning with ``only_missing=True`` (default) only touches articles\nstill in the gap. Use ``only_missing=False`` to force a re-render\nof everything (e.g. after a voice-quality bump).\n\nPairs with the auto-enqueue already in the intake worker, which\nhandles every newly-ingested article. This endpoint is the\none-shot backfill for what's already in the DB.\n\nRenders flow through the standard provider chain\n(kokoro → openai_tts → elevenlabs → cartesia). Failures land in\n/admin/queue with last_error set; retry is operator-driven via\nthe existing retry-by-error-pattern endpoint.","operationId":"admin_render_backfill_all_v1_admin_render_backfill_all_post","parameters":[{"name":"voice_id","in":"query","required":false,"schema":{"type":"string","maxLength":40,"default":"atlas","title":"Voice Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":500,"title":"Limit"}},{"name":"only_missing","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Only Missing"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Render Backfill All V1 Admin Render Backfill All Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/sources":{"get":{"summary":"Admin Intake Sources List","description":"Diagnostic surface for the intake worker. Lists every IntakeSource\nrow with its current status, last_polled_at, last_error, and item\ncount. Use ?only_failing=1 to filter to sources that have a\nnon-empty last_error — the fastest way to see why ~60 freshly-\nactivated feeds aren't producing articles.","operationId":"admin_intake_sources_list_v1_admin_intake_sources_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"only_failing","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Only Failing"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Sources List V1 Admin Intake Sources Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/activate-pending":{"post":{"summary":"Admin Intake Activate Pending","description":"Bulk-flip every IntakeSource currently in status=pending to\nstatus=active. Used right after /v1/admin/intake/seed-curated\npopulates the 65-feed bundle — once the operator has eyeballed\nthe list, one POST here lights them all up at once instead of\nclicking through /admin/queue rows.\n\nIdempotent: re-running on an empty pending set is a no-op.","operationId":"admin_intake_activate_pending_v1_admin_intake_activate_pending_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Activate Pending V1 Admin Intake Activate Pending Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/reconcile-stale-failures":{"post":{"summary":"Admin Intake Reconcile Stale Failures","description":"Reconcile any IntakeSource rows where ``status='active'`` but\n``consecutive_failures >= settings.intake_max_consecutive_failures``.\n\nBackground (2026-05-31): The intake worker auto-disables sources at\nthe cf++ moment, so a poll at cf=9 incrementing to cf=10 flips\nstatus=error. Two paths can leave a row stuck at ``status=active,\ncf>=threshold``:\n\n  (1) The `/v1/intake/sources/{src_id}/activate` endpoint pre-2026-\n      05-31 flipped status active without resetting cf — patched in\n      the same PR as this endpoint.\n  (2) A row was at high cf BEFORE PR #314 (intake auto-disable\n      landed); the existing rows never had a cf++ event after the\n      feature deployed because their poll_interval is 6h and the\n      feature flag-check only runs at increment time.\n\nSince the threshold check fires only on the next cf++ event, these\nrows soak poll cycles indefinitely (every ``poll_interval_minutes``\nthey spin up, fail, bump cf, log a WARNING, then sit again). This\nendpoint catches them up: any active row at cf>=threshold is flipped\nto status=error in one batched UPDATE.\n\nIdempotent + cheap (one indexed scan + small UPDATE). Set\n``?dry_run=1`` to inventory without writing.\n\nSensitive: requires the same admin token as the rest of\n``/v1/admin/intake/*``. No TG fan-out (the per-row WARNING was\nalready logged at the cf++ event); the per-batch summary alone is\nenough for the operator dashboard.","operationId":"admin_intake_reconcile_stale_failures_v1_admin_intake_reconcile_stale_failures_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Dry Run"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Reconcile Stale Failures V1 Admin Intake Reconcile Stale Failures Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publishers/{slug}/distribution":{"get":{"summary":"Publisher Distribution Dashboard","description":"D-2.1 · public-facing per-publisher distribution dashboard.\n\nReturns the live revenue picture for a publisher: total micros\nearned in the window, broken down by source (play/ad_served/\nsubscription/tip), top earning articles, and a 30-day trend.\n\nPublic read — no auth gate. The data here is what we'd be willing\nto show on a marketing page anyway. Sensitive operator data\n(full payout details, internal margins) lives behind admin auth\non /admin/publishers/{slug}/detail.","operationId":"publisher_distribution_dashboard_v1_publishers__slug__distribution_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Distribution Dashboard V1 Publishers  Slug  Distribution Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/marketplace/sponsored-episodes":{"post":{"summary":"Marketplace Sponsored Listing","description":"D-2.5 · Public submission endpoint for sponsored-episode bids.\n\nA merchant fills out the form on /partner/sponsor with:\n  - which article they want to sponsor (article_slug)\n  - sponsor name + url + 30-second creative text\n  - bid_micros_per_play (the price they'll pay per completed listen)\n\nWe return a placeholder review status; an operator approves\nvia /admin/sponsorships/{id}/approve in a follow-up. The submission\nitself is bounded by the existing rate limiter, IP-logged for\nabuse triage.","operationId":"marketplace_sponsored_listing_v1_marketplace_sponsored_episodes_post","parameters":[{"name":"idempotency-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SponsoredListing"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Marketplace Sponsored Listing V1 Marketplace Sponsored Episodes Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/tip":{"post":{"summary":"Article Tip","description":"D-2.8 · listener tip-jar endpoint.\n\nBody: {listener_token?, amount_usd, note?, tx_hash?}.\nRecords a pending tip + the 70/20/10 ledger split:\n  - 70% to article's publisher\n  - 20% to the recommender (Article.sender_email → ListenerSubscription)\n    — falls into the publisher's share when no recommender is on file\n  - 10% to the platform\n\nStatus flow: pending → settled when tx_hash is confirmed (operator\nor x402 facilitator hits PATCH /v1/admin/tips/{id}/settle). Until\nthen the LedgerEntry rows stay 'pending' too, so payouts can't\nadvance against an unconfirmed tip.","operationId":"article_tip_v1_articles__slug__tip_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"idempotency-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TipRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Tip V1 Articles  Slug  Tip Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/tip/checkout-session":{"post":{"summary":"Article Tip Checkout Session","description":"Fiat tip path via Stripe Checkout (mode=payment).\n\nDEPRECATED 2026-05-16: superseded by the Payment Element rail at\nPOST /v1/articles/{slug}/tip/payment-intent. Handler retained for\n24h grace window to catch any cached frontend bundles still calling\nit; removal in a follow-up commit after another quiet 24h.","operationId":"article_tip_checkout_session_v1_articles__slug__tip_checkout_session_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TipCheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Tip Checkout Session V1 Articles  Slug  Tip Checkout Session Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/tip/payment-intent":{"post":{"summary":"Article Tip Payment Intent","description":"Mint a Stripe PaymentIntent for a fiat tip — Payment Element flow.\n\nCoexists with the Embedded Checkout tip endpoint\n(POST /v1/articles/{slug}/tip/checkout-session). The frontend\nchooses one path per render; both write into the same\n``article_tips`` table so the ledger / 70-20-10 split is identical\nonce settlement fires.","operationId":"article_tip_payment_intent_v1_articles__slug__tip_payment_intent_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TipPaymentIntentBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Tip Payment Intent V1 Articles  Slug  Tip Payment Intent Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/comments":{"get":{"summary":"List Article Comments","operationId":"list_article_comments_v1_articles__slug__comments_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Article Comments V1 Articles  Slug  Comments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{token}/articles/{slug}/comments":{"post":{"summary":"Post Article Comment","operationId":"post_article_comment_v1_listener__token__articles__slug__comments_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"idempotency-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CommentCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Article Comment V1 Listener  Token  Articles  Slug  Comments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{token}/articles/comments/{comment_id}":{"patch":{"summary":"Edit Article Comment","operationId":"edit_article_comment_v1_listener__token__articles_comments__comment_id__patch","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","title":"Comment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CommentEditRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Edit Article Comment V1 Listener  Token  Articles Comments  Comment Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Article Comment","operationId":"delete_article_comment_v1_listener__token__articles_comments__comment_id__delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","title":"Comment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Article Comment V1 Listener  Token  Articles Comments  Comment Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/comments/{comment_id}/hide":{"post":{"summary":"Admin Hide Comment","operationId":"admin_hide_comment_v1_admin_comments__comment_id__hide_post","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","title":"Comment Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Hide Comment V1 Admin Comments  Comment Id  Hide Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/comments/{comment_id}/unhide":{"post":{"summary":"Admin Unhide Comment","operationId":"admin_unhide_comment_v1_admin_comments__comment_id__unhide_post","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","title":"Comment Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Unhide Comment V1 Admin Comments  Comment Id  Unhide Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/pull-external":{"post":{"summary":"Admin Intake Pull External","description":"S4 + S5 · One-shot pull from arXiv + Reddit per vertical.\n\nHits the existing intake → render pipeline so each pulled article\nbecomes a queued render job. Idempotent via slug uniqueness — re-\nrunning picks up only NEW items since the last call.\n\nSource filter: 'arxiv', 'reddit', or 'all' (default).\nVertical filter: when set, only pulls for that one vertical.","operationId":"admin_intake_pull_external_v1_admin_intake_pull_external_post","parameters":[{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"arxiv|reddit|all","title":"Source"},"description":"arxiv|reddit|all"},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Pull External V1 Admin Intake Pull External Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/seed-curated":{"post":{"summary":"Admin Intake Seed Curated","description":"One-shot ingest of the built-in curated RSS seed list across\nevery alpha vertical (tech, AI, finance, policy, science, climate,\nhealth, sports, media, crypto, automotive, gaming, design, security,\ntravel, food, real-estate, longform, foreign-affairs).\n\nRuns the same idempotent path as POST /v1/admin/intake/seed but\npulls the feed list from app/data/rss_seeds.py so the operator\ndoesn't have to maintain a separate JSON. Sources land in\n``status=pending`` so the operator still flips activation.","operationId":"admin_intake_seed_curated_v1_admin_intake_seed_curated_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Seed Curated V1 Admin Intake Seed Curated Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/opml":{"post":{"summary":"Admin Intake Opml","description":"Bulk-import a Feedly / NewsBlur / NetNewsWire OPML export.\nOPML is the standard XML interchange format for RSS subscriptions.\nEach ``<outline xmlUrl=\"...\" title=\"...\">`` becomes a Publisher +\nIntakeSource row. Idempotent on (tenant_slug, feed_url) — re-runs\nreturn existing rows.","operationId":"admin_intake_opml_v1_admin_intake_opml_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_OpmlImportRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Opml V1 Admin Intake Opml Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/opml/import":{"post":{"summary":"Listener Opml Import","description":"Import a user's existing Feedly / NewsBlur / NetNewsWire OPML\nexport into THIS listener's personal feed-subscription scope.\n\nEvery ``<outline type=\"rss\" xmlUrl=\"...\"/>`` becomes a Publisher +\nIntakeSource row namespaced under ``listener-{token12}-{slug}`` so\nimports don't pollute the global discovery pool — sources land in\n``status=pending`` so the operator still gates global activation.\n\nCaps at 500 feeds per request. Rate-limited to 1 import per hour\nper listener (IP-keyed). Returns ``{imported, skipped, errors}``.","operationId":"listener_opml_import_v1_listeners__token__opml_import_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerOpmlImportRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Opml Import V1 Listeners  Token  Opml Import Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/podcasts/audit":{"get":{"summary":"Admin Podcasts Audit","description":"Operator-facing feed-quality audit. Renders every public podcast\nfeed (the flagship + every PODCAST_VERTICAL_META slug) and reports\nwhether the Podcast 2.0 / iTunes tags directories like Podcast Index,\niVoox, and Pocket Casts require are present.\n\nValidates RFC 822 dates on each <item>, the <atom:link rel=\"self\">\nself-reference, <podcast:guid>, <podcast:funding>, and the optional\n<podcast:value> Lightning keysend block.","operationId":"admin_podcasts_audit_v1_admin_podcasts_audit_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Podcasts Audit V1 Admin Podcasts Audit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/impression":{"post":{"summary":"Embed Impression Record","description":"Record a Storyflo embed page-view from a third-party merchant\nsite. The frontend embed route fires this on mount; merchants\ndon't need to instrument anything.\n\nValidates that surface=article carries article_slug and surface=\nfeed carries vertical. Both null = 422. ip_hash + listener_token_\nhash are set when available — both used for the rev-share audit\n(qualified impressions are the ones that produce a listener click-\nthrough to storyflo.com).","operationId":"embed_impression_record_v1_embed_impression_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MerchantEmbedImpressionBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embed Impression Record V1 Embed Impression Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embeds/summary":{"get":{"summary":"Admin Embed Summary","description":"Per-merchant embed impressions for the lookback window. Drives\nthe partnership-conversion narrative for the PayPal BD pitch and\nthe operator-side rev-share calculation.","operationId":"admin_embed_summary_v1_admin_embeds_summary_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embed Summary V1 Admin Embeds Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/listen-notes/discover":{"post":{"summary":"Admin Listen Notes Discover","description":"Discover free podcasts in the given vertical via Listen Notes.\n\nWhen ``auto_register=true``, every returned candidate is also\nregistered as a pending IntakeSource — convenient for bulk seeding\na new vertical. Otherwise the response is read-only and the\noperator picks which to register manually.","operationId":"admin_listen_notes_discover_v1_admin_intake_listen_notes_discover_post","parameters":[{"name":"vertical","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":80,"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}},{"name":"auto_register","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Auto Register"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Listen Notes Discover V1 Admin Intake Listen Notes Discover Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/recent-bids":{"get":{"summary":"Admin Recent Bids","description":"Most recent SSP bid logs — operator triage view. Optional\nssp_name filter narrows to a single SSP.","operationId":"admin_recent_bids_v1_admin_ads_recent_bids_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"ssp_name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ssp Name"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Recent Bids V1 Admin Ads Recent Bids Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/spotify/show":{"get":{"summary":"Spotify Show Lookup","description":"Resolve the Storyflo Spotify show URL via the Web API. Cached\nin-process for 24h. Returns the show metadata + canonical URL so\n/connect can render a 'Listen on Spotify' tile that updates as\nsoon as Spotify's catalog indexes the show.\n\n503 when Spotify creds aren't configured. 404 when the search\ncompletes but Spotify hasn't indexed the show yet — the frontend\nshould hide the Spotify tile and link to the Apple Podcasts\nsubmission flow instead.","operationId":"spotify_show_lookup_v1_spotify_show_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Spotify Show Lookup V1 Spotify Show Get"}}}}}}},"/v1/spotify/oauth/start":{"get":{"summary":"Spotify Oauth Start","description":"Kick off the OAuth flow. Redirects the listener to Spotify's\nauthorize endpoint with a signed state token that binds the\ncallback back to this listener_token.","operationId":"spotify_oauth_start_v1_spotify_oauth_start_get","parameters":[{"name":"listener_token","in":"query","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/spotify/oauth/callback":{"get":{"summary":"Spotify Oauth Callback","description":"Spotify redirects here with ?code=&state=. Exchange the code,\npersist the refresh token, add the show to the listener's library,\nand bounce the user back to /listen on the marketing site.","operationId":"spotify_oauth_callback_v1_spotify_oauth_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/feedback":{"post":{"summary":"Public Feedback","description":"Site-wide feedback intake. Fan-out via operator_alerts\n(Telegram primary, Slack fallback). We do NOT persist this in\nthe DB — the goal is \"land in the operator's eyes within seconds\",\nnot a structured analytics surface. Build a real backing table\nlater when the volume justifies it.","operationId":"public_feedback_v1_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublicFeedbackBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Public Feedback V1 Feedback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/sentry/webhook":{"post":{"summary":"Sentry Webhook","description":"Receive Sentry issue alert payloads and forward to the existing\noperator Telegram channel with a [storyflo] prefix. Filters out\nanything below ERROR severity so the alert chat stays signal-only.\n\nAuth: shared secret in ``?secret=`` query param (Sentry doesn't\nsign its legacy webhook payloads). Returns 200 even on auth-fail\nso Sentry doesn't endlessly retry — we just no-op.","operationId":"sentry_webhook_v1_integrations_sentry_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Sentry Webhook V1 Integrations Sentry Webhook Post"}}}}}}},"/v1/listener/{listener_token}/me":{"get":{"summary":"Listener Data Export","description":"GDPR-style data export. Returns every row keyed on this listener\nin a single JSON document so the listener can take their data\nelsewhere or audit what we hold. The token IS the credential —\nsame trust model as the rest of the listener API.","operationId":"listener_data_export_v1_listener__listener_token__me_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Data Export V1 Listener  Listener Token  Me Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Listener Data Erase","description":"GDPR right-to-erasure. Deletes the listener subscription and\nevery row keyed on the hashed token. Idempotent — repeat calls on\na missing listener return ok=True so retries don't 404.\n\nNOT touched (intentional):\n  - AdImpression rows: these are advertiser-billing source-of-truth.\n    Listener_token_hash is one-way; the row is anonymous after the\n    subscription is gone.\n  - X402Payment / play rows: financial records we keep for audit.\n    Same anonymization story.","operationId":"listener_data_erase_v1_listener__listener_token__me_delete","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Data Erase V1 Listener  Listener Token  Me Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/spotify-disconnect":{"delete":{"summary":"Listener Spotify Disconnect","description":"Clear the listener's stored Spotify credentials. Idempotent —\ncalling on an already-disconnected listener returns ok=True. We\nkeep ``spotify_user_id`` and ``spotify_connected_at`` for audit\ntrail (they don't grant any access on their own); only the\nrefresh_token + show-added flag get wiped.\n\nThe Storyflo show stays in the listener's Spotify library — only\nthe listener can remove it from their end. That matches user\nexpectations (disconnecting our integration shouldn't silently\nunsubscribe them from the show).","operationId":"listener_spotify_disconnect_v1_listener__listener_token__spotify_disconnect_delete","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Spotify Disconnect V1 Listener  Listener Token  Spotify Disconnect Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/spotify-status":{"get":{"summary":"Listener Spotify Status","description":"Frontend pings this from /listen to render the right CTA:\n'Connect Spotify' when not connected, 'Connected ✓' when we have\na refresh token, and 'Show added ✓' once we've successfully\nadded the show to the listener's library.","operationId":"listener_spotify_status_v1_listener__listener_token__spotify_status_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Spotify Status V1 Listener  Listener Token  Spotify Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/byo-tts":{"put":{"tags":["byo-tts"],"summary":"Listener Byo Tts Set","description":"Set or update the listener's BYO TTS key. Verifies against the\ntarget provider before persisting (a typo bounces with 422 instead\nof breaking every future render).\n\nPlus / Pro tier only — free listeners get 402 with an upgrade\nhint. Encryption-at-rest via Fernet using BYO_TTS_ENCRYPTION_KEY;\nwhen the secret is unset, returns 503 (operator hasn't enabled\nthe feature yet).\n\nPool-safety (2026-08-11 SEV) — THREE PHASES\n===========================================\n\n``_byo.verify_key`` is an httpx probe against api.openai.com /\napi.elevenlabs.io with a **10s timeout** (``app/byo_tts.py``). This\nhandler used to carry ``db: AsyncSession = Depends(get_db)``, so the\nwriter connection stayed checked out across that call — the exact\nbug ``listener_byo_render`` (below) was refactored out of on\n2026-08-08, left behind on this sibling handler because the static\naudit counts ``await db.execute`` occurrences and this one has only\nONE (``db.commit()`` is not counted), putting it under the 2+\nthreshold.\n\nSo: read on a short session (phase 1), release it, hit the provider\nholding ZERO connections (phase 2), then re-open a short session to\npersist (phase 3).","operationId":"listener_byo_tts_set_v1_listener__listener_token__byo_tts_put","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ByoTtsSetBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Byo Tts Set V1 Listener  Listener Token  Byo Tts Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["byo-tts"],"summary":"Listener Byo Tts Clear","description":"Clear the stored BYO TTS key. Idempotent — calling on an\nalready-cleared listener returns ok=True. We zero the encrypted\nblob + status; the audit trail (set_at) stays.","operationId":"listener_byo_tts_clear_v1_listener__listener_token__byo_tts_delete","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Byo Tts Clear V1 Listener  Listener Token  Byo Tts Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["byo-tts"],"summary":"Listener Byo Tts Status","description":"Read-only status endpoint. NEVER returns the raw key — only\nmasked metadata so the settings UI can show 'connected to OpenAI\n(…ab12)' without round-tripping the secret.","operationId":"listener_byo_tts_status_v1_listener__listener_token__byo_tts_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Byo Tts Status V1 Listener  Listener Token  Byo Tts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/byo-render":{"post":{"tags":["byo-tts"],"summary":"Listener Byo Render","description":"On-demand audio render using the listener's own provider key.\nReturns the wav stream directly — bytes do NOT transit our cache,\nso each play burns the listener's own provider tokens. The Plus\nlistener gets instant high-quality renders without us paying for\nthe synthesis.\n\nOn 401 from their provider, we mark byo_tts_status =\n'verification_failed' so the frontend can surface a 'reconnect\nyour key' prompt and the next render falls back to the platform\npool via /v1/articles/{slug}/audio.\n\nSession discipline (2026-08-08)\n-------------------------------\nThis handler used to carry ``db: AsyncSession = Depends(get_db)``,\nwhich pins a writer-pool connection for the WHOLE request — including\n``_byo.byo_render()``, an HTTP call to OpenAI / ElevenLabs with a 60s\nhttpx timeout. On the basic plan the writer pool is 5 + 3 overflow =\n8 connections, so eight concurrent Plus listeners hitting this route\nexhaust it and every other endpoint starts queueing. That is exactly\nthe shape behind the six prod SEVs of PRs #657 / #658 / #674 / #677 /\n#678 / #694, and the per-request ``statement_timeout`` guard does not\nhelp: it bounds QUERY time, not the time a session sits open around an\nexternal call.\n\nSo: resolve + validate on a short-lived session, release it in a\n``finally``, make the provider call with NO session held, then take a\nfresh short session for the result write. Response semantics are\nunchanged — every status code, detail body, header and side effect is\npinned in ``tests/test_byo_tts.py``.","operationId":"listener_byo_render_v1_listener__listener_token__byo_render_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"article_slug","in":"query","required":true,"schema":{"type":"string","minLength":1,"title":"Article Slug"}},{"name":"voice_id","in":"query","required":false,"schema":{"type":"string","default":"atlas","title":"Voice Id"}},{"name":"speed","in":"query","required":false,"schema":{"type":"number","maximum":4.0,"minimum":0.25,"default":1.0,"title":"Speed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/substack/seed":{"post":{"tags":["substack-wedge"],"summary":"Seed Substack","description":"Crawl ``domain`` and persist one ``SubstackCandidate`` per\ndiscovered post. Idempotent on ``post_url`` — re-running the\nseed against an already-crawled newsletter only inserts net-new\nposts (the unique index on ``post_url`` swallows duplicates).","operationId":"seed_substack_v1_admin_substack_seed_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/substack/candidates":{"get":{"tags":["substack-wedge"],"summary":"List Candidates","description":"Paginated listing. Newest-first by ``discovered_at``.","operationId":"list_candidates_v1_admin_substack_candidates_get","parameters":[{"name":"publisher_domain","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Publisher Domain"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CandidateView"},"title":"Response List Candidates V1 Admin Substack Candidates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/substack/candidates/{candidate_id}/approve":{"post":{"tags":["substack-wedge"],"summary":"Approve Candidate","description":"Flip the row to ``approved``. Render pipeline pick-up is a\nfollow-up — blocked on PR #124 (bundler) + PR #125 (worker).","operationId":"approve_candidate_v1_admin_substack_candidates__candidate_id__approve_post","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/substack/candidates/{candidate_id}/reject":{"post":{"tags":["substack-wedge"],"summary":"Reject Candidate","description":"Flip the row to ``rejected``. Soft delete — keeps the URL\nfingerprint so re-crawls don't re-emit the same junk.","operationId":"reject_candidate_v1_admin_substack_candidates__candidate_id__reject_post","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bedtime/import/gutenberg/{book_id}":{"post":{"tags":["bedtime-stories"],"summary":"Import Gutenberg","description":"Fetch + classify + UPSERT a Project Gutenberg book.\n\nIdempotent on ``(source, source_id)`` — re-running this endpoint for\nthe same PG book updates the existing row's ``body_text`` and tags,\nand bumps ``updated_at``. It does *not* clear ``audio_url`` /\n``audio_rendered_at`` — those are render-pipeline state.","operationId":"import_gutenberg_v1_admin_bedtime_import_gutenberg__book_id__post","parameters":[{"name":"book_id","in":"path","required":true,"schema":{"type":"integer","title":"Book Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ImportGutenbergRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BedtimeStoryView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bedtime/stories":{"get":{"tags":["bedtime-stories"],"summary":"List Stories","description":"Newest-first listing with age-band and tag filters.","operationId":"list_stories_v1_admin_bedtime_stories_get","parameters":[{"name":"age_min","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":99,"minimum":0},{"type":"null"}],"title":"Age Min"}},{"name":"age_max","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":99,"minimum":0},{"type":"null"}],"title":"Age Max"}},{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Tag"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"include_deleted","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Deleted"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BedtimeStoryView"},"title":"Response List Stories V1 Admin Bedtime Stories Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bedtime/stories/{story_id}/render":{"post":{"tags":["bedtime-stories"],"summary":"Enqueue Render","description":"Mark the story for TTS rendering.\n\nScaffold note: the live render-queue worker integration is a\nfollow-up. For now this endpoint validates the story exists, logs\nan ``enqueued`` event, and returns ``enqueued=True``. The follow-up\nPR will wire the actual queue ``app/render_queue_*.py`` and write\n``audio_url`` / ``audio_rendered_at`` on completion.","operationId":"enqueue_render_v1_admin_bedtime_stories__story_id__render_post","parameters":[{"name":"story_id","in":"path","required":true,"schema":{"type":"string","title":"Story Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderEnqueueResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bedtime/stories/{story_id}/voice":{"post":{"tags":["bedtime-stories"],"summary":"Override Voice","description":"Override the rendered voice for a single bedtime story.\n\nThe default voice is set at import time by\n``app/bedtime/voice_presets.py``. This endpoint lets an operator\npick a different Kokoro/Piper preset — useful when a story doesn't\nfit the age-based default (e.g. a deeper register for an adventure\ntale, or matching a publisher request).","operationId":"override_voice_v1_admin_bedtime_stories__story_id__voice_post","parameters":[{"name":"story_id","in":"path","required":true,"schema":{"type":"string","title":"Story Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceOverrideRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BedtimeStoryView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/bedtime/stories/{story_id}/soft-delete":{"post":{"tags":["bedtime-stories"],"summary":"Soft Delete Story","description":"Flip ``deleted_at`` — does not drop the row.\n\nListings default to ``include_deleted=False``; an operator can still\nretrieve and (in a follow-up) un-delete by clearing the timestamp.","operationId":"soft_delete_story_v1_admin_bedtime_stories__story_id__soft_delete_post","parameters":[{"name":"story_id","in":"path","required":true,"schema":{"type":"string","title":"Story Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BedtimeStoryView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/lang/articles/{article_id}/set-language":{"post":{"tags":["lang"],"summary":"Set Language","operationId":"set_language_v1_admin_lang_articles__article_id__set_language_post","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetLanguageBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Language V1 Admin Lang Articles  Article Id  Set Language Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/lang/articles/by-language/{lang}":{"get":{"tags":["lang"],"summary":"By Language","operationId":"by_language_v1_admin_lang_articles_by_language__lang__get","parameters":[{"name":"lang","in":"path","required":true,"schema":{"type":"string","title":"Lang"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleRow"},"title":"Response By Language V1 Admin Lang Articles By Language  Lang  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/lang/detect-test":{"post":{"tags":["lang"],"summary":"Detect Test","operationId":"detect_test_v1_admin_lang_detect_test_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__lang__router__DetectTestBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetectTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/dead-letter":{"get":{"tags":["admin-render"],"summary":"Admin Render Dead Letter","description":"List jobs in the dead-letter queue, newest-DLQ'd first.","operationId":"admin_render_dead_letter_v1_admin_render_dead_letter_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DLQListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/retry-stats":{"get":{"tags":["admin-render"],"summary":"Admin Render Retry Stats","description":"Aggregate failure-reason counts + current DLQ size.","operationId":"admin_render_retry_stats_v1_admin_render_retry_stats_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RetryStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/{job_id}/requeue":{"post":{"tags":["admin-render"],"summary":"Admin Render Requeue","description":"Manually re-queue a single (typically DLQ'd) render job.","operationId":"admin_render_requeue_v1_admin_render__job_id__requeue_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RequeueResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/dead-letter/sweep":{"post":{"tags":["admin-render"],"summary":"Admin Render Dlq Sweep","description":"Bulk requeue every job in the DLQ (up to ``limit``).","operationId":"admin_render_dlq_sweep_v1_admin_render_dead_letter_sweep_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":500,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SweepResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pipeline/status":{"get":{"tags":["pipeline-status"],"summary":"Pipeline Status","description":"Full pipeline health dashboard. Five sections + alerts +\ndeploys metadata. See :class:`PipelineStatus` for the shape.","operationId":"pipeline_status_v1_admin_pipeline_status_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"1h","title":"Since"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pipeline Status V1 Admin Pipeline Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/alerts/recent":{"get":{"tags":["pipeline-status"],"summary":"Alerts Recent","description":"Paginated alert-history list. Newest first.","operationId":"alerts_recent_v1_admin_alerts_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Severity"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"only_unacknowledged","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Only Unacknowledged"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/alerts/{alert_id}/acknowledge":{"post":{"tags":["pipeline-status"],"summary":"Acknowledge Alert","description":"Mark a single alert acknowledged. Idempotent — re-acking a\npreviously-ack'd row is a no-op (the original timestamp wins).","operationId":"acknowledge_alert_v1_admin_alerts__alert_id__acknowledge_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/dashboard/snapshot":{"get":{"tags":["admin-dashboard"],"summary":"Admin Dashboard Snapshot","description":"Aggregated real-time operator dashboard.\n\nSee :func:`build_snapshot` for the response shape. Polls comfortably at\n30s — each section is a single COUNT() or small SELECT.","operationId":"admin_dashboard_snapshot_v1_admin_dashboard_snapshot_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Dashboard Snapshot V1 Admin Dashboard Snapshot Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/dashboard/share-token":{"post":{"tags":["admin-dashboard"],"summary":"Admin Dashboard Share Token","description":"Mint a short-lived (default 24h) read-only token.\n\nIdempotent in spirit but not in implementation — each call mints a\nfresh token. Operator usage is one mint per share, so the storage cost\nis trivial (a row per share-event in ``kv_store``).","operationId":"admin_dashboard_share_token_v1_admin_dashboard_share_token_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/dashboard/snapshot/shared/{share_token}":{"get":{"tags":["admin-dashboard"],"summary":"Admin Dashboard Snapshot Shared","description":"Public-by-token read of the dashboard snapshot.\n\nNo admin header — the token IS the auth. Looks the token up in\n``kv_store``, verifies ``expires_at > now``, and serves the same\npayload the admin route serves (passed through :func:`_redact_for_shared`).","operationId":"admin_dashboard_snapshot_shared_v1_admin_dashboard_snapshot_shared__share_token__get","parameters":[{"name":"share_token","in":"path","required":true,"schema":{"type":"string","title":"Share Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Dashboard Snapshot Shared V1 Admin Dashboard Snapshot Shared  Share Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/health/snapshot":{"get":{"tags":["admin-health"],"summary":"Admin Health Snapshot","description":"Aggregated platform health for the operator screen-share.\n\nREAD-ONLY. Polls at 60s — heaviest section is the 3 GH-Actions calls;\nthe rest are sub-200ms in-process aggregate reads.","operationId":"admin_health_snapshot_v1_admin_health_snapshot_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Health Snapshot V1 Admin Health Snapshot Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/telegram/webhook":{"post":{"tags":["telegram-webhook"],"summary":"Telegram Webhook","description":"Telegram update ingest.\n\nReturns 200 on every accepted update — Telegram retries non-200s\nand we never want to be a retry source for non-reply spam. The\nonly non-200 we return is 403 on a bad / missing secret token.\n\nWe deliberately accept-and-ignore many shapes (channel posts,\ninline queries, edited messages, ...) — we ONLY act on\n``update.message.reply_to_message.message_id`` matches against\na pending decision row.","operationId":"telegram_webhook_v1_telegram_webhook_post","parameters":[{"name":"x-telegram-bot-api-secret-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Telegram-Bot-Api-Secret-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Telegram Webhook V1 Telegram Webhook Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/telegram/register-webhook":{"post":{"tags":["telegram-webhook"],"summary":"Register Telegram Webhook","description":"Idempotent one-shot setWebhook against Telegram.\n\nFires ``POST https://api.telegram.org/bot{TOKEN}/setWebhook`` with\nthe configured public URL + the secret-token. Operator hits this\nonce after rotating the secret (or on first install). Don't put\nthis in a startup hook — TG rate-limits webhook re-registration\nand we restart machines frequently.\n\nAuth: standard ``X-Storyflo-Email-Token`` admin gate.","operationId":"register_telegram_webhook_v1_admin_telegram_register_webhook_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RegisterWebhookRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterWebhookResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/integrity-check/{article_id}":{"get":{"tags":["admin-render-integrity"],"summary":"Integrity Check","operationId":"integrity_check_v1_admin_render_integrity_check__article_id__get","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}},{"name":"full_verify","in":"query","required":false,"schema":{"type":"boolean","description":"When True (default for this endpoint), re-download the audio and recompute SHA256. The hot path uses False; this manual endpoint defaults True since the operator is explicitly debugging.","default":true,"title":"Full Verify"},"description":"When True (default for this endpoint), re-download the audio and recompute SHA256. The hot path uses False; this manual endpoint defaults True since the operator is explicitly debugging."},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VerdictResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render/audit-r2-integrity":{"post":{"tags":["admin-render-integrity"],"summary":"Audit R2 Integrity","description":"Batch-verify the N most-recent articles' uploads.\n\nDesigned for a cron loop: cheap-mode (HEAD only) scans every\nnight for partial uploads, full-verify-mode runs weekly on a\nsmaller sample to catch silent corruption.","operationId":"audit_r2_integrity_v1_admin_render_audit_r2_integrity_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"full_verify","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Full Verify"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_BatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/marketing/generate-teaser":{"post":{"tags":["marketing-teaser"],"summary":"Generate Teaser Endpoint","description":"Render a teaser .mp4, upload to R2, return the public URL.\n\nReturns 503 if ffmpeg isn't installed or the Kokoro model isn't\nloaded — these are deployment-environment problems, not bad\nrequests, so the operator sees the right signal.","operationId":"generate_teaser_endpoint_v1_admin_marketing_generate_teaser_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateTeaserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateTeaserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pii/scan":{"post":{"tags":["pii"],"summary":"Scan Text","operationId":"scan_text_v1_admin_pii_scan_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pii/audit/run-now":{"post":{"tags":["pii"],"summary":"Audit Run Now","operationId":"audit_run_now_v1_admin_pii_audit_run_now_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunNowBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Audit Run Now V1 Admin Pii Audit Run Now Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pii/audit/last-run":{"get":{"tags":["pii"],"summary":"Audit Last Run","operationId":"audit_last_run_v1_admin_pii_audit_last_run_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Audit Last Run V1 Admin Pii Audit Last Run Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pii/patterns":{"get":{"tags":["pii"],"summary":"List Patterns","description":"List the catalog: category name → per-pattern summary. Returns\npattern names + severities (NOT the regex strings, to keep the\npublic response small), plus whether the pattern carries a\nstructural validator (see ``pii/validators.py``).","operationId":"list_patterns_v1_admin_pii_patterns_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Patterns V1 Admin Pii Patterns Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/marketing/publish/x":{"post":{"tags":["marketing-publish"],"summary":"Publish X","description":"Post the video to X and return the tweet URL.","operationId":"publish_x_v1_admin_marketing_publish_x_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishXBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/marketing/publish/youtube":{"post":{"tags":["marketing-publish"],"summary":"Publish Youtube","description":"Upload the video to YouTube and return the watch URL.","operationId":"publish_youtube_v1_admin_marketing_publish_youtube_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishYouTubeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/marketing/publish/both":{"post":{"tags":["marketing-publish"],"summary":"Publish Both","description":"Fan out to X + YouTube. Atomic-ish: one failure does not\nshort-circuit the other. The response surfaces each side's\noutcome independently so the operator can replay just the\nfailed half.","operationId":"publish_both_v1_admin_marketing_publish_both_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishBothBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/programmatic-revenue/{ssp_provider}":{"post":{"tags":["programmatic-revenue"],"summary":"Programmatic Revenue Webhook","description":"Ingest one programmatic-revenue event from a Storyflo-onboarded SSP.\n\nSee module docstring for the failure-mode contract.","operationId":"programmatic_revenue_webhook_v1_programmatic_revenue__ssp_provider__post","parameters":[{"name":"ssp_provider","in":"path","required":true,"schema":{"type":"string","description":"Lowercase SSP slug: triton | adswizz | magnite | pubmatic.","title":"Ssp Provider"},"description":"Lowercase SSP slug: triton | adswizz | magnite | pubmatic."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Programmatic Revenue Webhook V1 Programmatic Revenue  Ssp Provider  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/me/provision-wallet":{"post":{"tags":["embedder-wallets"],"summary":"Provision Wallet","description":"Provision the calling embedder's Circle custody wallet on the\ncurrently-selected ``CIRCLE_NETWORK`` (default ``BASE`` mainnet).\nIdempotent: returns the existing wallet on repeat calls.\n\nResponse shape::\n\n    {\"wallet_id\": \"...\",\n     \"status\": \"pending\" | \"provisioned\",\n     \"network\": \"BASE\" | \"BASE-SEPOLIA\",\n     \"address\": null | \"0x...\"}\n\n503 with a precise operator-action body when env is mis-configured\n(missing creds, unsupported ``CIRCLE_NETWORK`` value, or\n``CIRCLE_USDC_TOKEN_ID`` still set to the ``PENDING_LOOKUP``\nsentinel).","operationId":"provision_wallet_v1_embedders_me_provision_wallet_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Provision Wallet V1 Embedders Me Provision Wallet Post"}}}}}}},"/webhooks/circle/wallet-created":{"post":{"tags":["embedder-wallets"],"summary":"Circle Wallet Created Webhook","description":"Public Circle ``wallets.created`` webhook.\n\nSignature verification uses the RSA-PKCS1v15-SHA256 helper in\n``app.circle_mint`` (matches Circle's documented webhook security\nscheme — they sign with their private key, we verify with their\npublished public key). The task spec said \"HMAC\" but Circle's real\nwebhook model is RSA; we use the established verify helper rather\nthan implementing a parallel HMAC path that would break the first\nreal Circle ping.\n\nHeaders expected\n----------------\n``X-Circle-Signature``  — base64 RSA-PKCS1v15-SHA256 over the raw body\n``X-Circle-Key-Id``     — Circle public key identifier (used to fetch\n                           the PEM from /v2/notifications/publicKey)\n\nOn signature mismatch returns 401. On unparseable body returns 400.\nOn any successful match dispatches to\n``circle_embedder_wallets.handle_provision_webhook`` for the actual\nDB write.","operationId":"circle_wallet_created_webhook_webhooks_circle_wallet_created_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Circle Wallet Created Webhook Webhooks Circle Wallet Created Post"}}}}}}},"/v1/embedders/me/withdraw":{"post":{"tags":["embedder-claim"],"summary":"Claim Withdraw","description":"Initiate an embedder USDC claim from custody → personal address.\n\nReturns ``{withdrawal_id, status, circle_transfer_id, network}``\non success. ``status`` is ``processing`` once Circle has accepted\nthe transfer; the eventual on-chain settlement (and ``base_tx_hash``)\narrives via Circle's ``transfers.confirmed`` webhook — out of scope\nfor this PR (deferred per design).\n\nSession discipline (2026-08-08)\n-------------------------------\nThis handler used to carry ``db: AsyncSession = Depends(get_db)``.\nThat pins a writer-pool connection for the whole request, and\n``_circle_create_transfer`` below is an httpx POST to Circle with a\n20s timeout — so a slow or hanging Circle held one of the pool's 8\nwriter connections for up to 20s per in-flight claim. Same shape as\nthe six pool-exhaustion SEVs (PRs #657 / #658 / #674 / #677 / #678 /\n#694); the per-request ``statement_timeout`` does not bound it,\nbecause the time is spent outside any query.\n\nThe split is a natural fit here because the money ordering already\ndemanded it: the withdrawal row is COMMITTED before the Circle call\n(so a Circle timeout can't lose the audit trail) and both post-call\nbranches re-SELECT the row by id. So phase 1 ends exactly where the\ncommit already was, and phases 3a/3b are the re-SELECTs that already\nexisted — now on a fresh session. No lock is held across the external\ncall in either the old or the new shape (the ``FOR UPDATE`` on the\nbalance row is released by the phase-1 commit), and the ordering of\nevery gate, log line, status code and response field is unchanged.","operationId":"claim_withdraw_v1_embedders_me_withdraw_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Claim Withdraw V1 Embedders Me Withdraw Post"}}}}}}},"/v1/creator/claim/{claim_token}":{"get":{"tags":["creator-claim"],"summary":"Creator Claim Lookup","description":"Resolve a claim token to the claim-page payload — publisher-\nlookup response parity (see module docstring) plus the original\n``creator`` block. Claimed + provisioned tokens 409 with the\n``already_claimed`` rescue shape.","operationId":"creator_claim_lookup_v1_creator_claim__claim_token__get","parameters":[{"name":"claim_token","in":"path","required":true,"schema":{"type":"string","title":"Claim Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Creator Claim Lookup V1 Creator Claim  Claim Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["creator-claim"],"summary":"Creator Claim Accept","description":"Accept the claim and provision the managed service. Idempotent —\nre-posting an already-claimed token returns the SAME publisher +\nlinks (no duplicate rows, no re-fired one-shot emails). A\nprovisioning failure after the ``claimed_at`` stamp still returns a\nusable 200 (loudly logged) — never the pre-2026-07-16 dead-end.","operationId":"creator_claim_accept_v1_creator_claim__claim_token__post","parameters":[{"name":"claim_token","in":"path","required":true,"schema":{"type":"string","title":"Claim Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CreatorClaimAcceptRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Creator Claim Accept V1 Creator Claim  Claim Token  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/acquisition/funnel":{"get":{"tags":["acquisition-admin"],"summary":"Acquisition Funnel","description":"Windowed acquisition-funnel rollup. Each stage counts rows whose\nOWN lifecycle timestamp falls inside the last ``days`` days —\nstages are therefore independent tallies, not a strict subset chain\n(a creator discovered 40 days ago who claimed yesterday counts in\n``claimed`` but not ``discovered`` for days=30).\n\n``funnel.repair_sent`` is windowed on the ledger's ``sent_at``\nstamps like every other stage (2026-07-16 revenue-accuracy fix —\nit previously rendered the ALL-TIME ledger size beside windowed\nstages). The all-time ledger size now lives at\n``all_time.repair_sent`` (additive to the frozen shape).","operationId":"acquisition_funnel_v1_admin_acquisition_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acquisition Funnel V1 Admin Acquisition Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/access/request":{"post":{"tags":["publisher-access"],"summary":"Publisher Access Request","description":"Email-first recovery + login. Public (the whole point is the\ncaller has NO working credential). Always answers the same 202 —\nsee module docstring for the identity-state matrix.","operationId":"publisher_access_request_v1_publisher_access_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AccessRequestBody"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Publisher Access Request V1 Publisher Access Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/shows":{"get":{"tags":["publisher-shows"],"summary":"Publisher List Shows","description":"Every show on this account — the tenant you signed in as plus any\nadded through POST /shows. One row per (publisher, source), so a tenant\ncarrying two feeds reads as two rows instead of being folded into one.","operationId":"publisher_list_shows_v1_publisher__tenant_slug__shows_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher List Shows V1 Publisher  Tenant Slug  Shows Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["publisher-shows"],"summary":"Publisher Add Show","description":"Add a second (third, …) show to an ALREADY-AUTHENTICATED account.\n\nDifferences from the public onboard endpoint, all deliberate:\n\n  * the caller proves ownership of an existing tenant, so there is no\n    email round-trip, no authorization re-prompt and no 24h review — the\n    account is already opted in and its grant is inherited;\n  * the show is named from the feed, never from the URL's hostname;\n  * a slug collision is RESOLVED (``name-2``), not 409'd: the public\n    endpoint's 409 protects a slug's original owner from a stranger, and\n    inside an authenticated request there is no stranger;\n  * a feed already ingested by ANOTHER account is refused with the\n    holder named, because the common cause is the author forgetting.","operationId":"publisher_add_show_v1_publisher__tenant_slug__shows_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/show-name":{"patch":{"tags":["publisher-shows"],"summary":"Publisher Patch Show Name","description":"Rename a show you own.\n\nWrites BOTH the tenant name (what the creator feed and every listener\nsurface render) and the per-source ``show_name`` (the durable record of\nwhat this specific feed is called), so a later re-onboard of the same\nfeed can't quietly revert the correction.","operationId":"publisher_patch_show_name_v1_publisher__tenant_slug__show_name_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowNamePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Patch Show Name V1 Publisher  Tenant Slug  Show Name Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/name":{"patch":{"tags":["publisher-shows"],"summary":"Admin Patch Publisher Name","description":"Operator rename for a publisher / show.\n\nExists because until 2026-08-08 there was NO way to correct a\npublisher's name — not for the operator, not for the publisher. A row\nseeded from a podcast directory under one show's title but pointed at a\nDIFFERENT show's feed (exactly what a live publisher reported) was\nuncorrectable, and the wrong name flowed into the creator podcast feed,\nevery listener surface, and outbound email.","operationId":"admin_patch_publisher_name_v1_admin_publishers__tenant_slug__name_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherNamePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Publisher Name V1 Admin Publishers  Tenant Slug  Name Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/sources/{source_id}":{"patch":{"tags":["publisher-shows"],"summary":"Admin Patch Intake Source","description":"Operator repair for a single IntakeSource.\n\n``default_vertical`` exists because prod runs\n``INTAKE_PRIORITY_VERTICALS_ONLY=1``: a source whose vertical is NULL or\nnot a ``PODCAST_VERTICAL_META`` key has EVERY article silently skipped\n(``app.intake.worker._passes_priority_vertical``). Such a row is\nindistinguishable from a healthy one — active, polled on schedule,\n``last_error`` NULL — while producing nothing, for as long as nobody\nnotices. Onboard has derived a valid vertical since 2026-07-16 and\n``repair_active_source_verticals`` can sweep the backlog, but there was\nno way to fix ONE named source without a shell inside the VPC. That gap\nis why a live publisher's feed sat active and empty for 23 days.\n\n``show_name`` is the per-source title — the same field the\npublisher-facing rename writes.","operationId":"admin_patch_intake_source_v1_admin_intake_sources__source_id__patch","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSourcePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Intake Source V1 Admin Intake Sources  Source Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embedders/me/kyc/submit":{"post":{"tags":["embedder-kyc"],"summary":"Submit Kyc","description":"Capture LIGHT-tier KYC fields and bump ``kyc_level`` to ``light``.\n\nIdempotent — a second submit overwrites the on-file fields and\nrefreshes ``kyc_verified_at``. We do NOT downgrade ``kyc_level`` if\nthe embedder is already at ``full`` (ratchet only goes up); the\nLIGHT fields are still overwritten so the embedder can correct\ntypos at any tier.\n\nReturns the new on-file state so the FE can re-render without a\nsecond GET.","operationId":"submit_kyc_v1_embedders_me_kyc_submit_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Submit Kyc V1 Embedders Me Kyc Submit Post"}}}}}}},"/v1/admin/declassified/manifest":{"get":{"tags":["declassified-admin"],"summary":"Get Manifest","description":"Return the persisted manifest + spend posture.\n\nResponse shape::\n\n    {\n        \"ok\": true,\n        \"feature_flag_on\": bool,\n        \"monthly_ceiling_usd_micros\": int,\n        \"month_to_date_usd_micros\": int,\n        \"row_count\": int,\n        \"rows\": [{slug, domain, doc_id, source_url, title, status,\n                  estimated_cost_usd_micros}, ...],\n    }\n\nEmpty ``rows`` is fine — the operator may call this before any\nfetcher tick has run; the response still surfaces the FF state +\nceiling so they can plan.","operationId":"get_manifest_v1_admin_declassified_manifest_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Manifest V1 Admin Declassified Manifest Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declassified/trigger-render":{"post":{"tags":["declassified-admin"],"summary":"Trigger Render","description":"Run one scheduler tick. Returns the tick summary verbatim.\n\nWhen the FF is OFF the underlying ``render_scheduler.tick`` no-ops\n+ returns ``{skipped_ff_off: true, ...}`` — surfaced here so the\noperator can confirm wiring is alive before flipping spend on.","operationId":"trigger_render_v1_admin_declassified_trigger_render_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Trigger Render V1 Admin Declassified Trigger Render Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declassified/ingest":{"post":{"tags":["declassified-admin"],"summary":"Ingest","description":"Bulk-ingest declassified cases from a manifest URL, as a background job.\n\nRuns the same pipeline that created the existing cases (download PDF →\nGroq narration → ElevenLabs audio → R2 → INSERT persona_briefings), inside\nthe app. Returns a ``job_id`` immediately; poll ``/ingest/status/{job_id}``.\nPublished rows appear on every ``/v1/declassified/*`` surface automatically.\n~$1/case ElevenLabs — the caller owns the spend decision via ``limit``.","operationId":"ingest_v1_admin_declassified_ingest_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ingest V1 Admin Declassified Ingest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declassified/ingest/status/{job_id}":{"get":{"tags":["declassified-admin"],"summary":"Ingest Status","description":"Poll a bulk-ingest job's progress.","operationId":"ingest_status_v1_admin_declassified_ingest_status__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ingest Status V1 Admin Declassified Ingest Status  Job Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/declassified/backfill-collections":{"post":{"tags":["declassified-admin"],"summary":"Backfill Collections Route","description":"Classify the whole declassified archive into Vault collections and\npersist the slug to ``declassified_category`` (powers /v1/declassified/\ncollections). Idempotent — only rewrites rows whose collection changed.\n``only_uncatalogued=true`` leaves any hand-curated codes intact.","operationId":"backfill_collections_route_v1_admin_declassified_backfill_collections_post","parameters":[{"name":"only_uncatalogued","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Only Uncatalogued"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Backfill Collections Route V1 Admin Declassified Backfill Collections Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/declassified/episodes":{"get":{"tags":["declassified-public"],"summary":"List Declassified Episodes","description":"Public list of declassified episodes for the storyflo.com\n``/declassified`` archive page.\n\nPulls from ``persona_briefings WHERE briefing_kind = 'declassified'\nAND status = 'published'``. The same gate the public-RSS feed uses\n(see ``_STANDALONE_BRIEFING_KINDS``), so this surface can never\nleak draft / unpublished editorial copy.\n\nOptional ``category`` / ``agency`` / ``era`` query params narrow the\nresult set to rows whose taxonomy column matches case-insensitively.\nThe composite index ``ix_persona_briefings_kind_category`` (migration\n0159) handles the hot category-filter path; the other two are\nsequential scans on the already-tiny declassified subset.","operationId":"list_declassified_episodes_v1_declassified_episodes_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","pattern":"^(case_number_asc|case_number_desc|date_asc|date_desc)$","default":"date_desc","title":"Sort"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40,"pattern":"^[A-Za-z0-9 _-]+$"},{"type":"null"}],"description":"Filter by declassified_category (UAP / JFK / COINTELPRO / mind-control / drone-sightings / mob-surveillance / …).","title":"Category"},"description":"Filter by declassified_category (UAP / JFK / COINTELPRO / mind-control / drone-sightings / mob-surveillance / …)."},{"name":"agency","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":20,"pattern":"^[A-Za-z0-9 _-]+$"},{"type":"null"}],"description":"Filter by declassified_agency (CIA / FBI / DoD / NRO / NSA / DOJ / NARA / …).","title":"Agency"},"description":"Filter by declassified_agency (CIA / FBI / DoD / NRO / NSA / DOJ / NARA / …)."},{"name":"era","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":20,"pattern":"^[A-Za-z0-9 _-]+$"},{"type":"null"}],"description":"Filter by declassified_era (cold-war / vietnam / civil-rights / 1990s / 2000s / 2020s / …).","title":"Era"},"description":"Filter by declassified_era (cold-war / vietnam / civil-rights / 1990s / 2000s / 2020s / …)."},{"name":"exclude_slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Exclude a specific slug — used by the per-case page to surface 'Related cases' without the case itself.","title":"Exclude Slug"},"description":"Exclude a specific slug — used by the per-case page to surface 'Related cases' without the case itself."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Declassified Episodes V1 Declassified Episodes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/declassified/facets":{"get":{"tags":["declassified-public"],"summary":"List Declassified Facets","description":"Distinct taxonomy values across the published declassified archive.\n\nPowers the filter chip strip + agency dropdown + era dropdown on\n``/declassified``. Each facet is returned as ``(value, count)`` so\nthe FE can render a hit-count badge next to each chip and order the\nchips by archive coverage. Excludes NULL — uncategorised rows\ncontribute nothing to the facets.","operationId":"list_declassified_facets_v1_declassified_facets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Declassified Facets V1 Declassified Facets Get"}}}}}}},"/v1/declassified/collections":{"get":{"tags":["declassified-public"],"summary":"List Declassified Collections","description":"The Vault browse surface: every published case grouped into a curated,\nevocative collection (Mind Control Programs, UAP & Aerial Phenomena, War\nDepartment Records, …). Each collection carries an ``answer_capsule`` —\nthe BLUF text an AI engine can cite verbatim (the AEO surface competitors\ncan't match: narrated primary-source government audio, by theme).\n\nCases are pre-classified into ``declassified_category`` at ingest time\n(+ a backfill), so this is a plain GROUP BY that scales to thousands.\nReturns collections in curated browse order; empty collections are hidden.","operationId":"list_declassified_collections_v1_declassified_collections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Declassified Collections V1 Declassified Collections Get"}}}}}}},"/v1/declassified/volumes":{"get":{"tags":["declassified-public"],"summary":"List Declassified Volumes","description":"List Declassified *Volumes* (ordered archive.org collections).\n\nGated by ``DECLASSIFIED_VOLUMES_ENABLED`` — when OFF this returns an\nempty list (the schema + expansion pipeline ship dark). Each volume\ncarries its status, released/planned episode counts, and the\n``next_release_at`` of its soonest upcoming episode.","operationId":"list_declassified_volumes_v1_declassified_volumes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Declassified Volumes V1 Declassified Volumes Get"}}}}}}},"/v1/declassified/volumes/{slug}":{"get":{"tags":["declassified-public"],"summary":"Get Declassified Volume","description":"A single Volume + its ordered RELEASED episodes and a\n``coming_soon`` list of scheduled-but-unreleased episodes.\n\nGated by ``DECLASSIFIED_VOLUMES_ENABLED`` — 404 when OFF (parity with\n\"no such volume\"). Released episodes carry the full playable card;\ncoming-soon episodes expose only ``episode_number`` / ``title`` /\n``release_at`` (NO audio).","operationId":"get_declassified_volume_v1_declassified_volumes__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Declassified Volume V1 Declassified Volumes  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/declassified/case/{slug}":{"get":{"tags":["declassified-public"],"summary":"Get Declassified Case","description":"Full case record for a published declassified episode.\n\nPublic read — same gate as ``/episodes``. Returns the rich shape\nthe MCP ``get_declassified_case`` tool surfaces (summary +\ntranscript_excerpt + agency + category + era + related_cases).\n404 when no published row matches the slug.","operationId":"get_declassified_case_v1_declassified_case__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Declassified Case V1 Declassified Case  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/affiliate/kraken/link":{"get":{"tags":["kraken-affiliate"],"summary":"Kraken Affiliate Link","description":"Resolve the Dub short link for the Kraken affiliate.\n\nThe FE chip POSTs the embedder slug + detected asset; we mint the\nDub URL with the attribution token attached and return it for the\nchip to redirect through. The ``click_id`` is an opaque per-render\ntoken for our own observability — Dub's webhook still uses\n``?ref={slug}`` as the conversion key, so the click_id is\ninformational only.","operationId":"kraken_affiliate_link_v1_affiliate_kraken_link_get","parameters":[{"name":"embedder","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"description":"Embedder tenant slug. Round-trips through Dub's subId / ?ref attribution into Kraken's affiliate dashboard. Falls back to the canonical house slug when omitted.","title":"Embedder"},"description":"Embedder tenant slug. Round-trips through Dub's subId / ?ref attribution into Kraken's affiliate dashboard. Falls back to the canonical house slug when omitted."},{"name":"asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"description":"Optional asset hint (e.g. 'BTC') from the crypto-tag detector. Passed through as ``&asset={asset}`` for downstream observability.","title":"Asset"},"description":"Optional asset hint (e.g. 'BTC') from the crypto-tag detector. Passed through as ``&asset={asset}`` for downstream observability."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Kraken Affiliate Link V1 Affiliate Kraken Link Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/affiliate/kraken/crypto-relevance":{"get":{"tags":["kraken-affiliate"],"summary":"Kraken Crypto Relevance","description":"Internal / admin helper for QA + Mason persona prompt construction.\n\nMirrors :func:`app.intake.crypto_tag_detector.is_crypto_relevant`\n— returns ``{is_crypto, matched_assets}``. Cached for 5 minutes\nso repeated probes on the same article body don't hit the\ndetector hot-path multiple times.\n\nFF-gated. Default-OFF on prod, ON on dev (operator flips\n``KRAKEN_AFFILIATE_ENABLED=1`` after the Dub short-link is in the\nFly secret store).","operationId":"kraken_crypto_relevance_v1_affiliate_kraken_crypto_relevance_get","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","maxLength":20000,"description":"Article title / summary / body to classify. URL-encoded by the caller.","title":"Text"},"description":"Article title / summary / body to classify. URL-encoded by the caller."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Kraken Crypto Relevance V1 Affiliate Kraken Crypto Relevance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/affiliate/kraken/click":{"post":{"tags":["kraken-affiliate"],"summary":"Kraken Click Beacon","description":"Fire-and-forget click beacon from the FE Kraken chip.\n\nAccepts ``{click_id?, embedder_slug?, asset?}``, emits a single\nstructured ``kraken.click`` log line, and persists a zero-amount\n``click`` :class:`EmbedderRevenueEvent` attributed to the embedder\n(so the affiliate roll-up can build a click→conversion funnel\nwithout a second table — same mechanics as the Polymarket click\nbeacon). Rows are kraken-tagged via a ``kraken:``-prefixed\n``polymarket_subid`` + ``broker`` payload field so roll-ups can\ndistinguish partners.\n\nBest-effort discipline (matches the Polymarket beacon):\n  * Unknown / omitted embedder slug → fall back to the house\n    embedder if one exists, else log-only (no DB row) and still\n    200. We never 500 a beacon.\n  * Replay within the same UTC day → IntegrityError on the\n    ``dub_event_id`` UNIQUE constraint → rollback + NOOP, still 200.\n  * Any other persist failure → rollback + log, still 200 — a DB\n    failure must never break the listener's outbound to Kraken.\n\nIntentionally NOT FF-gated. ``KRAKEN_AFFILIATE_ENABLED`` controls\nwhether we mint a Dub short link on :func:`kraken_affiliate_link`;\nif the operator temporarily flips it off, we still want to record\nthe click attempts the chip is sending so we don't lose tracking on\na transient disable.\n\nCompliance posture matches the rest of this router: linkout-only\nsurface, no \"trade now\" / \"buy now\" copy in the response body.","operationId":"kraken_click_beacon_v1_affiliate_kraken_click_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Kraken Click Beacon V1 Affiliate Kraken Click Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/affiliate/polymarket/link":{"get":{"tags":["polymarket-affiliate"],"summary":"Polymarket Affiliate Link","description":"Resolve the Polymarket event link-out, attributed to storyflo when\nthe affiliate is configured + enabled.\n\nFAILS OPEN: :func:`polymarket.build_event_url` returns the plain\n``polymarket.com/event/<slug>`` when the affiliate is disabled or the\nreferral code is unset, and appends ``?<param>=<ref>`` only when both\nare present. We therefore ALWAYS return 200 with a usable URL — the\neditorial link must never break. The ``click_id`` is an opaque per-\nrender token for our own observability, not the affiliate conversion\nkey (Polymarket round-trips the ``?ref`` param).","operationId":"polymarket_affiliate_link_v1_affiliate_polymarket_link_get","parameters":[{"name":"event","in":"query","required":true,"schema":{"type":"string","maxLength":256,"description":"Polymarket event slug (the trailing segment of polymarket.com/event/<slug>). Comes from the per-story market_context.matched_market.url the FE already holds.","title":"Event"},"description":"Polymarket event slug (the trailing segment of polymarket.com/event/<slug>). Comes from the per-story market_context.matched_market.url the FE already holds."},{"name":"embedder","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"description":"Embedder tenant slug. Reserved for downstream storyflo-internal sub-attribution (revenue split); Polymarket itself only ever sees storyflo's house referral code. Falls back to the house slug when omitted.","title":"Embedder"},"description":"Embedder tenant slug. Reserved for downstream storyflo-internal sub-attribution (revenue split); Polymarket itself only ever sees storyflo's house referral code. Falls back to the house slug when omitted."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Polymarket Affiliate Link V1 Affiliate Polymarket Link Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/affiliate/polymarket/click":{"post":{"tags":["polymarket-affiliate"],"summary":"Polymarket Click Beacon","description":"Fire-and-forget click beacon from the FE Polymarket bet-card.\n\nAccepts ``{click_id?, embedder_slug?, event?}`` and persists a zero-\namount ``click`` :class:`EmbedderRevenueEvent` attributed to the\nembedder (so the affiliate roll-up can build a click→deposit funnel\nwithout a second table — same shape the embed click-broker uses).\n\nBest-effort discipline (matches the rest of the affiliate surface):\n  * Unknown / omitted embedder slug → fall back to the house embedder\n    if one exists, else log-only (no DB row) and still 200. We never\n    500 a beacon.\n  * Replay within the same UTC day → IntegrityError on the\n    ``dub_event_id`` UNIQUE constraint → rollback + NOOP, still 200.\n  * Any other persist failure → rollback + log, still 200.\n\nIntentionally NOT FF-gated: ``POLYMARKET_AFFILIATE_ENABLED`` only\ncontrols whether :func:`polymarket_affiliate_link` mints an\nattributed URL; if the operator temporarily flips it off we still\nwant to log the clicks the card is sending so we don't lose tracking\non a transient disable.","operationId":"polymarket_click_beacon_v1_affiliate_polymarket_click_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Polymarket Click Beacon V1 Affiliate Polymarket Click Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/r/{token}":{"get":{"tags":["embed-click-broker"],"summary":"Embed Click Broker Redirect","description":"Public click-token attribution broker.\n\nSingle canonical click-out beacon for ALL storyflo embed creative\nformats. Verifies the HMAC, mints exactly one\n``EmbedderRevenueEvent`` row (event_type=``click``,\namount_usd_cents=0), and 302's to the target URL with\n``?utm_storyflo=<slug>`` appended.","operationId":"embed_click_broker_redirect_r__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/imp.gif":{"get":{"tags":["embed-impression-beacon"],"summary":"Embed Impression Gif","description":"1×1 transparent GIF impression beacon.\n\nALWAYS 200 with the GIF body — even on validation soft-fail — so a\nmisconfigured tag in production doesn't trigger ad-network alerts.\nCache-Control: no-store so every fire counts (the beacon-side\ndedupe is the only de-dupe layer).","operationId":"embed_impression_gif_v1_embed_imp_gif_get","parameters":[{"name":"slug","in":"query","required":true,"schema":{"type":"string","maxLength":120,"description":"publisher slug","title":"Slug"},"description":"publisher slug"},{"name":"size","in":"query","required":false,"schema":{"type":"string","maxLength":20,"description":"IAB size token","default":"300x250","title":"Size"},"description":"IAB size token"},{"name":"context","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Context"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/imp.json":{"post":{"tags":["embed-impression-beacon"],"summary":"Embed Impression Json","description":"JSON-body impression beacon for SDK-direct / in-iframe Beacon\nAPI fire.\n\nReturns ``{ok, deduped}`` so SDKs can short-circuit retries on\ndeduped fires. Always 200 — never raises so SDK error-paths don't\naccumulate beacon retries.","operationId":"embed_impression_json_v1_embed_imp_json_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ImpJsonBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embed Impression Json V1 Embed Imp Json Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/iab/{size}/{publisher_slug}/creative.json":{"get":{"tags":["embed-iab"],"summary":"Embed Iab Creative Payload","description":"JSON creative payload for the IAB display unit.\n\nConsumed by a DSP-side renderer or by the BE-served HTML below.\nSoft-fails to a zero-state payload when the embedder is unknown so\nDSPs that pre-fetch don't 404 noisily during onboarding.","operationId":"embed_iab_creative_payload_v1_embed_iab__size___publisher_slug__creative_json_get","parameters":[{"name":"size","in":"path","required":true,"schema":{"type":"string","title":"Size"}},{"name":"publisher_slug","in":"path","required":true,"schema":{"type":"string","title":"Publisher Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embed Iab Creative Payload V1 Embed Iab  Size   Publisher Slug  Creative Json Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/embed/iab/{publisher_slug}/{size}":{"get":{"tags":["embed-iab"],"summary":"Embed Iab Html","description":"Server-rendered IAB display creative.\n\nRenders fixed-pixel HTML at the exact IAB size, branded with\nstoryflo + the publisher slug. Inline CSS only. Click-through\nroutes via PR 2's ``/r/{token}`` broker.\n\n``noindex,nofollow`` per IAB iframe-creative convention — the page\nis meant to be embedded, not crawled.","operationId":"embed_iab_html_embed_iab__publisher_slug___size__get","parameters":[{"name":"publisher_slug","in":"path","required":true,"schema":{"type":"string","title":"Publisher Slug"}},{"name":"size","in":"path","required":true,"schema":{"type":"string","title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/embed/{publisher_slug}/manifest.json":{"get":{"tags":["embed-manifest"],"summary":"Embed Agent Manifest","description":"Agent-discoverable manifest for a publisher embedder.\n\nReturns a stable JSON shape downstream agents can rely on to plan\nad buys, audio renders, and attribution wiring. CORS-open + edge-\ncacheable.","operationId":"embed_agent_manifest_embed__publisher_slug__manifest_json_get","parameters":[{"name":"publisher_slug","in":"path","required":true,"schema":{"type":"string","title":"Publisher Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Embed Agent Manifest Embed  Publisher Slug  Manifest Json Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/embed/vast/{publisher_slug}.xml":{"get":{"tags":["embed-vast"],"summary":"Embed Vast Creative","description":"Return a VAST 4.0 audio creative for the named publisher slug.\n\n404s on unknown slug (mirrors PR #662 + #663 behavior). Falls back\nto a bare storyflo URL for ClickThrough when the click broker is\nnot provisioned (``EMBEDDER_HMAC_SECRET`` unset).","operationId":"embed_vast_creative_embed_vast__publisher_slug__xml_get","parameters":[{"name":"publisher_slug","in":"path","required":true,"schema":{"type":"string","title":"Publisher Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embedders/{slug}/network-rollup":{"get":{"tags":["embed-admin-rollup"],"summary":"Get Embedder Network Rollup","description":"Return the per-day per-creative-size rollup for one embedder.\n\nSee the module docstring for the response shape + window\nconstraints.","operationId":"get_embedder_network_rollup_v1_admin_embedders__slug__network_rollup_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Embedder Network Rollup V1 Admin Embedders  Slug  Network Rollup Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/embed/network-manifest.json":{"get":{"tags":["embed-network-manifest"],"summary":"Get Network Manifest","description":"Public ad-network onboarding catalog.\n\nStable + cacheable. 5-minute CDN cache so operator edits propagate\nquickly without a redeploy. CORS ``*`` so DSP discovery agents can\nfetch from any origin (this is the whole point of the surface).","operationId":"get_network_manifest_embed_network_manifest_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Network Manifest Embed Network Manifest Json Get"}}}}}}},"/v1/admin/revenue/summary":{"get":{"tags":["admin-revops"],"summary":"Admin Revenue Summary","description":"True-MRR + 30d revenue + payout-liability summary (shape frozen —\nsee module docstring).\n\nCounts come from :mod:`app.revenue_truth`, which is the single place\nthat knows the difference between a *paying* subscription and a\n*comped* one, and which renders every revenue rail — including the\nones that have never fired.","operationId":"admin_revenue_summary_v1_admin_revenue_summary_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Revenue Summary V1 Admin Revenue Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/distribution/rollup":{"get":{"tags":["admin-revops"],"summary":"Admin Distribution Rollup","description":"Per-channel distribution status board + publisher-level live\ncounts (shape frozen — see module docstring). Three round-trips.","operationId":"admin_distribution_rollup_v1_admin_distribution_rollup_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Distribution Rollup V1 Admin Distribution Rollup Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/metrics/play":{"post":{"tags":["metrics-beacon"],"summary":"Metrics Play Beacon","description":"Record an onsite play. Fire-and-forget — always 200, never raises.\n\nFlag OFF → accepted no-op (``recorded: false``). Flag ON → appends a\n``distribution_events`` row; a DB failure still returns 200 with\n``recorded: false`` so playback is never interrupted.","operationId":"metrics_play_beacon_v1_metrics_play_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PlayBeaconBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Metrics Play Beacon V1 Metrics Play Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/metrics/view":{"post":{"tags":["metrics-beacon"],"summary":"Metrics View Beacon","description":"Record an onsite article page-view. Fire-and-forget — always 200,\nnever raises.\n\nFlag OFF → accepted no-op (``recorded: false``). Flag ON → appends a\n``distribution_events`` row (event_type=view, channel=onsite) unless an\nidentical (article, ip_hash) view already landed this UTC day, in which\ncase it returns ``recorded: false, reason: deduped``. A DB failure still\nreturns 200 with ``recorded: false`` so page loads are never disturbed.","operationId":"metrics_view_beacon_v1_metrics_view_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ViewBeaconBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Metrics View Beacon V1 Metrics View Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/northstar":{"get":{"tags":["admin-northstar"],"summary":"Admin Northstar","description":"North-Star (DMI) operator dashboard. 404 when the flag is OFF (before\nauth — hides the surface); 401 without a valid admin token when ON.","operationId":"admin_northstar_v1_admin_northstar_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","maximum":52,"minimum":1,"default":12,"title":"Weeks"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Window Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Northstar V1 Admin Northstar Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/traction":{"get":{"tags":["admin-traction"],"summary":"Admin Traction","description":"One-shot operator traction headline. 401 without a valid admin token.","operationId":"admin_traction_v1_admin_traction_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Traction V1 Admin Traction Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/classify-backfill":{"post":{"tags":["admin-inbox-backfill"],"summary":"Admin Inbox Classify Backfill","description":"Classify ``limit`` of the oldest ``reply_intent IS NULL`` inbound\nrows through the ingest-time bridge. One-shot, resumable, idempotent.\n\nEvery row goes through\n:func:`app.outreach.inbound_reply_bridge.classify_inbound_message` —\nthe identical call the M365 poller and the ``/v1/intake/email`` writer\nmake. Consequences, all inherited rather than re-implemented:\n\n* **Nothing is sent.** The bridge has no send path; neither does this\n  route. Lead-class intents (tell_me_more / yes_keep_going /\n  who_are_you) are stamped ``awaiting_action`` with the intent\n  attached and left for a human — never templated, never answered.\n* **Nothing is marked auto-handled** that wasn't already. A row the\n  pre-existing auto-responder machinery had already closed is\n  recorded as ``auto_handled`` rather than re-opened.\n* **Machine mail** (bounce / auto-response) is skipped, and is\n  already outside the candidate predicate.\n* **UNRELATED stays NULL**, so the admin intent filter keeps meaning.\n* **Already-classified rows are never re-stamped.**\n\nResume by feeding ``next_cursor`` back in. ``drained`` true (and\n``next_cursor`` null) means the sweep read past the END of the\ncandidate set. Re-invoking with a stale cursor, or with none, is\nsafe — it just re-reads rows that are now no-ops.\n\n``unclassified_replies`` HAS A FLOOR AND DOES NOT REACH ZERO.\n``UNRELATED`` rows (which now include machine-notice mail) are left\nat ``reply_intent`` NULL on purpose, so they stay inside the tile's\npredicate forever. ``drained`` — not ``unclassified_after == 0`` —\nis the completion signal, and when ``drained`` is true this response\nechoes ``unclassified_floor``: the number the tile will sit at from\nhere. Before 2026-08-08 a caller that re-POSTed without threading\n``next_cursor`` saw ``drained=false`` forever once the stampable\nrows ran out, because each call re-read the same page of unstampable\nones; a non-productive page now walks on by itself.\n\n``scan_capped`` true means the opposite: the sweep walked\n``MAX_SCAN_PER_CALL`` rows without stamping anything and stopped\nwith a cursor. Re-invoke with that cursor.\n\nCost: zero. The classifier is pure regex (``re`` + ``enum``, no\nnetwork, no model call), so a full 254-row drain bills $0.00. The\n``limit`` cap bounds transaction count and request duration, not\nspend.","operationId":"admin_inbox_classify_backfill_v1_admin_inbox_classify_backfill_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows to look at this invocation.","default":100,"title":"Limit"},"description":"Rows to look at this invocation."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque resume token from a previous response's next_cursor. Omit to start from the oldest unclassified row.","title":"Cursor"},"description":"Opaque resume token from a previous response's next_cursor. Omit to start from the oldest unclassified row."},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Classify Backfill V1 Admin Inbox Classify Backfill Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/removal":{"patch":{"tags":["admin-publisher-removal"],"summary":"Admin Patch Publisher Removal","description":"Record (or clear) a publisher's removal request. Idempotent.\n\nRe-running with ``revoked: true`` is a no-op on ``takedown_at`` — the\noriginal stamp is kept and ``already_revoked`` comes back true — but it\nWILL re-pause any source that has since been re-activated, and it will\nupdate ``takedown_reason`` when a new one is supplied. That is\ndeliberate: the endpoint's job is to make the current state match the\ncreator's instruction every time it is called.","operationId":"admin_patch_publisher_removal_v1_admin_publishers__tenant_slug__removal_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherRemovalPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Publisher Removal V1 Admin Publishers  Tenant Slug  Removal Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listen/progress":{"post":{"tags":["listen-progress"],"summary":"Listen Progress","description":"Record playback progress against this listener's play row.\n\nReturns ``{\"ok\": true, \"recorded\": bool, ...}``. ``recorded: false`` with\na ``reason`` means the beacon was accepted but nothing was written\n(unknown article, no derivable listener, transient write failure) — the\nclient treats every 2xx identically and drops the response.","operationId":"listen_progress_v1_listen_progress_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListenProgressBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listen Progress V1 Listen Progress Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight":{"post":{"tags":["admin-greenlight"],"summary":"Greenlight","description":"Green-light a publisher: create-or-reuse the candidate (idempotent on\nemail), then provision the comped account + linked creator + claim token.\n404 when the flag is OFF (before auth); 401 without a valid admin token.","operationId":"greenlight_v1_admin_greenlight_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreenlightRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Greenlight V1 Admin Greenlight Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/candidates":{"get":{"tags":["admin-greenlight"],"summary":"List Candidates","description":"List onboarding candidates, newest first — optionally filtered by\n``state``. 404 when the flag is OFF (before auth).","operationId":"list_candidates_v1_admin_greenlight_candidates_get","parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Candidates V1 Admin Greenlight Candidates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/candidates/{candidate_id}":{"get":{"tags":["admin-greenlight"],"summary":"Get Candidate","description":"Single-candidate detail (with the full stage ledger). 404 when the flag\nis OFF (before auth) OR when the candidate does not exist.","operationId":"get_candidate_v1_admin_greenlight_candidates__candidate_id__get","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Candidate V1 Admin Greenlight Candidates  Candidate Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/candidates/{candidate_id}/confirm-source":{"post":{"tags":["admin-greenlight"],"summary":"Confirm Source Route","description":"Approve / override the discovered source for a candidate that source\ndiscovery flagged ``source_needs_confirm`` (audio- or video-typed, or a\nborderline text feed — the §3 \"never auto-attach\" rule). Attaches the\nadmin's ``feed_url``, provisions if needed, and advances the candidate to\n``provisioned`` — the next source-discovery tick then sends the invite.\n\n404 when the flag is OFF (before auth) OR the candidate does not exist;\n409 when the candidate is not awaiting confirmation.","operationId":"confirm_source_route_v1_admin_greenlight_candidates__candidate_id__confirm_source_post","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmSourceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Confirm Source Route V1 Admin Greenlight Candidates  Candidate Id  Confirm Source Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/candidates/{candidate_id}/retry":{"post":{"tags":["admin-greenlight"],"summary":"Retry Candidate","description":"Re-run provisioning for a candidate idempotently (the ``retry-stage``\naffordance — recovers a ``failed`` candidate, or no-ops a healthy one).\n404 when the flag is OFF (before auth) OR the candidate does not exist.","operationId":"retry_candidate_v1_admin_greenlight_candidates__candidate_id__retry_post","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Retry Candidate V1 Admin Greenlight Candidates  Candidate Id  Retry Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/candidates/{candidate_id}/ledger":{"get":{"tags":["admin-greenlight"],"summary":"Candidate Ledger","description":"The candidate's formalized stage ledger (PR-6): each transition as\n``{from, to, at, reason, actor, evidence, error}``, plus the on-file\nlive-evidence proof + last liveness check. 404 when the flag is OFF (before\nauth) OR the candidate does not exist.","operationId":"candidate_ledger_v1_admin_greenlight_candidates__candidate_id__ledger_get","parameters":[{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string","title":"Candidate Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Candidate Ledger V1 Admin Greenlight Candidates  Candidate Id  Ledger Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/greenlight/metrics":{"get":{"tags":["admin-greenlight"],"summary":"Pipeline Metrics","description":"Pipeline-wide observability read (PR-6) the admin board renders: counts per\nstage, the greenlit→live funnel, failures, median time-in-stage,\noldest-in-stage, the stuck-candidate list (amber >48h), and the north-star\ngreenlit→live P50. 404 when the flag is OFF (before auth).","operationId":"pipeline_metrics_v1_admin_greenlight_metrics_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pipeline Metrics V1 Admin Greenlight Metrics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/partners":{"get":{"tags":["admin-partners"],"summary":"List Partners","description":"The partners roster — paid (plus|pro) publishers UNION pipeline\ncandidates, seeded + synthetic rows excluded. 404 when the flag is OFF\n(before auth); 401 without a valid admin token.\n\n``segment`` ∈ {pro, plus, pipeline, all} (default all); ``stage`` filters to\na single ``OnboardingState``; ``q`` is a name/slug/email substring match;\n``limit`` / ``offset`` paginate the merged, most-recent-first list.","operationId":"list_partners_v1_admin_partners_get","parameters":[{"name":"segment","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Segment"}},{"name":"stage","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Partners V1 Admin Partners Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/partners/stats":{"get":{"tags":["admin-partners"],"summary":"Partner Stats","description":"Headline partner counts: pro / plus, comped vs paid, in-pipeline by\nstage, live-this-week, and needs-attention (opted-in but no payout rail;\ncandidates stuck >48h via ``compute_pipeline_metrics``). 404 when the flag\nis OFF (before auth).","operationId":"partner_stats_v1_admin_partners_stats_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Partner Stats V1 Admin Partners Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/partners/{tenant_slug}":{"get":{"tags":["admin-partners"],"summary":"Partner Detail","description":"A single partner 360: publisher fields + the linked candidate's stage\nledger (``build_ledger``) + recent articles (≤20) + payout status + the\non-file authorization receipt. 404 when the flag is OFF (before auth) OR\nwhen no publisher owns ``tenant_slug``.","operationId":"partner_detail_v1_admin_partners__tenant_slug__get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Partner Detail V1 Admin Partners  Tenant Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/credits":{"get":{"tags":["admin-credits"],"summary":"Admin Credits","description":"Per-publisher credits ledger + platform rollup + the gaps.\n\nNo ``Depends(get_db)``: a short-lived session is opened here and\nreleased in the ``finally`` before the response shape is built, so no\npooled connection is held while the payload is assembled (the canonical\npattern from ``app/routes/publisher_shows.py``).","operationId":"admin_credits_v1_admin_credits_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":1825,"minimum":1},{"type":"null"}],"description":"Trailing window in days. Omit for ALL TIME — credits extended is a lifetime number by nature.","title":"Days"},"description":"Trailing window in days. Omit for ALL TIME — credits extended is a lifetime number by nature."},{"name":"segment","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Segment"}},{"name":"product","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Product"}},{"name":"include_seeded","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Seeded"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Credits V1 Admin Credits Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/internal/notify":{"post":{"tags":["internal-notify"],"summary":"Internal Notify","description":"Send ONE allowlisted transactional email on behalf of a sibling product.\n\n404 while ``INTERNAL_NOTIFY_ENABLED`` is off (checked before auth).\n401 on a missing/incorrect bearer token, or when no token is configured.","operationId":"internal_notify_v1_internal_notify_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/NotifyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tts/render":{"post":{"tags":["tts-render"],"summary":"Tts Render","description":"Render (or cache-resolve) narration audio for machine callers.\n\nOrder of gates matters: flag first (OFF ⇒ 404 for everyone, surface\ndark), then shared-token auth (401), then the read-through (free),\nthen — only on a genuine miss — the paid EL call + upload.","operationId":"tts_render_v1_tts_render_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/TtsRenderBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Tts Render V1 Tts Render Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/wallet/circle":{"post":{"summary":"Listener Mint Circle Wallet","description":"Mint a Circle Programmable Wallet on the listener's behalf and\nbind the resulting address as their faucet recipient.\n\nThe \"skip-the-wallet\" path for crypto-shy listeners — Storyflo\nholds the keys via Circle's developer-controlled SCA wallets;\nthe listener never sees a private key, never installs an\nextension, never touches a seed phrase. Idempotent on\nlistener_token (Circle's idempotencyKey is derived from the\ntoken + chain), so repeat calls return the same wallet.\n\nReturns 503 when Circle isn't configured (CIRCLE_API_KEY +\nCIRCLE_ENTITY_SECRET_RAW) — the frontend renders that as\n\"Continue with your own wallet\" and falls back to the RainbowKit\nflow.","operationId":"listener_mint_circle_wallet_v1_listener__listener_token__wallet_circle_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Mint Circle Wallet V1 Listener  Listener Token  Wallet Circle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs":{"get":{"summary":"Admin Jobs List","description":"List render-jobs for operator triage. Filter by ``status``\n(``queued | running | done | failed``) and cap with ``limit`` (default\n20, hard ceiling 50). Sorted newest-first by ``queued_at``.","operationId":"admin_jobs_list_v1_admin_jobs_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs List V1 Admin Jobs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs/{job_id}/retry":{"post":{"summary":"Admin Jobs Retry","description":"Reset a single render-job back to ``queued`` so the worker picks it\nup on its next tick. Clears ``error`` and zeroes ``attempts`` so the\nretry budget starts fresh.","operationId":"admin_jobs_retry_v1_admin_jobs__job_id__retry_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs Retry V1 Admin Jobs  Job Id  Retry Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs/reset-failed":{"post":{"summary":"Admin Jobs Reset Failed","description":"Bulk-reset every ``failed`` job back to ``queued``. Useful after a\nprovider outage where dozens of jobs all dead-lettered for the same\ntransient reason.","operationId":"admin_jobs_reset_failed_v1_admin_jobs_reset_failed_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs Reset Failed V1 Admin Jobs Reset Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/jobs/reset-stuck":{"post":{"summary":"Admin Jobs Reset Stuck","description":"Reset every ``running`` job whose ``started_at`` is older than\n``max_age_minutes`` (default 5) back to ``queued``. Recovers from the\ncase where a Fly machine restart kills a worker mid-render — without\nthis, those rows stay ``running`` forever and the operator has to SSH\nin to SQL-update them by hand.\n\nRecently-started jobs are left alone so we don't yank work out from\nunder a still-healthy worker.","operationId":"admin_jobs_reset_stuck_v1_admin_jobs_reset_stuck_post","parameters":[{"name":"max_age_minutes","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Max Age Minutes"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Jobs Reset Stuck V1 Admin Jobs Reset Stuck Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/prequalify-backlog":{"post":{"summary":"Admin Intake Prequalify Backlog","description":"Pre-qualify the existing render-job backlog by cancelling jobs whose\nsource article body falls below ``min_words``. PR #184 added an intake-\nside gate, but the queue still holds thousands of pre-gate stubs that\nwould render into 8-second clickbait. This endpoint sweeps them in\nbounded batches; call repeatedly with ``dry_run=false`` until\n``would_cancel`` drops to zero.\n\nBody:\n    min_words: int (optional; default INTAKE_MIN_WORDS env, then 500)\n    dry_run: bool (default true)\n    limit: int (default 5000) — max render_jobs rows examined per call\n    status_filter: list[str] (default [\"queued\", \"deferred\"])\n\nJobs with a NULL ``article_id`` (e.g. forwarded-newsletter chunks with\nno 1:1 article row) are skipped — we only judge jobs we can word-count.","operationId":"admin_intake_prequalify_backlog_v1_admin_intake_prequalify_backlog_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Intake Prequalify Backlog V1 Admin Intake Prequalify Backlog Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/intake/sources":{"post":{"summary":"Intake Source Create","description":"Register a feed for a publisher. Status starts as ``pending`` —\noperators flip to ``active`` once they've vetted the source. Until\nthen the intake worker skips it.\n\nThis is the **registration** the public discovery feed waits on:\nonce the first article ingested through any active source lands in\nthe ``articles`` table, the demo fallback flips off automatically.","operationId":"intake_source_create_v1_intake_sources_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSourceCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSourceView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Intake Source List","operationId":"intake_source_list_v1_intake_sources_get","parameters":[{"name":"publisher_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IntakeSourceView"},"title":"Response Intake Source List V1 Intake Sources Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/intake/sources/{src_id}/activate":{"post":{"summary":"Intake Source Activate","description":"Operator gate — flips status pending → active. Until activated, the\nintake worker skips the source.\n\nAlso resets ``consecutive_failures`` to 0 and clears ``last_error``\non transition. Without this reset (pre 2026-05-31), re-activating a\npreviously-auto-disabled source would carry the old failure streak\nover — one more failure would immediately re-auto-disable it. Worse,\nif status was manually flipped active by a sibling code path that\nDIDN'T touch cf, the row sat at ``status=active, cf>threshold``\nforever and burned poll cycles without ever firing the disable gate\n(since the threshold check runs only at the cf++ moment). See triage\n`/tmp/storyflo_intake_log_triage_2026_05_31.md`.","operationId":"intake_source_activate_v1_intake_sources__src_id__activate_post","parameters":[{"name":"src_id","in":"path","required":true,"schema":{"type":"string","title":"Src Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSourceView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/intake/sources/{src_id}":{"delete":{"summary":"Intake Source Delete","operationId":"intake_source_delete_v1_intake_sources__src_id__delete","parameters":[{"name":"src_id","in":"path","required":true,"schema":{"type":"string","title":"Src Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks":{"post":{"summary":"Webhooks Create","description":"Register a new outbound webhook subscription. The plaintext secret\nis returned in the 201 body and never again — store it on your side.","operationId":"webhooks_create_v1_webhooks_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}}},"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"}}}}}},"get":{"summary":"Webhooks List","operationId":"webhooks_list_v1_webhooks_get","parameters":[{"name":"publisher_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookView"},"title":"Response Webhooks List V1 Webhooks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{sub_id}":{"delete":{"summary":"Webhooks Delete","operationId":"webhooks_delete_v1_webhooks__sub_id__delete","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{sub_id}/deliveries":{"get":{"summary":"Webhooks Deliveries","operationId":"webhooks_deliveries_v1_webhooks__sub_id__deliveries_get","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryView"},"title":"Response Webhooks Deliveries V1 Webhooks  Sub Id  Deliveries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{sub_id}/test":{"post":{"summary":"Webhooks Test","description":"Enqueue a synthetic ``webhook.ping`` delivery against this\nsubscription so receivers can verify their endpoint + signing logic.","operationId":"webhooks_test_v1_webhooks__sub_id__test_post","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"string","title":"Sub Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Webhooks Test V1 Webhooks  Sub Id  Test Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ratelimit/{scope}/{subject}":{"get":{"summary":"Ratelimit Debug","description":"Read-only debug peek at a rate-limit bucket. ``subject`` is the full\nderived identifier (e.g. ``k:abc123def456`` or ``ip:abc123def456``).\nUseful when triaging a 429 from the field — call with the same key the\nfailing request used and you can see the current count + reset time.\n\nAuth: admin token (X-Storyflo-Email-Token). Was previously gated by\nplain api_key; promoted to admin token as part of the 2026-05-16\nadmin auth audit.","operationId":"ratelimit_debug_v1_admin_ratelimit__scope___subject__get","parameters":[{"name":"scope","in":"path","required":true,"schema":{"type":"string","title":"Scope"}},{"name":"subject","in":"path","required":true,"schema":{"type":"string","title":"Subject"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/register":{"post":{"summary":"Embed Register","description":"Register an article view from a publisher's on-page embed.\n\nIdempotent on (publisher, slug-from-url): a returning visitor just\ngets the existing article + the most-recent done render-job's audio\nURL (or null when audio is still cooking). First-touch creates the\nArticle row + enqueues a render-job so the next pageview can play.","operationId":"embed_register_v1_embed_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedRegisterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embed Register V1 Embed Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/embed/track":{"post":{"summary":"Embed Track","description":"Record a play_started or play_completed event from a publisher\nembed. ``play_completed`` writes the three-leg ledger so the rev-share\naggregator can credit the publisher at month-end.","operationId":"embed_track_v1_embed_track_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedTrackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Embed Track V1 Embed Track Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/{period}":{"get":{"summary":"Admin Payouts","description":"Aggregate ledger entries for ``period`` (``YYYY-MM``) grouped by\npublisher. Returns gross / platform fee / net payout in micros plus a\nrounded USD float for human-readable display. Sorted by net payout\ndesc so the operator sees the biggest payouts first.","operationId":"admin_payouts_v1_admin_payouts__period__get","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Payouts V1 Admin Payouts  Period  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}":{"patch":{"summary":"Admin Publisher Patch","description":"Update name or payout_rail on an existing publisher. Used during\nprogrammatic onboarding when payout details land after the initial\ncreate call (e.g. Triton Media starts at ``payout_rail=null`` and gets\nflipped to ``stripe`` once their Connect onboarding completes).","operationId":"admin_publisher_patch_v1_admin_publishers__tenant_slug__patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherView"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/creatives":{"post":{"summary":"Admin Create Ad Creative","description":"Register an audio ad. ``audio_url`` should already be hosted —\nR2/S3, the publisher's CDN, or any HTTPS URL the audio pipeline\ncan fetch. Pre-roll length capped at 60s by the data model.","operationId":"admin_create_ad_creative_v1_admin_ads_creatives_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdCreativeCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Create Ad Creative V1 Admin Ads Creatives Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin List Ad Creatives","operationId":"admin_list_ad_creatives_v1_admin_ads_creatives_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Ad Creatives V1 Admin Ads Creatives Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/creatives/{creative_id}":{"patch":{"summary":"Admin Patch Ad Creative","operationId":"admin_patch_ad_creative_v1_admin_ads_creatives__creative_id__patch","parameters":[{"name":"creative_id","in":"path","required":true,"schema":{"type":"string","title":"Creative Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdCreativePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Ad Creative V1 Admin Ads Creatives  Creative Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/vertical-settings":{"get":{"summary":"Admin List Vertical Settings","description":"Per-vertical sponsor rules (CPM floor + exclusivity). Operator\ndashboard reads this to show the current state of every canonical\nvertical, including ones without rules yet.","operationId":"admin_list_vertical_settings_v1_admin_ads_vertical_settings_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Vertical Settings V1 Admin Ads Vertical Settings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/vertical-settings/{vertical}":{"patch":{"summary":"Admin Patch Vertical Setting","description":"Set CPM floor + exclusivity for a vertical. Upserts: creates the\nrow if missing. Pass exclusive_creative_id=\"\" to clear exclusivity\nwhile keeping the floor.","operationId":"admin_patch_vertical_setting_v1_admin_ads_vertical_settings__vertical__patch","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VerticalSettingPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Vertical Setting V1 Admin Ads Vertical Settings  Vertical  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ads/impressions/{period}":{"get":{"summary":"Admin Ad Impressions Rollup","description":"Per-creative impression rollup for the YYYY-MM period — used to\ninvoice advertisers. Sums impressions, completed plays, and gross\nrevenue from impressions × CPM. Returns rows sorted by gross\nrevenue desc so the operator sees biggest accounts first.","operationId":"admin_ad_impressions_rollup_v1_admin_ads_impressions__period__get","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ad Impressions Rollup V1 Admin Ads Impressions  Period  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/transcript.vtt":{"get":{"summary":"Article Transcript Vtt","description":"WebVTT version of the transcript — referenced by the\n/podcast.xml `<podcast:transcript>` tag. Used by podcast apps\nthat surface read-along captions during playback.","operationId":"article_transcript_vtt_v1_articles__article_slug__transcript_vtt_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/reactions/tally":{"get":{"summary":"Article Reactions Tally","description":"love/skip tally for an article + the caller's own reaction (if a\nlistener token is supplied). Powers the story-page Reactions widget.","operationId":"article_reactions_tally_v1_articles__article_slug__reactions_tally_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Reactions Tally V1 Articles  Article Slug  Reactions Tally Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/reactions":{"post":{"summary":"Article Reactions Post","description":"Upsert the caller's reaction (one per listener per article). Toggles\noff when the same kind is re-sent. Listener-token auth (Bearer).","operationId":"article_reactions_post_v1_articles__article_slug__reactions_post","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ReactionBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Reactions Post V1 Articles  Article Slug  Reactions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/chapters.json":{"get":{"summary":"Article Chapters Json","description":"Podcasting 2.0 chapter format. If the renderer stored\nchapters_json on the article (LLM-authored long-form chapters or\nlegacy paragraph-aligned heuristic output), serve those. Otherwise\nfall back to the cheap paragraph-splitter heuristic on body_text\nso clients always get something to scrub against.","operationId":"article_chapters_json_v1_articles__article_slug__chapters_json_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Article Chapters Json V1 Articles  Article Slug  Chapters Json Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ads/vast":{"get":{"summary":"Vast Ad Response","description":"VAST 4.x compliant ad response — Triton / AdsWizz / Magnite hit\nthis for fill on any audio impression. Currently returns a single\nInLine creative based on our existing stitched_creative selection;\nchains to a no-fill response when no creative matches.\n\nQuery params (per IAB OpenRTB-Audio convention):\n  slug    — article slug being played\n  vertical — content category for matching\n  gdpr_consent — TCF v2.2 consent string\n  gpp / gpp_sid — GPP signal\n\nFor SSPs that do programmatic bidding, the upstream RTB integration\nlands later (year-2 work). This endpoint covers the static-VAST\nseat application path.","operationId":"vast_ad_response_v1_ads_vast_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/inventory/forecast":{"get":{"summary":"Inventory Forecast","description":"Predicted impression count for the next N hours.\n\nMVP: linear extrapolation from the last N hours of play activity.\nReplace with a real model (Prophet / ARIMA / a learned baseline)\nonce we have a quarter of play data per vertical.","operationId":"inventory_forecast_v1_inventory_forecast_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Inventory Forecast V1 Inventory Forecast Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/transcript":{"get":{"summary":"Public Article Transcript","description":"Return time-coded sentences for the read-along overlay.\nDistributes the audio's total duration across sentences by\ncharacter count — coarse but cache-stable. Frontend uses\naudio.currentTime to pick the active segment.\n\nReturns 404 if the slug doesn't exist, 200 with empty segments if\nbody or audio isn't ready yet (frontend hides the overlay).","operationId":"public_article_transcript_v1_articles__article_slug__transcript_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Public Article Transcript V1 Articles  Article Slug  Transcript Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/alignment":{"get":{"summary":"Public Article Alignment","description":"Tier 2 #11 · character-level audio timing for the read-along\noverlay. Returns the alignment JSON captured at render time\n(ElevenLabs `/with-timestamps` response shape). 404 if the slug\ndoesn't exist, 200 with ``alignment: null`` if the article was\nrendered by a provider without native timestamps (Kokoro,\nOpenAI TTS) — frontend falls back to ``/transcript`` segments.","operationId":"public_article_alignment_v1_articles__article_slug__alignment_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Public Article Alignment V1 Articles  Article Slug  Alignment Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/audio":{"get":{"summary":"Public Article Audio","description":"Public audio resolve by slug — works for anonymous visitors\non /story/[slug]. No listener token required, no api_key gate.\nThree states:\n\n  200 → audio is ready. Body: {audio_url, audio_status: \"ready\"}\n  202 → article exists but render-job is queued / running. We\n        kick off a render-on-demand if no job exists yet, then\n        return {audio_status, retry_after_seconds}.\n  404 → article slug doesn't exist OR is listener-private and the\n        viewer doesn't own it (audio_status='listener_private').\n\nAudioPlayer treats 202 as \"still rendering — show friendly UI\nand poll\" rather than throwing an error like 404 used to.\n\nWrapped end-to-end in try/except so any unexpected failure\nsurfaces as a 202 'unavailable' rather than a 500 the listener\ncan see. The exception still hits Sentry + structured logs.","operationId":"public_article_audio_v1_articles__article_slug__audio_get","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}},{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{article_slug}/play-beacon":{"post":{"summary":"Public Article Play Beacon","description":"Frontend audio-element beacon fallback.\n\nListeners on Spotify / Apple Podcasts fetch the audio enclosure\ndirectly from R2, bypassing our /v1/articles/{slug}/audio route —\nso we can't observe those plays server-side. The web player\nPOSTs here on the ``play`` audio event as a near-zero-cost\nfallback so the rev-share earnings widget reflects real listens.\n\nSame write semantics as the resolve path: per-day SHA-256 ip_hash\n(or hash of X-Listener-Token when present), no PII stored. The\nwrite is wrapped + rollback-safe so a beacon never returns 500.\nReturns fast with ``{\"ok\": true}`` regardless of write outcome —\na beacon is fire-and-forget by design.\n\nFlywheel pull-through (migration 0164): an optional JSON body\n``{\"source_embedder_slug\": \"<slug>\"}`` (or the ``?via=<slug>`` query\nparam) attributes the play to the embedder surface it came through.\nBoth are optional + parsed defensively — a body-less beacon (every\nexisting caller) still works unchanged.","operationId":"public_article_play_beacon_v1_articles__article_slug__play_beacon_post","parameters":[{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Public Article Play Beacon V1 Articles  Article Slug  Play Beacon Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/audio/{article_slug}":{"get":{"summary":"Listener Audio With Preroll","description":"Return the audio URL for an article with pre-roll stitched in\n(free tier) or the bare article audio (plus tier / no creative\neligible). Single URL — listener client just plays it.\n\nTier-gated: free=3/day, plus=10/day, pro=unlimited. Burst credits\nstack on top. 402 with structured upgrade hints when the daily cap\nis exhausted.","operationId":"listener_audio_with_preroll_v1_listener__listener_token__audio__article_slug__get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Audio With Preroll V1 Listener  Listener Token  Audio  Article Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/quota":{"get":{"summary":"Listener Quota Status","description":"Read-only quota probe — frontend uses this to show 'X left\ntoday' without burning a unit. Same shape as the audio endpoint's\n'quota' subobject.","operationId":"listener_quota_status_v1_listener__listener_token__quota_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Quota Status V1 Listener  Listener Token  Quota Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/audio/{article_slug}/rerender":{"post":{"summary":"Listener Voice Rerender","description":"Plus/Pro listeners re-render the current article in a chosen\nvoice on demand. Free listeners get 402 with an upgrade pointer.\n\nThrottled per-listener (env-tunable, default 5/day UTC bucket).\nCache hits on (article, voice) skip the throttle so a listener\nflipping between two pre-rendered voices doesn't burn quota.","operationId":"listener_voice_rerender_v1_listener__listener_token__audio__article_slug__rerender_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VoiceRerenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Voice Rerender V1 Listener  Listener Token  Audio  Article Slug  Rerender Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/render-voice":{"post":{"summary":"Lazy Render Voice","description":"Lazy-render an article in a non-default free voice.\n\nWhen a listener picks a free-tier voice via /v1/listeners/{token}/voice\nthat the worker never pre-rendered, this endpoint produces it on\ndemand — NO Plus/Pro gate (the on-demand /rerender path above carries\nthat gate; this one is the free-tier counterpart so EVERY free voice\nworks once a listener selects it).\n\nFlow:\n  1. Cache hit on (slug, voice) → 200 with audio_url.\n  2. Miss → enqueue a fresh render at 'prerender' priority + 202\n     with a polling URL.\n\nAuth: a valid (non-expired) listener_token is the only gate. We\ndon't bill the listener and don't decrement any quota; the audio\njoin the warm cache so the second listener who picks the same\n(slug, voice) hits step 1.","operationId":"lazy_render_voice_v1_articles__slug__render_voice_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_LazyRenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/render-voice/status":{"get":{"summary":"Lazy Render Voice Status","description":"Polling counterpart to POST /v1/articles/{slug}/render-voice.\n\nRe-runs the cache pre-check; returns 200 ready when the worker has\nfinished, 202 rendering otherwise. Unauthenticated by design —\nonce a listener has kicked the render off, polling is a read-only\ncache probe.","operationId":"lazy_render_voice_status_v1_articles__slug__render_voice_status_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"voice_id","in":"query","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/ad-for/{article_slug}":{"get":{"summary":"Listener Select Ad","description":"Resolve a pre-roll for the listener × article pair. Returns\n``{creative: {...}}`` when an ad is selected, ``{creative: null}``\nfor plus-tier or no eligible creatives. The listener client plays\nthe audio_url before the article and posts back to the impression\nendpoint when done.","operationId":"listener_select_ad_v1_listener__listener_token__ad_for__article_slug__get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Select Ad V1 Listener  Listener Token  Ad For  Article Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/ad-impression":{"post":{"summary":"Listener Ad Impression","description":"Record a played ad. Idempotent at the row level (a duplicate\nPOST creates a duplicate row — the impression rollup intentionally\ncounts every playback). Frequency cap reads these rows.","operationId":"listener_ad_impression_v1_listener__listener_token__ad_impression_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdImpressionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Ad Impression V1 Listener  Listener Token  Ad Impression Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/checkout":{"post":{"summary":"Listener Checkout","description":"Mint a Stripe Checkout Session for a listener to upgrade to\nStoryflo+.\n\nDEPRECATED 2026-05-16: superseded by the Subscription Intent +\nPayment Element rail at POST /v1/listener/{token}/subscription-intent.\nHandler retained for 24h grace window to catch cached bundles still\ncalling it; removal in a follow-up commit after another quiet 24h.","operationId":"listener_checkout_v1_listener__listener_token__checkout_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CheckoutRequest","default":{"plan":"monthly","tier":"plus","ui_mode":"hosted"}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/subscription-intent":{"post":{"summary":"Listener Subscription Intent","description":"Mint a Stripe Subscription in ``default_incomplete`` state and\nreturn the first invoice's PaymentIntent client_secret for the\nfrontend Payment Element to confirm.\n\nCoexists with POST /v1/listener/{token}/checkout (Embedded\nCheckout). Both must remain functional during the 48h validation\nwindow.","operationId":"listener_subscription_intent_v1_listener__listener_token__subscription_intent_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubscriptionIntentBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Subscription Intent V1 Listener  Listener Token  Subscription Intent Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/billing-portal":{"post":{"summary":"Listener Billing Portal","description":"Return a Stripe-hosted billing portal URL for the listener to\nmanage their subscription (cancel, switch plan, update card). 404\nif the listener has no Stripe customer record (i.e. never started\ncheckout).","operationId":"listener_billing_portal_v1_listener__listener_token__billing_portal_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Billing Portal V1 Listener  Listener Token  Billing Portal Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/stripe/payment-method-domain":{"post":{"summary":"Admin Register Payment Method Domain","description":"Register (or revalidate) a Stripe payment-method-domain so Apple\nPay / Google Pay / Link render inside Embedded Checkout.\n\nOperator-only; protected by the admin token. Idempotent — re-runs\nrevalidate the existing record rather than create duplicates.\n\nRecommended cadence: run once per environment after first deploy\n(curl POST with X-Storyflo-Email-Token), then re-run any time the\nStripe dashboard reports a wallet flipped to \"inactive\". Embedded\nCheckout hosts the well-known association file automatically, so\nno static file needs to be served from storyflo.com — Stripe\nfetches it from their own infrastructure on validation.","operationId":"admin_register_payment_method_domain_v1_admin_stripe_payment_method_domain_post","parameters":[{"name":"domain","in":"query","required":false,"schema":{"type":"string","default":"storyflo.com","title":"Domain"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Register Payment Method Domain V1 Admin Stripe Payment Method Domain Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/wallet":{"patch":{"summary":"Admin Publisher Set Wallet","description":"Set the publisher's USDC payout wallet (EVM address). The\naddress is validated by shape; the operator is expected to confirm\nvia a basescan lookup or a 0-amount test transfer before relying\non it for real payouts.","operationId":"admin_publisher_set_wallet_v1_admin_publishers__tenant_slug__wallet_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_WalletPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publisher Set Wallet V1 Admin Publishers  Tenant Slug  Wallet Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/paypal":{"patch":{"summary":"Admin Publisher Set Paypal","description":"Persist a publisher's PayPal payer id ahead of full Payouts API\nintegration. Default settlement is PYUSD — PayPal converts at\npayout time if the publisher's account has the PYUSD balance pref.\nThe actual disbursement endpoint will land when paypal_client_id +\npaypal_client_secret are set on Fly.","operationId":"admin_publisher_set_paypal_v1_admin_publishers__tenant_slug__paypal_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PaypalPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publisher Set Paypal V1 Admin Publishers  Tenant Slug  Paypal Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payout-rails-status":{"get":{"summary":"Admin Payout Rails Status","description":"Operator dashboard: which rails are wired and which are scaffolds.\nSentinel for the publisher-onboarding UI to know which rails to\nshow a real onboarding link for vs. which are coming-soon.","operationId":"admin_payout_rails_status_v1_admin_payout_rails_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Payout Rails Status V1 Admin Payout Rails Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/{period}/prepare":{"post":{"summary":"Admin Prepare Payout Batch","description":"Generate one PayoutBatch per publisher for the given YYYY-MM\nperiod from the existing ledger rollup. Uses the same window logic\nas ``/v1/admin/payouts/{period}`` so the numbers match.\n\nIdempotent on (period, publisher_id):\n- Rows that don't exist yet → created in ``prepared`` state.\n- Rows in ``prepared`` state → ``net_payout_micros`` and\n  ``destination_address`` recomputed (in case the publisher updated\n  their wallet between runs).\n- Rows in any other state (signed/confirmed/failed) → left alone.","operationId":"admin_prepare_payout_batch_v1_admin_payouts__period__prepare_post","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Prepare Payout Batch V1 Admin Payouts  Period  Prepare Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/batches/{batch_id}":{"get":{"summary":"Admin Payout Batch Detail","operationId":"admin_payout_batch_detail_v1_admin_payouts_batches__batch_id__get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","title":"Batch Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Payout Batch Detail V1 Admin Payouts Batches  Batch Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/{period}/batches.csv":{"get":{"summary":"Admin Payouts Csv Export","description":"CSV export of prepared batches for a period — designed for direct\nimport into disperse.app, Coinbase Wallet bulk-send, or Safe's\ntransaction batcher.\n\nFormat: ``address,amount`` where ``amount`` is denominated in the\ncurrency unit (USDC, not micros) so the operator pastes a\nhuman-readable file. Only includes rows in ``prepared`` state with\na valid destination address — anything missing the wallet shows up\nin the operator dashboard but is excluded from the CSV so the\nimport doesn't choke.","operationId":"admin_payouts_csv_export_v1_admin_payouts__period__batches_csv_get","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"currency","in":"query","required":false,"schema":{"type":"string","default":"usdc_base","title":"Currency"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/batches/{batch_id}/mark-sent":{"post":{"summary":"Admin Payout Mark Sent","description":"Record that the operator has broadcast this batch on-chain.\nFlips status: prepared → signed. Stores tx_hash + sender_address for\nthe confirmation worker (and for audit). The worker independently\npolls Base RPC and flips signed → confirmed once the tx lands.","operationId":"admin_payout_mark_sent_v1_admin_payouts_batches__batch_id__mark_sent_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","title":"Batch Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MarkSentRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Payout Mark Sent V1 Admin Payouts Batches  Batch Id  Mark Sent Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/payouts/{period}/execute-stripe":{"post":{"summary":"Admin Execute Stripe Payouts","description":"Execute Stripe Connect transfers for all eligible publishers in a period.\n\nReads prepared PayoutBatch rows for publishers whose:\n  - payout_preference is 'usd' (or unset/null — default rail)\n  - stripe_account_id is set\n  - stripe_payouts_enabled is True\n  - net_payout_micros ≥ min_payout_usd * 1_000_000\n\n``dry_run=True`` (DEFAULT) returns a preview without calling Stripe.\n``dry_run=False`` calls ``stripe.Transfer.create`` with a deterministic\nidempotency key ``payout:{period}:{publisher_id}`` so re-runs are safe.\nOn success flips the batch row status to ``signed`` and stores the\nStripe transfer id in ``tx_hash``.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_execute_stripe_payouts_v1_admin_payouts__period__execute_stripe_post","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"min_payout_usd","in":"query","required":false,"schema":{"type":"number","default":0.5,"title":"Min Payout Usd"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Execute Stripe Payouts V1 Admin Payouts  Period  Execute Stripe Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/stripe-connect":{"post":{"summary":"Admin Stripe Connect Onboarding","description":"Start (or resume) Stripe Connect Express onboarding for a publisher.\n\nFirst call: creates a Stripe connected account, persists\n``stripe_account_id`` on the publisher row, mints a one-time\nonboarding URL, returns it.\n\nSubsequent calls: reuses the existing ``stripe_account_id`` and just\nmints a fresh onboarding URL (the previous one expires after a\nsingle use).","operationId":"admin_stripe_connect_onboarding_v1_admin_publishers__tenant_slug__stripe_connect_post","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_StripeOnboardingRequest","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/_StripeOnboardingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/plan/checkout":{"post":{"summary":"Listener Plus Checkout","description":"Mint a Stripe Checkout Session for the Listener Plus ($4.99/mo)\nupgrade. Trust model matches the rest of the listener surface —\npossession of ``listener_token`` is the credential.\n\nReturns ``{\"checkout_url\": \"https://checkout.stripe.com/...\"}``.\n503 when STRIPE_PRICE_LISTENER_PLUS / platform key not set (operator\nhint in the body). 404 when the token doesn't resolve to a row.","operationId":"listener_plus_checkout_v1_listeners__listener_token__plan_checkout_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerPlusCheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Plus Checkout V1 Listeners  Listener Token  Plan Checkout Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/stripe":{"post":{"summary":"Stripe Webhook","description":"Stripe-signed webhook handling both rails:\n\n1. Connect account state — ``account.updated`` mirrors charges_enabled\n   + payouts_enabled onto the publisher row.\n2. Listener subscriptions — ``checkout.session.completed`` flips a\n   listener's tier to 'plus' once they finish Storyflo+ checkout;\n   ``customer.subscription.updated`` mirrors active/canceled state;\n   ``customer.subscription.deleted`` flips back to 'free'.\n\nAll other event types return 200 ignored so Stripe stops retrying.","operationId":"stripe_webhook_v1_webhooks_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Stripe Webhook V1 Webhooks Stripe Post"}}}}}}},"/v1/listeners/{listener_token}/notifications":{"get":{"summary":"List Listener Notifications","description":"Return the bell-icon feed for one listener.\n\n``unread_only`` filters to ``read_at IS NULL`` for the badge count\npath. The default ordering is created_at DESC so the freshest\nrender-done sits at the top of the popover. limit is capped at\n100 so a misbehaving client can't pull the entire history in one\nshot.","operationId":"list_listener_notifications_v1_listeners__listener_token__notifications_get","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"unread_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Unread Only"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Listener Notifications V1 Listeners  Listener Token  Notifications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/notifications/{notification_id}/read":{"post":{"summary":"Mark Listener Notification Read","description":"Stamp ``read_at`` on a single notification. 404 if the row\ndoesn't belong to the listener — same 404 whether the listener\nis wrong or the id is wrong, so a probe can't fingerprint the\ntable contents through this endpoint.","operationId":"mark_listener_notification_read_v1_listeners__listener_token__notifications__notification_id__read_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}},{"name":"notification_id","in":"path","required":true,"schema":{"type":"string","title":"Notification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Listener Notification Read V1 Listeners  Listener Token  Notifications  Notification Id  Read Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{listener_token}/notifications/mark-all-read":{"post":{"summary":"Mark All Listener Notifications Read","description":"Bulk-stamp ``read_at`` on every unread row. Returns the count\nof rows updated so the client can short-circuit the badge refresh\ninstead of round-tripping the list endpoint again.","operationId":"mark_all_listener_notifications_read_v1_listeners__listener_token__notifications_mark_all_read_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark All Listener Notifications Read V1 Listeners  Listener Token  Notifications Mark All Read Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uptime/ping":{"get":{"summary":"Uptime Ping","description":"Lightweight uptime endpoint — touches DB + cache so the probe\ncatches deeper outages than /v1/health (which only confirms the\nHTTP listener is up). Designed for BetterStack / Healthchecks.io\nstyle monitors that expect HTTP 200 within 5s.\n\nReturns ``{ok: true, ...}`` on success; raises 503 if any\ndependency fails so the monitor flips immediately.","operationId":"uptime_ping_v1_uptime_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Uptime Ping V1 Uptime Ping Get"}}}}}}},"/v1/admin/agent-revenue/{period}":{"get":{"summary":"Admin Agent Revenue","description":"Per-(client, action) usage rollup for the YYYY-MM period.\n\nCombines OAuth-token usage (agent_usage table — no direct revenue\nbut indicates which clients drove subscriber upgrades) with x402\nreceipt totals (agent_usage.revenue_micros — direct USDC revenue\nposted by the x402 verify path).","operationId":"admin_agent_revenue_v1_admin_agent_revenue__period__get","parameters":[{"name":"period","in":"path","required":true,"schema":{"type":"string","title":"Period"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Agent Revenue V1 Admin Agent Revenue  Period  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/x402.json":{"get":{"summary":"X402 Descriptor","description":"x402 service descriptor — published per the x402 spec for\nAgentKit and other client toolkits to auto-discover.","operationId":"x402_descriptor__well_known_x402_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response X402 Descriptor  Well Known X402 Json Get"}}}}}}},"/.well-known/agentkit-tools.json":{"get":{"summary":"Agentkit Manifest","description":"Coinbase AgentKit tool registration. Drop this URL into the\nCoinbase Developer Platform to expose Storyflo as an agent tool.","operationId":"agentkit_manifest__well_known_agentkit_tools_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agentkit Manifest  Well Known Agentkit Tools Json Get"}}}}}}},"/v1/x402/articles":{"get":{"summary":"X402 Search Articles","description":"x402 metered search. Charges $0.001 USDC on Base per call.","operationId":"x402_search_articles_v1_x402_articles_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/x402/articles/{slug}":{"get":{"summary":"X402 Get Article","operationId":"x402_get_article_v1_x402_articles__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/x402/articles/{slug}/audio":{"get":{"summary":"X402 Get Audio","operationId":"x402_get_audio_v1_x402_articles__slug__audio_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/resource/{slug}/stream":{"get":{"summary":"Resource Stream","description":"Agent listen-fetch — mp3 bytes + settlement receipt (via _x402_gate),\npriced by the owning publisher's toggle. Free until they opt in + the master\nflag is on. NOT a fair-use action, so audio is never granted free.","operationId":"resource_stream_v1_resource__slug__stream_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/resource/{slug}/download":{"get":{"summary":"Resource Download","description":"Agent download-fetch — same as /stream at the download-tier price, served\nas an attachment (a keepable copy).","operationId":"resource_download_v1_resource__slug__download_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/resource/{slug}/transcript":{"get":{"summary":"Resource Transcript","description":"Free discovery / citation surface — the narrated words as text. Runs the\nsame 404 / DMCA gate as the paid tiers but is ALWAYS free (never touches the\ngate), so an agent can cite or decide before paying to listen. JSON by\ndefault; text/plain on Accept: text/plain.","operationId":"resource_transcript_v1_resource__slug__transcript_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/partner/alpha":{"get":{"summary":"Partner Alpha","description":"Signalflo partner API — the freshest daily-brief alpha (text + audio URL)\nfor a vertical, key-gated by ``X-Signalflo-Partner-Key``.\n\nStoryflo is consumed by Signalflo as an external, paid content/audio infra\nprovider; this is the dedicated seam (vs the public x402 rail). Returns only\nalready-public content: the free transcript text + the public audio URL — no\nnew data exposure, no write path. SELF-GATING: if ``SIGNALFLO_PARTNER_KEY``\nis unset the endpoint 404s (inert on prod until a key is issued).","operationId":"partner_alpha_v1_partner_alpha_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"type":"string","default":"crypto","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Partner Alpha V1 Partner Alpha Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/partner/plays":{"get":{"summary":"Partner Plays","description":"Signalflo partner API — a publisher's most-recent rendered alpha plays\n(newest first), key-gated by ``X-Signalflo-Partner-Key``.\n\nWhere /v1/partner/alpha serves the freshest daily brief for a vertical,\nthis serves the recent articles authored by a specific Storyflo publisher\n(a fund manager such as Theo, ``tenant_slug='theo-alpha'``). Returns only\nalready-public content: the free transcript text + the public audio URL —\nno new data exposure, no write path. SELF-GATING: if\n``SIGNALFLO_PARTNER_KEY`` is unset the endpoint 404s (inert on prod until a\nkey is issued).","operationId":"partner_plays_v1_partner_plays_get","parameters":[{"name":"publisher","in":"query","required":true,"schema":{"type":"string","title":"Publisher"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Partner Plays V1 Partner Plays Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/x402-audio.json":{"get":{"summary":"X402 Audio Manifest","description":"x402-audio resource manifest for agentic discovery (the SPEC the OSS\nx402storyfloaudio kit consumes). Lists ONLY episodes whose owning publisher\nopted into agent monetization. PUBLIC projection only — no ranking /\ndiscovery signal, no internal ids / keys, no provider / cost. Empty until\nX402_AUDIO_RESOURCES is on.","operationId":"x402_audio_manifest__well_known_x402_audio_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response X402 Audio Manifest  Well Known X402 Audio Json Get"}}}}}}},"/v1/x402/digest":{"post":{"summary":"X402 Digest","operationId":"x402_digest_v1_x402_digest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_X402DigestRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefing/daily":{"get":{"summary":"Daily Premium Briefing","description":"D-2.9 · daily premium briefing.\n\nTop-100 trending articles for the day, stitched into a single audio\nartifact (or returned as an ordered audio_url list when the stitch\ninfra isn't available — see ``app/audio_stitch.py``). x402-gated;\ndefault price 100_000 USDC micros ($0.10), override via\n``BRIEFING_DAILY_PRICE_MICROS``.\n\nRevenue split on settled payment:\n  - 70% across the day's publishers, weighted by article count\n    in the briefing (the more of your stories made the cut, the\n    bigger your slice)\n  - 20% to the calling agent's recommender_address (when an\n    sf_live_… key was presented); folds into platform when absent\n  - 10% (or 30% w/o agent) → platform\n\nErrors NEVER eat the payment: if ledger insertion fails after the\nfacilitator confirmed settlement we log loudly, ping operator_alerts,\nand return 500 so the listener-side wallet keeps the dispute open.","operationId":"daily_premium_briefing_v1_briefing_daily_get","parameters":[{"name":"idempotency-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/briefing/{vertical}/daily":{"get":{"summary":"Vertical Premium Briefing","description":"Per-vertical premium briefing — top-25 trending articles in the\ngiven vertical from the last 24h, stitched per the same shape as\n/v1/briefing/daily. x402-gated; default price 50_000 USDC micros\n($0.05), override via ``BRIEFING_VERTICAL_PRICE_MICROS``.\n\nRevenue split + ledger semantics are identical to the daily briefing:\n70% publishers (weighted by article count), 20% recommender agent\n(folds into platform when no sf_live_… key was presented), 10%/30%\nplatform.\n\nUnknown vertical → 404. The 7-bucket taxonomy is canonical (see\n[[project_storyflo_vertical_taxonomy]]); ``other`` is a classifier\nfallback, not a sellable bucket.","operationId":"vertical_premium_briefing_v1_briefing__vertical__daily_get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}},{"name":"idempotency-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/referrals/leaderboard":{"get":{"summary":"Admin Referral Leaderboard","description":"Top referrers by # of new listener signups attributed. Used for\ngrowth experiments + future referral-reward triggers.","operationId":"admin_referral_leaderboard_v1_admin_referrals_leaderboard_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Referral Leaderboard V1 Admin Referrals Leaderboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{listener_token}/burst-checkout":{"post":{"summary":"Listener Burst Checkout","description":"Mint a Stripe Checkout Session for a one-shot Burst credit\npack. Webhook (checkout.session.completed with pack metadata)\nincrements burst_credits_remaining + sets expiry.","operationId":"listener_burst_checkout_v1_listener__listener_token__burst_checkout_post","parameters":[{"name":"listener_token","in":"path","required":true,"schema":{"type":"string","title":"Listener Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_BurstCheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Burst Checkout V1 Listener  Listener Token  Burst Checkout Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/backfill-embeds":{"post":{"summary":"Admin Backfill Embeds","description":"Re-scan legacy articles with the embed_detector (added in commit\nda3d16d) and populate ``parsed_json['embeds']`` for rows ingested\nbefore the detector existed.\n\nBody:\n  - ``since``: optional ISO datetime — only consider rows created\n    after this (default: 60 days ago).\n  - ``limit``: max rows to scan (default 200, max 5000).\n  - ``dry_run``: if True (default), count only — no writes.\n  - ``only_if_no_embeds``: if True (default), skip rows whose\n    ``parsed_json['embeds']`` is already a non-empty list.\n\nReturns counts and a ``by_type`` breakdown of newly detected\nembeds. Per-row exceptions are caught + logged — one malformed\narticle never poisons the batch.","operationId":"admin_backfill_embeds_v1_admin_articles_backfill_embeds_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Embeds V1 Admin Articles Backfill Embeds Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/ai-plugin.json":{"get":{"summary":"Openai Ai Plugin Manifest","description":"ChatGPT (GPTs) legacy manifest — kept for backcompat. New\ninstalls use the Apps SDK manifest at /.well-known/openai-app.yaml.","operationId":"openai_ai_plugin_manifest__well_known_ai_plugin_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Openai Ai Plugin Manifest  Well Known Ai Plugin Json Get"}}}}}}},"/.well-known/openai-app.yaml":{"get":{"summary":"Openai App Spec","description":"OpenAPI 3.1 spec the ChatGPT Apps SDK consumes. Returned as\ntext/yaml so the SDK reads it directly.","operationId":"openai_app_spec__well_known_openai_app_yaml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/mcp.json":{"get":{"summary":"Mcp Manifest","description":"Claude / Anthropic MCP discovery manifest. The connector UI in\nclaude.ai fetches this to populate the install card.","operationId":"mcp_manifest__well_known_mcp_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Mcp Manifest  Well Known Mcp Json Get"}}}}}}},"/.well-known/oauth-protected-resource/mcp":{"get":{"summary":"Oauth Protected Resource Metadata","operationId":"oauth_protected_resource_metadata__well_known_oauth_protected_resource_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Protected Resource Metadata  Well Known Oauth Protected Resource Mcp Get"}}}}}}},"/.well-known/oauth-protected-resource/mcp/v1":{"get":{"summary":"Oauth Protected Resource Metadata","operationId":"oauth_protected_resource_metadata__well_known_oauth_protected_resource_mcp_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Protected Resource Metadata  Well Known Oauth Protected Resource Mcp V1 Get"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"summary":"Oauth Protected Resource Metadata","operationId":"oauth_protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Protected Resource Metadata  Well Known Oauth Protected Resource Get"}}}}}}},"/v1/listeners/{token}/referral":{"get":{"summary":"Listener Referral Status","description":"Listener's HEX-format referral card. 2026-05-20: format shifted\nfrom base32 → FLO-XXXXXX (operator vision · listener-earn loop).\nReturns share URL + referred count + community tier (Listener /\nCommunity Curator at 10 / Platform Maintainer at 100).\nPublic read — token IS the credential.","operationId":"listener_referral_status_v1_listeners__token__referral_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Referral Status V1 Listeners  Token  Referral Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listeners/{token}/role":{"post":{"summary":"Admin Set Listener Role","description":"Operator override for the HEX-FLO role ladder.\n\nThe /referral endpoint auto-bumps role on referral threshold cross\n(one-way up). This endpoint is the only path that can demote, and\nthe only path that can promote without earning the referrals (e.g.\ndeputizing a known-good listener as curator before they've hit 10\nreferrals). Admin-token gated.","operationId":"admin_set_listener_role_v1_admin_listeners__token__role_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Set Listener Role V1 Admin Listeners  Token  Role Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/curator/articles/{slug}/flag":{"post":{"summary":"Curator Flag Article","description":"Community-curator content flag. Inserts a row into curator_flags\nfor the maintainers to review. Requires role >= community_curator\non the X-Listener-Token caller.","operationId":"curator_flag_article_v1_curator_articles__slug__flag_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Curator Flag Article V1 Curator Articles  Slug  Flag Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/curator/flags":{"get":{"summary":"Curator List Flags","description":"Recent curator flags. Maintainer-only review queue.\nRequires role >= platform_maintainer.","operationId":"curator_list_flags_v1_curator_flags_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"x-listener-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Listener-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Curator List Flags V1 Curator Flags Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/{slug}/render":{"post":{"summary":"Admin Rerender Article","description":"Force a fresh render-job for an article. Use when a previous\njob ended in 'cancelled' or 'failed' and the operator wants to\nretry. Idempotent only on the article (always queues a new job;\nthe worker dedupes by content if needed).","operationId":"admin_rerender_article_v1_admin_articles__slug__render_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Rerender Article V1 Admin Articles  Slug  Render Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/dedupe-by-slug":{"post":{"summary":"Admin Articles Dedupe By Slug","description":"D-12/13 · find + merge duplicate Article rows that share a\n(publisher_id, slug). Keeps the oldest row (most accumulated FKs),\nrewrites every dependent FK on the 10 child tables to point at the\nkeeper, then deletes the duplicates. Idempotent: a re-run after a\nclean pass reports zero groups.\n\nOnce this runs to zero, the matching alembic migration that adds a\nUNIQUE constraint on (publisher_id, slug) can apply without error.","operationId":"admin_articles_dedupe_by_slug_v1_admin_articles_dedupe_by_slug_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DedupeArticlesBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Dedupe By Slug V1 Admin Articles Dedupe By Slug Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/rerender-all-done":{"post":{"summary":"Admin Rerender All Done","description":"Bulk re-enqueue articles whose latest job is `done`. Used to\nbackfill after a rendering-pipeline bug (e.g. the silenceremove\nchop-after-first-pause issue) where every cached WAV was wrong\nbut the DB shows everything as `done`. Combined with a cache_key\nversion bump (see cache.py `_CACHE_VERSION`), each re-render\nbypasses the corrupt cache and writes a fresh WAV.","operationId":"admin_rerender_all_done_v1_admin_articles_rerender_all_done_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":200,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Rerender All Done V1 Admin Articles Rerender All Done Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/rerender-cancelled":{"post":{"summary":"Admin Rerender Cancelled Batch","description":"Bulk re-enqueue articles whose latest render-job ended in\n'cancelled'. Targets the May 6 curate-and-skip backlog. Optional\nvertical filter so the operator can drain one domain at a time\nwithout melting the worker.","operationId":"admin_rerender_cancelled_batch_v1_admin_articles_rerender_cancelled_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Rerender Cancelled Batch V1 Admin Articles Rerender Cancelled Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/backfill-intake":{"post":{"summary":"Admin Publishers Backfill Intake","description":"Unblock cold-outreach publishers whose articles were stuck at\n``render_status=skipped_paywall``. For each slug we look at the most\nrecent ``max_articles_per_publisher`` articles, re-fetch each\nsource URL, re-evaluate the paywall heuristic against the fresh\nbody, and (when public) flip ``is_public_post=True``, refresh\nbody_text, and enqueue a render job.\n\nReturns per-publisher counts: ``scanned``, ``flipped_public``,\n``enqueued``, ``still_paywalled``, ``fetch_failed``,\n``no_articles``, ``no_intake_source``.\n\nIdempotency: publishers that already have >=1 ``done`` render job\nare skipped entirely (they're not actually blocked — the fire-batch\nfailure is something else and an operator should investigate).","operationId":"admin_publishers_backfill_intake_v1_admin_publishers_backfill_intake_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_BackfillIntakeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publishers Backfill Intake V1 Admin Publishers Backfill Intake Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{token}/spotify/seed-verticals":{"post":{"summary":"Listener Spotify Seed Verticals","description":"Pull the listener's Spotify top-artists + saved-podcast-shows,\nmap genres + show descriptions to Storyflo verticals, and apply\nthe highest-weight verticals to their `verticals` filter. Return\nthe mix so the frontend can show 'we picked these for you' UI.\n\nHonest behavior: we OVERLAY (not overwrite) the existing filter.\nA listener who already chose ['tech'] manually will end up with\nthe union of their pick + the spotify-derived ones. Apply on a\nfresh signup for max personalization; idempotent on repeat calls.","operationId":"listener_spotify_seed_verticals_v1_listener__token__spotify_seed_verticals_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Spotify Seed Verticals V1 Listener  Token  Spotify Seed Verticals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/youtube-music/oauth/start":{"get":{"summary":"Youtube Music Oauth Start","description":"Returns the Google OAuth URL with `youtube.readonly` +\n`youtube.force-ssl` scopes (read playlists, read recently watched).\nDisabled until YT_MUSIC_ENABLED=1 + YT_MUSIC_CLIENT_ID set on Fly.\n\nReturns JSON with status (not HTTPException) when gated, so Sentry\ndoesn't log every gate-hit as a runtime error.","operationId":"youtube_music_oauth_start_v1_youtube_music_oauth_start_get","parameters":[{"name":"listener_token","in":"query","required":true,"schema":{"type":"string","title":"Listener Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Youtube Music Oauth Start V1 Youtube Music Oauth Start Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/opml":{"get":{"summary":"Listener Opml Export","description":"Export the listener's vertical subscriptions + the per-vertical\nStoryflo RSS feeds as standard OPML. Drops into Feedly / NetNewsWire /\nInoreader without any conversion. Mirror of the existing /admin/intake/\nopml import.","operationId":"listener_opml_export_v1_listeners__token__opml_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/backfill-vertical":{"post":{"summary":"Admin Backfill Publisher Vertical","description":"Classify any publisher with a NULL vertical using domain map +\nkeyword fallback on their most-recent article. Idempotent — skips\nrows already classified. Returns per-vertical counts after the run.","operationId":"admin_backfill_publisher_vertical_v1_admin_publishers_backfill_vertical_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":500,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Backfill Publisher Vertical V1 Admin Publishers Backfill Vertical Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/substack-curated":{"post":{"summary":"Admin Substack Curated","description":"Register the operator-curated CURATED_SUBSTACK_SLUGS list as\nshadow publishers + ACTIVE IntakeSource rows. Idempotent — re-runs\npick up new slugs added to the list without duplicating existing\nrows or re-activating already-active sources beyond what's needed.\n\nPicked over the leaderboard crawl when the priority is quality\n(high-sub target authors whose audiences overlap Storyflo's\nlistener ICP) rather than breadth (25-per-category coverage).","operationId":"admin_substack_curated_v1_admin_intake_substack_curated_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Substack Curated V1 Admin Intake Substack Curated Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/supply-sequence":{"post":{"summary":"Admin Supply Sequence","description":"Manually fire the full newsletter-supply expansion sequence:\ncurated Substack list → leaderboard crawl (mapped) → wide-net\ncrawl (unmapped) → apex-domain catalog. Auto-fires daily via the\ndaily-brief worker; this endpoint is for ad-hoc operator runs.","operationId":"admin_supply_sequence_v1_admin_intake_supply_sequence_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Supply Sequence V1 Admin Intake Supply Sequence Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/substack-subcategories":{"post":{"summary":"Admin Substack Subcategories","description":"Crawl Substack subcategory leaderboards (B2 of the 5K-supply\nplan). For each top-level category, walks every subcategory's\n/all endpoint with pagination + dedupe.","operationId":"admin_substack_subcategories_v1_admin_intake_substack_subcategories_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Substack Subcategories V1 Admin Intake Substack Subcategories Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/beehiiv-discover":{"post":{"summary":"Admin Beehiiv Discover","description":"Run the Beehiiv discoverer (B3 of the 5K-supply plan). Scrapes\nhttps://www.beehiiv.com/discover when allowed by robots.txt, falls\nback to a curated seed list when the page is JS-rendered or\nrobots-blocked.","operationId":"admin_beehiiv_discover_v1_admin_intake_beehiiv_discover_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Beehiiv Discover V1 Admin Intake Beehiiv Discover Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/substack-gap-fill":{"post":{"summary":"Admin Substack Gap Fill","description":"LLM gap-fill via Anthropic Haiku 4.5 (B4 of the 5K-supply plan).\nAsks the model, per vertical, to name big-subs Substack newsletters\nwe don't already track. Each suggestion is verified against the\npublic RSS feed before insertion so hallucinated slugs get filtered.\n\nBudget guards: 50 Haiku calls/day (Upstash UTC bucket), 100 newly-\naccepted sources per vertical. Both configurable via env.","operationId":"admin_substack_gap_fill_v1_admin_intake_substack_gap_fill_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Substack Gap Fill V1 Admin Intake Substack Gap Fill Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/apex-catalog":{"post":{"summary":"Admin Apex Catalog","description":"Register the curated apex-domain newsletter catalog\n(non-Substack big names: 404 Media, Platformer, The Atlantic,\nQuanta, Bloomberg Markets, etc.) as Publisher + active\nIntakeSource rows. Catalog lives in app/apex_newsletter_catalog.py.\n\nUsed in tandem with the Substack crawlers when the operator wants\nbreadth beyond what Substack indexes. Each entry's RSS URL was\nlive-verified at registration time.","operationId":"admin_apex_catalog_v1_admin_intake_apex_catalog_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Apex Catalog V1 Admin Intake Apex Catalog Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/substack-crawl-all":{"post":{"summary":"Admin Substack Crawl All","description":"Wide-net crawl across EVERY Substack category (32 total),\nmapped + unmapped. Unmapped-category publishers land with\nvertical=NULL and need operator triage. Designed for the 5K-author\nbaseline target. Polite — 1 req/sec across categories so a single\nrun takes ~32s plus DB writes; safe to schedule weekly.","operationId":"admin_substack_crawl_all_v1_admin_intake_substack_crawl_all_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubstackCrawlAllBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Substack Crawl All V1 Admin Intake Substack Crawl All Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/substack-crawl":{"post":{"summary":"Admin Substack Crawl","description":"Crawl substack.com/leaderboard for top newsletters per category\nand register each one as a shadow Publisher + IntakeSource. Idempotent\n— re-running picks up new entries without duplicating rows. Returns\ncounts so the operator can confirm meaningful supply growth on each\nrun. Polite to Substack: ~1 req/sec, real User-Agent with opt-out\ncontact, only hits public leaderboard pages.","operationId":"admin_substack_crawl_v1_admin_intake_substack_crawl_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SubstackCrawlBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Substack Crawl V1 Admin Intake Substack Crawl Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/vertical":{"patch":{"summary":"Admin Patch Publisher Vertical","description":"Operator override for vertical classification when the\nauto-classifier picked the wrong bucket. Pass vertical=null to\nclear the tag and let the next ingest re-classify. Accepted slugs\nare the canonical 7 from vertical_classifier.ALL_VERTICALS.","operationId":"admin_patch_publisher_vertical_v1_admin_publishers__tenant_slug__vertical_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VerticalPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Publisher Vertical V1 Admin Publishers  Tenant Slug  Vertical Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/csp/report":{"post":{"summary":"Csp Report","description":"Receives Content-Security-Policy-Report-Only violations from\nthe browser. Rate-limited at the network edge; any payload above\n16KB is rejected. We log + sample to Sentry breadcrumbs so the\noperator can see real-world violations before flipping to\nenforcing CSP.","operationId":"csp_report_v1_csp_report_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/audio/mos":{"post":{"summary":"Submit Mos","description":"Listener Mean Opinion Score after a completed listen. Stored\nin admin_audit_log under action='audio.mos.submit' so we can\naggregate by voice_id / provider without a new table for v0.\nOperator can graduate to a dedicated MOS table once data shape\nsettles.","operationId":"submit_mos_v1_audio_mos_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MosSurvey"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Submit Mos V1 Audio Mos Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/audio/mos-summary":{"get":{"summary":"Admin Mos Summary","description":"Aggregate MOS scores by voice_id over the window. Pulls from\nthe audit log so no schema dependency.","operationId":"admin_mos_summary_v1_admin_audio_mos_summary_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Mos Summary V1 Admin Audio Mos Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/lang/voice-map":{"get":{"summary":"Language Voice Map","description":"Public read of the language→voice map so the frontend can\nsurface 'this article will be narrated in <voice> (Spanish)'.","operationId":"language_voice_map_v1_lang_voice_map_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Language Voice Map V1 Lang Voice Map Get"}}}}}}},"/v1/admin/sponsorships":{"get":{"summary":"Admin List Sponsorships","operationId":"admin_list_sponsorships_v1_admin_sponsorships_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Sponsorships V1 Admin Sponsorships Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/sponsorships/{sponsorship_id}":{"patch":{"summary":"Admin Review Sponsorship","operationId":"admin_review_sponsorship_v1_admin_sponsorships__sponsorship_id__patch","parameters":[{"name":"sponsorship_id","in":"path","required":true,"schema":{"type":"string","title":"Sponsorship Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SponsorshipReview"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Review Sponsorship V1 Admin Sponsorships  Sponsorship Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/advertisers":{"post":{"summary":"Admin Create Advertiser","description":"Register an advertiser / brand partner and the verticals they want\nto reach. ``desired_verticals`` empty = run-of-network.","operationId":"admin_create_advertiser_v1_admin_advertisers_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdvertiserCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Create Advertiser V1 Admin Advertisers Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin List Advertisers","description":"List advertisers, newest first. Optional ``status`` filter and\n``vertical`` filter (advertisers whose desired_verticals include it,\nRON included).","operationId":"admin_list_advertisers_v1_admin_advertisers_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Advertisers V1 Admin Advertisers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/advertisers/{advertiser_id}":{"get":{"summary":"Admin Get Advertiser","operationId":"admin_get_advertiser_v1_admin_advertisers__advertiser_id__get","parameters":[{"name":"advertiser_id","in":"path","required":true,"schema":{"type":"string","title":"Advertiser Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Get Advertiser V1 Admin Advertisers  Advertiser Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Admin Patch Advertiser","operationId":"admin_patch_advertiser_v1_admin_advertisers__advertiser_id__patch","parameters":[{"name":"advertiser_id","in":"path","required":true,"schema":{"type":"string","title":"Advertiser Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdvertiserPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Advertiser V1 Admin Advertisers  Advertiser Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/advertisers/register-interest":{"post":{"summary":"Public Advertiser Intake","description":"Public / partner intake — a brand registers interest in sponsoring\none or more verticals. Lands as ``pending`` for operator review; not\nsurfaced in the recommender until promoted to ``active``.","operationId":"public_advertiser_intake_v1_advertisers_register_interest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AdvertiserIntake"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Public Advertiser Intake V1 Advertisers Register Interest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/matchmaking/advertisers":{"get":{"summary":"Admin Matchmaking Advertisers","description":"Active advertisers who want ``vertical``, ranked by explicit-match\nthen CPM then budget. ``include_ron=false`` hides run-of-network\nadvertisers (those with no vertical preference).","operationId":"admin_matchmaking_advertisers_v1_admin_matchmaking_advertisers_get","parameters":[{"name":"vertical","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":80,"title":"Vertical"}},{"name":"include_ron","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Ron"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Matchmaking Advertisers V1 Admin Matchmaking Advertisers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/matchmaking/recommend":{"get":{"summary":"Admin Matchmaking Recommend","description":"Suggested sponsors (advertisers + creatives) for a piece of content\nor a vertical. Pass ``article_id`` OR ``vertical``. When both are\ngiven, ``article_id`` wins (its resolved vertical is authoritative).","operationId":"admin_matchmaking_recommend_v1_admin_matchmaking_recommend_get","parameters":[{"name":"article_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Article Id"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Matchmaking Recommend V1 Admin Matchmaking Recommend Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/matchmaking/advertisers":{"get":{"summary":"Publisher Matchmaking Advertisers","description":"Publisher/author-facing: advertisers interested in the verticals\nthis publisher actually writes in — so authors see monetization fit.\n\nThe publisher's verticals are derived from the distinct\n``Article.vertical`` values across their catalogue. Advertisers are\nde-duplicated across verticals; each carries the list of the\npublisher's verticals it matched.","operationId":"publisher_matchmaking_advertisers_v1_publisher__tenant_slug__matchmaking_advertisers_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Limit"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Matchmaking Advertisers V1 Publisher  Tenant Slug  Matchmaking Advertisers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/voices":{"get":{"summary":"List Voices","description":"Public voice catalog — drives the voice picker on /listen/preferences.\n\nDERIVED FROM ``app/voices.py``, deliberately. This used to be a\nhard-coded list maintained beside the real registry, and the two drifted\nuntil they shared exactly two entries out of ten:\n\n  * The catalog advertised ``kokoro-warm``, ``kokoro-clear``, four\n    ``openai-*`` and two ``eleven-*`` ids that exist in NO registry, so\n    ``GET /v1/voices/{id}/sample`` 404'd on eight of the ten voices it\n    had just offered — you could not audition what you were asked to pick.\n  * It also HID ten real voices, including the named show hosts (Jessica,\n    Chloe, Wit, Brock, Dex, Sage) and both Spanish voices. Listeners could\n    not choose the voices the platform actually ships.\n  * Its ``tier`` field sold ``plus`` and ``pro`` voices the router will\n    never reach: ``router.py`` refuses to let a metered provider LEAD a\n    chain (``test_no_chain_leads_with_a_metered_provider``), and\n    ``ELEVENLABS_MONTHLY_FREE_CHARS`` is 0. Picking \"Rachel (Pro)\"\n    silently rendered Kokoro instead — the old docstring said so outright.\n\nOne source of truth removes all three at once. Every registry voice has a\nself-hosted path (Kokoro, Piper or StyleTTS2), so every voice offered here\nis genuinely free to render; ``self_hosted`` states that rather than\nimplying a paid ladder that does not exist.","operationId":"list_voices_v1_voices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Voices V1 Voices Get"}}}}}}},"/v1/listeners/{token}/voice":{"patch":{"summary":"Listener Set Voice","description":"Set the listener's voice favorite. Render router applies this\nwhen no per-publisher override exists.","operationId":"listener_set_voice_v1_listeners__token__voice_patch","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ListenerVoiceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Set Voice V1 Listeners  Token  Voice Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/saved-searches":{"post":{"summary":"Listener Create Saved Search","operationId":"listener_create_saved_search_v1_listeners__token__saved_searches_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SavedSearchCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Create Saved Search V1 Listeners  Token  Saved Searches Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Listener List Saved Searches","operationId":"listener_list_saved_searches_v1_listeners__token__saved_searches_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener List Saved Searches V1 Listeners  Token  Saved Searches Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/saved-searches/{search_id}":{"delete":{"summary":"Listener Delete Saved Search","operationId":"listener_delete_saved_search_v1_listeners__token__saved_searches__search_id__delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"search_id","in":"path","required":true,"schema":{"type":"string","title":"Search Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Delete Saved Search V1 Listeners  Token  Saved Searches  Search Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listener/{token}/cohorts":{"get":{"summary":"Listener Get Cohorts","description":"0086 · engagement-loop · read the listener's persisted cohort tags.\n\nReturns whatever the daily-brief worker last wrote — does NOT\nrecompute on-demand, since on-demand computation across active\nlisteners would make this endpoint trivial to abuse for DoS. The\ndaily tick is the only writer.","operationId":"listener_get_cohorts_v1_listener__token__cohorts_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Get Cohorts V1 Listener  Token  Cohorts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/feed/personalized":{"get":{"summary":"Personalized Feed","description":"Re-rank the trending feed for this listener based on listening\nhistory → vertical preference scoring.\n\nFor each vertical we count completed plays in the last 60 days and\nconvert to a preference weight. We then pull the top trending\narticles + boost ones in preferred verticals.","operationId":"personalized_feed_v1_listeners__token__feed_personalized_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Personalized Feed V1 Listeners  Token  Feed Personalized Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/feed/pinned":{"get":{"summary":"Listener Pinned Feed","description":"Pinned-articles rail for the /listen/queue page (Stream-2b).\n\nReturns ``{\"items\": [...]}`` mirroring the frontend ``QueueItem``\nshape (defined in ``app/listen/queue/page.tsx``) so the pinned\nrail renders with the same card component used for the\nsubscription queue. As of 2026-06-04 each item carries\n``summary`` / ``listen_seconds`` / ``publisher_icon_url`` /\n``audio_status`` / ``audio_url`` / ``created_at`` in addition to\nthe original ``pinned_at`` / ``position`` / ``audio_ready`` legacy\nfields (kept for back-compat with pre-2026-06-04 consumers).\n\nSee ``app/listener_pins.py`` for business rules (50-pin cap,\nidempotency, slug-not-id resolution).","operationId":"listener_pinned_feed_v1_listeners__token__feed_pinned_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Pinned Feed V1 Listeners  Token  Feed Pinned Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/listeners/{token}/feed/pinned/{article_slug}":{"post":{"summary":"Listener Pin Article","description":"Pin an article. Idempotent on (listener, slug).\n\nBody optional ``{\"position\": int}`` to set explicit reorder slot.\nReturns ``{\"ok\": true, \"pinned_at\": \"...\"}`` on success, 404 on\nunknown article slug, 409 ``{ok: false, reason: 'pin_limit'}``\nif the listener is already at the 50-pin cap.","operationId":"listener_pin_article_v1_listeners__token__feed_pinned__article_slug__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/_PinBody"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Pin Article V1 Listeners  Token  Feed Pinned  Article Slug  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Listener Unpin Article","description":"Unpin an article. Idempotent — no error if not pinned.\n\nReturns ``{\"ok\": true}``.","operationId":"listener_unpin_article_v1_listeners__token__feed_pinned__article_slug__delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"article_slug","in":"path","required":true,"schema":{"type":"string","title":"Article Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Listener Unpin Article V1 Listeners  Token  Feed Pinned  Article Slug  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agents/register":{"post":{"summary":"Agent Register","description":"Public endpoint — anyone can register an agent. Returns an\nAPI key ONCE; we hash + persist immediately and never show the\nplaintext again. Operator-controlled `active` flag remains true\nby default (we'll tighten if abuse shows up).","operationId":"agent_register_v1_agents_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AgentRegister"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Register V1 Agents Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agents/me":{"get":{"summary":"Agent Me","description":"Bearer-authenticated read of the calling agent's registration\n+ lifetime stats.","operationId":"agent_me_v1_agents_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Me V1 Agents Me Get"}}}}}}},"/v1/agents/me/usage":{"get":{"summary":"Agent Me Usage","description":"Per-agent usage + recommender-earnings dashboard payload.\n\nv0 reports lifetime calls (atomic counter on the row) + a window\nsnapshot from tenant_metrics. Recommender earnings folds in\nonce LedgerEntry.agent_id lands (track in #167 follow-up).","operationId":"agent_me_usage_v1_agents_me_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Me Usage V1 Agents Me Usage Get"}}}}}}},"/v1/admin/agents":{"get":{"summary":"Admin Agents List","operationId":"admin_agents_list_v1_admin_agents_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Agents List V1 Admin Agents Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/mcp/tool-usage":{"get":{"summary":"Admin Mcp Tool Usage","description":"Aggregate MCP ``tools/call`` audit rows over the last ``days``.\n\nShape::\n\n    {\n      \"window_days\": <int>,\n      \"total_calls\": <int>,\n      \"by_tool\": [\n        {\"tool_name\", \"calls\", \"errors\", \"error_rate\",\n         \"p50_latency_ms\", \"p95_latency_ms\"},\n        ...\n      ],\n      \"by_source\": [\n        {\"caller_source\", \"calls\"},\n        ...\n      ],\n      \"top_callers\": [\n        {\"caller_id\", \"calls\"},\n        ...\n      ],\n    }\n\nReturns the empty shape when the audit table is missing (pre-0161\ndeploys) — the FE renders \"no data yet\" rather than crashing.","operationId":"admin_mcp_tool_usage_v1_admin_mcp_tool_usage_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"top_callers_limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Top Callers Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Mcp Tool Usage V1 Admin Mcp Tool Usage Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/cost-per-render":{"get":{"summary":"Admin Cost Per Render","description":"Approximate cost per successful render, broken down by voice\nfamily × vertical × day. Voice prefix encodes provider:\n'kokoro-*' / 'openai-*' / 'eleven-*'. Cheap aggregation over the\nrender_jobs + articles tables — no per-call billing trace yet,\nso this is an approximation, not exact spend.","operationId":"admin_cost_per_render_v1_admin_cost_per_render_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Cost Per Render V1 Admin Cost Per Render Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/slo":{"get":{"summary":"Admin Slo","description":"Service-level objective dashboard — surface-by-surface error\nbudget burn against the monthly target. Targets are conservative\nstarting points; tune as the data lands.","operationId":"admin_slo_v1_admin_slo_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Slo V1 Admin Slo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/tenant-metrics":{"get":{"summary":"Admin Tenant Metrics","description":"Per-tenant call snapshot — top 50 publishers / listeners /\nagents by 60m call count. In-memory + per-process; resets on\ndeploy.","operationId":"admin_tenant_metrics_v1_admin_tenant_metrics_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Tenant Metrics V1 Admin Tenant Metrics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/saved-search/digest":{"post":{"summary":"Admin Saved Search Digest","description":"Manually fire the saved-search digest worker tick (sprint κ #2).\nCron triggers it inside cache_prerender_worker every 10min, but\nthe operator may want to force a sweep after editing rows.","operationId":"admin_saved_search_digest_v1_admin_saved_search_digest_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Saved Search Digest V1 Admin Saved Search Digest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/prerender/burst":{"post":{"summary":"Admin Trigger Prerender Burst","description":"Manually fire the trending top-N burst, ignoring the once-per-day\ngate (still honors the daily-spend cap). Used when the operator\nwants to force a refresh, or to backfill after a config tweak.","operationId":"admin_trigger_prerender_burst_v1_admin_prerender_burst_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Trigger Prerender Burst V1 Admin Prerender Burst Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/prerender/status":{"get":{"summary":"Admin Prerender Status","operationId":"admin_prerender_status_v1_admin_prerender_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Prerender Status V1 Admin Prerender Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{slug}/voice":{"patch":{"summary":"Admin Set Publisher Voice","description":"Set or clear a publisher's preferred voice. When set, every\nsubsequent render job for the publisher uses this voice_id.","operationId":"admin_set_publisher_voice_v1_admin_publishers__slug__voice_patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherVoiceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Set Publisher Voice V1 Admin Publishers  Slug  Voice Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{slug}/known-books":{"post":{"summary":"Admin Set Publisher Known Books","description":"Replace a publisher's known-books list + author flag. Idempotent.\n\nUsed to enforce per-author book-IP after a Spotify takedown — operator\nseeds the publisher row with the author's published titles so the\nbook-content filter blocks any newsletter that mentions them.","operationId":"admin_set_publisher_known_books_v1_admin_publishers__slug__known_books_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherKnownBooksUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Set Publisher Known Books V1 Admin Publishers  Slug  Known Books Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin Get Publisher Known Books","description":"Read current per-author book-IP state for a publisher.","operationId":"admin_get_publisher_known_books_v1_admin_publishers__slug__known_books_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Get Publisher Known Books V1 Admin Publishers  Slug  Known Books Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/highlight":{"get":{"summary":"Get Article Highlight","description":"Public read of the LLM-picked highlight reel for an article.\nNo auth — the clip URL is meant for social-share embeds (Twitter\naudio cards, Reddit native players, etc.) where adding listener\nauth would defeat the share funnel.\n\nReturns 404 when the article hasn't been clipped yet (worker hasn't\nrun, listen_seconds too short, daily Haiku cap exhausted, etc.).\nClients should treat the 404 as \"no highlight available\" not as\na broken article.","operationId":"get_article_highlight_v1_articles__slug__highlight_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Article Highlight V1 Articles  Slug  Highlight Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/chapters":{"get":{"summary":"Get Article Chapters","description":"Public read of the chapter list for an article. Lazily generates\n+ persists if the column is empty so we don't have to backfill the\nwhole corpus in one go.","operationId":"get_article_chapters_v1_articles__slug__chapters_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Article Chapters V1 Articles  Slug  Chapters Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/search/metrics":{"get":{"summary":"Admin Search Metrics","description":"Live external-search metrics: per-provider call/hit/error counts,\nP95 latency, hit rate. In-memory + per-process — resets on deploy.","operationId":"admin_search_metrics_v1_admin_search_metrics_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Search Metrics V1 Admin Search Metrics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/cluster":{"get":{"summary":"Get Article Cluster","description":"Cross-publisher cluster view — returns up to 5 other articles\nthat look like they cover the same news. v0 uses Postgres\nsimilarity on title (pg_trgm) + same vertical + same week.\n\nCheap-and-correct: for v0 a single SQL similarity query beats a\npgvector embedding pipeline. We can graduate to embeddings once\nwe have nightly batch infra running.","operationId":"get_article_cluster_v1_articles__slug__cluster_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Article Cluster V1 Articles  Slug  Cluster Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/articles/{slug}/og-image":{"get":{"summary":"Og Image Legacy Redirect","description":"Legacy OG card route — 301 to the Next.js file-convention renderer.\n\nExternal share-card crawlers (X / Twitter, LinkedIn, iMessage, Slack,\nDiscord) cache the og:image URL aggressively. Historically that meta\ntag pointed here on ``api.storyflo.com``; the frontend has since\nmoved the canonical renderer to the Next.js file convention at\n``https://www.storyflo.com/story/{slug}/opengraph-image`` (PR #87 — beige\nfallback + publisher-cover backdrop, single source of truth).\n\nFor any cache that still holds the old URL we hand back a permanent\nredirect so the next crawl naturally re-caches the new URL. New page\nrenders point ``<meta property=\"og:image\">`` directly at the new URL\nvia the frontend's ``generateMetadata`` flip — this handler exists\npurely to drain the long-tail of cached embeds.","operationId":"og_image_legacy_redirect_v1_articles__slug__og_image_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agents/openai-tools.json":{"get":{"summary":"Openai Function Calling Spec","description":"D-3.2 · OpenAI function-calling tool spec.\n\nDrops the same MCP tool catalog out as an OpenAI tools=[…] array\nso any chat.completions caller can use Storyflo without speaking\nMCP. The MCP `inputSchema` block is already JSON Schema, which\nis exactly what OpenAI's `parameters` field wants — we just\nrewrap as `{type: \"function\", function: {…}}`.","operationId":"openai_function_calling_spec_v1_agents_openai_tools_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Openai Function Calling Spec V1 Agents Openai Tools Json Get"}}}}}}},"/v1/agents/revenue-share":{"get":{"summary":"Agent Revenue Share Summary","description":"D-3.5 · public summary of x402 / agent-attributed revenue.\n\nReturns the total micros routed through agent-bearing channels\nin the window and the running 70/20/10 split. Public read so\nwe can surface the live number on /agents.","operationId":"agent_revenue_share_summary_v1_agents_revenue_share_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Revenue Share Summary V1 Agents Revenue Share Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/google/billing-status":{"get":{"summary":"Admin Google Billing Status","description":"Aggregate Google Cloud credit + spend + per-surface daily usage\nsnapshot. Drives the cron-fired Telegram alerts.","operationId":"admin_google_billing_status_v1_admin_google_billing_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Google Billing Status V1 Admin Google Billing Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/google/billing-check":{"post":{"summary":"Admin Google Billing Check","description":"Probe billing thresholds + fire any Telegram alerts on first\ncrossing today. Idempotent: re-runs no-op until the next UTC\nmidnight.","operationId":"admin_google_billing_check_v1_admin_google_billing_check_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Google Billing Check V1 Admin Google Billing Check Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pagespeed/probe":{"post":{"summary":"Admin Pagespeed Probe","description":"Run the PageSpeed Insights probe set + persist results. By\ndefault probes the configured URL list (storyflo.com mobile +\ndesktop). FREE — no quota.","operationId":"admin_pagespeed_probe_v1_admin_pagespeed_probe_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PageSpeedProbeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pagespeed Probe V1 Admin Pagespeed Probe Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pagespeed/recent":{"get":{"summary":"Admin Pagespeed Recent","description":"Recent PageSpeed probe rows for charting / dashboard surfaces.","operationId":"admin_pagespeed_recent_v1_admin_pagespeed_recent_get","parameters":[{"name":"url","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},{"name":"strategy","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Strategy"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pagespeed Recent V1 Admin Pagespeed Recent Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pagespeed/summary":{"get":{"summary":"Admin Pagespeed Summary","description":"Aggregated dashboard payload: latest per (url, strategy), 7-day\ndeltas vs anchor row, Lighthouse color-band counts over the window,\n+ mobile-vs-desktop comparison strip + sparkline trend series.","operationId":"admin_pagespeed_summary_v1_admin_pagespeed_summary_get","parameters":[{"name":"url","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pagespeed Summary V1 Admin Pagespeed Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/translate":{"post":{"summary":"Admin Translate","description":"On-demand Cloud Translate v2 call. Caller pays out of the daily\nchar cap. Returns 503 when the daily cap is exhausted.","operationId":"admin_translate_v1_admin_translate_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TranslateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Translate V1 Admin Translate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/google/diagnostics":{"get":{"summary":"Admin Google Diagnostics","description":"Probe which Google auth mode is wired up — service-account\nJSON key, user-account ADC, or YouTube refresh-token OAuth. Pure\nenv + filesystem check, no network calls. Used by the agent to\nconfirm credentials landed after operator setup.","operationId":"admin_google_diagnostics_v1_admin_google_diagnostics_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Google Diagnostics V1 Admin Google Diagnostics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/search-console/sites":{"get":{"summary":"Admin Search Console Sites","description":"List verified Search Console properties on the configured\nrefresh token. Returns 503 cleanly when the surface isn't wired.","operationId":"admin_search_console_sites_v1_admin_search_console_sites_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Search Console Sites V1 Admin Search Console Sites Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/ga/report":{"post":{"summary":"Admin Ga Report","description":"Run a GA4 ``runReport`` over the configured property. Returns\n503 cleanly when the surface isn't wired.","operationId":"admin_ga_report_v1_admin_ga_report_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_GAReportBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Ga Report V1 Admin Ga Report Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/gemini/generate":{"post":{"summary":"Admin Gemini Generate","description":"One-shot Gemini ``:generateContent`` smoke. Returns 503 cleanly\nwhen the surface isn't wired.","operationId":"admin_gemini_generate_v1_admin_gemini_generate_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_GeminiGenerateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Gemini Generate V1 Admin Gemini Generate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/scrub-leaked-slugs":{"post":{"summary":"Admin Scrub Leaked Slugs","description":"PII · re-slug articles whose slug literally contains an\noperator-personal email fragment. The slug was derived from the\nforward preamble before the 2026-05-12 intake fix and is now part\nof every share URL + OG card + RSS guid. Body scrub already ran;\nthis fixes the URL-layer leak.\n\nSaves the OLD slug into ``raw_json['previous_slugs']`` so a future\nredirect resolver can map old shares to the new slug.","operationId":"admin_scrub_leaked_slugs_v1_admin_articles_scrub_leaked_slugs_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ScrubLeakedSlugsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Scrub Leaked Slugs V1 Admin Articles Scrub Leaked Slugs Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/youtube/upload-top-trending":{"post":{"summary":"Admin Youtube Upload Top Trending","description":"D-6 · pick the top-N trending articles in the last ``days``\nwindow and upload each as a YouTube video via the dispatcher.\n\nYouTube quota is ~6 uploads/day (10K units / 1600 per upload), so\nthe default top_n=3 leaves headroom for the existing daily-brief\n+ highlight reel uploads to coexist. Skips articles that already\nhave a `__youtube__.article` video_id stamped.\n\nOperator can crontab this endpoint daily, or fire manually.","operationId":"admin_youtube_upload_top_trending_v1_admin_youtube_upload_top_trending_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_YouTubeTrendingBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Youtube Upload Top Trending V1 Admin Youtube Upload Top Trending Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agents/leaderboard":{"get":{"summary":"Agent Leaderboard","description":"D-7 · public leaderboard of MCP / agent integrators ranked by\npaid x402 volume + recommender revenue share in the window.\n\nReturns the top ``limit`` agents by total recommender micros\nreceived via the 20% LedgerKind.licensor_payable leg with\n``agent_id`` set. Public read — no API key or contact_email\nsurfaced. Drives the public marketing for the agent-recommender\nrevenue loop on /agents.","operationId":"agent_leaderboard_v1_agents_leaderboard_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Leaderboard V1 Agents Leaderboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/v1":{"get":{"summary":"Mcp Sse Stream","description":"MCP streamable-http requires a GET endpoint that opens a\nserver-sent events channel. Claude Desktop's custom connector\nprobes this BEFORE attempting OAuth — without it the UI shows\n'Couldn't reach the MCP server'.\n\nWe return a short keepalive stream and close. The actual JSON-RPC\ntraffic still flows over POST /mcp/v1; this exists purely so the\nclient's GET-side probe sees a 200 + text/event-stream and lets\nthe user proceed to OAuth.","operationId":"mcp_sse_stream_mcp_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Mcp Jsonrpc","operationId":"mcp_jsonrpc_mcp_v1_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"summary":"Mcp Cors Preflight","description":"CORS preflight for browser-based MCP clients (claude.ai web,\nOpenAI Apps SDK). The response itself contains no body — only the\nheaders that authorize the cross-origin POST.","operationId":"mcp_cors_preflight_mcp_v1_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/mcp/sse":{"get":{"summary":"Mcp Sse Handshake","description":"Open a long-lived SSE channel for the MCP http+sse transport.\n\nPer the 2024-11-05 spec the first frame is an ``event: endpoint``\npointing the client at the POST URL it should send JSON-RPC to.\nThe connection then stays open; ``POST /v1/mcp/messages`` enqueues\nJSON-RPC responses onto this stream as ``event: message`` frames.\n\nAnonymous handshake is allowed when ``MCP_ANON_DISCOVERY_ENABLED``\nis on — the client still has to pass a bearer token before\n``tools/call`` will dispatch, but the SSE channel itself can be\nopened so discovery probes from MCP directories work without\nOAuth.","operationId":"mcp_sse_handshake_v1_mcp_sse_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"summary":"Mcp Sse Cors Preflight","description":"CORS preflight for browser-mediated MCP clients hitting the\nSSE transport (ChatGPT Apps web UI, future browser-side wrappers).\nMirrors the allowed headers on ``/mcp/v1`` so the same client\nbootstrap code works against either transport.","operationId":"mcp_sse_cors_preflight_v1_mcp_sse_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/mcp/messages":{"post":{"summary":"Mcp Sse Messages","description":"Receive a JSON-RPC 2.0 message for an open SSE session.\n\nReturns 202 Accepted immediately; the JSON-RPC response is pushed\nback over the SSE channel keyed by ``sessionId``. Bearer-token\nenforcement matches POST /mcp/v1: anonymous discovery methods are\nallowed when the kill-switch is on, ``tools/call`` always requires\na valid bearer.","operationId":"mcp_sse_messages_v1_mcp_messages_post","parameters":[{"name":"sessionId","in":"query","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"title":"Sessionid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"summary":"Mcp Sse Cors Preflight","description":"CORS preflight for browser-mediated MCP clients hitting the\nSSE transport (ChatGPT Apps web UI, future browser-side wrappers).\nMirrors the allowed headers on ``/mcp/v1`` so the same client\nbootstrap code works against either transport.","operationId":"mcp_sse_cors_preflight_v1_mcp_messages_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"summary":"Oauth Metadata","description":"RFC 8414 — OAuth 2.0 Authorization Server Metadata. MCP / OpenAI\nApps / agent toolkits read this to auto-configure.\n\nEndpoint host split: authorization_endpoint (browser-rendered consent\npage) lives on storyflo.com; token / register / revoke (programmatic\nJSON) live on api.storyflo.com. RFC 8414 doesn't require endpoints\nto share a host with the issuer — only the issuer field identifies\nthe authorization server (which is storyflo.com).","operationId":"oauth_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Metadata  Well Known Oauth Authorization Server Get"}}}}}}},"/oauth/register":{"post":{"summary":"Oauth Dynamic Client Register","description":"RFC 7591 Dynamic Client Registration. Returns a fresh client_id\n(and a client_secret only when token_endpoint_auth_method is NOT\n'none') so MCP clients can complete OAuth without prior coordination.\n\nL1 monetization attribution (2026-06-13): the registration captures\nwhere the agent came from so the operator can rank MCP-directory\ndistribution effort. Priority: ``?source=<slug>`` query param (when\na directory installs us with explicit attribution) → ``Origin``\nheader → ``Referer`` header → ``direct``/``unknown`` fallback. See\n``app/oauth_discovery_source.py`` for the canonical slug allowlist.","operationId":"oauth_dynamic_client_register_oauth_register_post","parameters":[{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Source"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DCRRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Oauth Dynamic Client Register Oauth Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/authorize":{"post":{"summary":"Oauth Authorize","description":"Authorization endpoint. Frontend collects user consent, then POSTs\nhere with the listener_token (proving the human is signed in) and\nPKCE challenge. Returns the auth code; frontend redirects the agent\nback to its redirect_uri with code + state.\n\nThe actual consent UI lives at /oauth/consent on the frontend; this\nendpoint is the server-side step that mints the code after the user\nclicks Authorize.","operationId":"oauth_authorize_oauth_authorize_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AuthorizeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Authorize Oauth Authorize Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"summary":"Oauth Token","description":"Token endpoint. Form-encoded per RFC 6749 §4.1.3. Supports\nauthorization_code, refresh_token, and client_credentials grants.\n\nAccepts client_id from EITHER the form body OR an Authorization:\nBasic header (RFC 6749 §2.3.1 says public clients may send client_id\nvia either; some clients like Anthropic's MCP custom-connector\nflow default to Basic auth even when the client was registered as\na 'public' client with no secret).","operationId":"oauth_token_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Token Oauth Token Post"}}}}}}},"/oauth/revoke":{"post":{"summary":"Oauth Revoke","description":"RFC 7009 token revocation. Always returns 200 — don't leak whether\na token existed.","operationId":"oauth_revoke_oauth_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Revoke Oauth Revoke Post"}}}}}}},"/v1/agent/articles":{"get":{"summary":"Agent Search Articles","description":"Agent action: search articles. Maps to the existing search +\nfeed endpoints, returning a clean shape an LLM can consume.","operationId":"agent_search_articles_v1_agent_articles_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Search Articles V1 Agent Articles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/market-context":{"get":{"summary":"Agent Market Context","description":"Agent action: Storyflo stories that match an actively traded\nevent contract on Kalshi and/or Polymarket. Each item carries\nqualitative signal tags + a link-out to the venue's own page for the\nlive numbers — never raw odds. Editorial sourcing surface, not data\nredistribution.\n\nServed from the ``market_context:v1`` (Kalshi) and\n``market_context:polymarket:v1`` (Polymarket) kv snapshots (PK\nlookups) that ``app/market_context.py`` / ``app/\nmarket_context_polymarket.py`` workers refresh — see those modules\nfor the matching + compliance posture. The Polymarket poller writes\nits row; this endpoint is what serves it (``source=polymarket|all``).","operationId":"agent_market_context_v1_agent_market_context_get","parameters":[{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Category"}},{"name":"signal","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Signal"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","maxLength":16,"description":"Market source: 'kalshi' (default, the CFTC-regulated DCM), 'polymarket' (the Polymarket prediction market), or 'all' to merge both snapshots. Each source is independently feature-flagged; a disabled source contributes no items.","default":"kalshi","title":"Source"},"description":"Market source: 'kalshi' (default, the CFTC-regulated DCM), 'polymarket' (the Polymarket prediction market), or 'all' to merge both snapshots. Each source is independently feature-flagged; a disabled source contributes no items."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Market Context V1 Agent Market Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/divergence-index":{"get":{"summary":"Divergence Index","description":"Public: the Storyflo Market-vs-Media Divergence Index — the top\nevents where prediction markets disagree with the press, ranked by the\nabsolute divergence between the market-implied probability and\nStoryflo's own news-narrative probability.\n\nOriginal analysis (the divergence + news-narrative numbers are computed\nby Storyflo). No auth — this is the citeable asset. Served from the\n``divergence_index:v1`` kv snapshot the worker refreshes; the read path\nprojects the public shape (raw market odds withheld unless the\ncounsel-gated flag is on). See ``app/divergence_index.py``.","operationId":"divergence_index_v1_divergence_index_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":25,"title":"Limit"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Divergence Index V1 Divergence Index Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/divergence-index/methodology":{"get":{"summary":"Divergence Index Methodology","description":"Public: the Divergence Index methodology (transparency = citeability).\nNo auth, no feature gate — the methodology is always readable so a\njournalist can verify how the metric is computed before citing it.","operationId":"divergence_index_methodology_v1_divergence_index_methodology_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Divergence Index Methodology V1 Divergence Index Methodology Get"}}}}}}},"/v1/market-reprice-cards":{"get":{"summary":"Market Reprice Cards","description":"Public: the \"market just repriced\" brief-cards — shareable cards for\nPolymarket event contracts that materially moved, produced off the free\nGamma API by ``app/market_reprice_cards.py``. Mirrors the divergence-index\npublic route: no auth, CDN-cacheable, served from the\n``market_reprice_cards:v1`` kv snapshot the worker refreshes.\n\nTAGS-ONLY: the read path projects each card through\n``market_reprice_cards.project_card_public`` which strips the internal raw\nodds / numeric move unless the counsel-gated\n``MARKET_REPRICE_CARDS_EXPOSE_RAW_ODDS`` flag is on — editorial surfaces\nnever see raw odds. Gated on ``MARKET_REPRICE_CARDS_ENABLED``: when the\nworker is off the route is a no-op returning an empty, warming payload\n(never a 5xx) so a crawler/CDN hit is cheap and cacheable.","operationId":"market_reprice_cards_v1_market_reprice_cards_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":25,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Market Reprice Cards V1 Market Reprice Cards Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/articles/{slug}":{"get":{"summary":"Agent Get Article","description":"Full article record for an agent. Returns title, body, audio_url,\ncover_image_url, listen_seconds.","operationId":"agent_get_article_v1_agent_articles__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Get Article V1 Agent Articles  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/articles/{slug}/audio":{"get":{"summary":"Agent Get Audio","description":"Resolve the playable audio URL for an article. Returns\n``{audio_url, duration_sec, mime}``. Listener context is implicit\nin the OAuth token's listener_token, so plus/pro listeners get the\nbare audio and free listeners get the stitched-with-pre-roll URL.","operationId":"agent_get_audio_v1_agent_articles__slug__audio_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Get Audio V1 Agent Articles  Slug  Audio Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/subscriptions":{"get":{"summary":"Agent List Subscriptions","operationId":"agent_list_subscriptions_v1_agent_subscriptions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent List Subscriptions V1 Agent Subscriptions Get"}}}}}},"post":{"summary":"Agent Subscribe Topic","description":"Create or update an agent-managed listener feed scoped to verticals.\nRequires authorization_code grant (listener_token must be present).","operationId":"agent_subscribe_topic_v1_agent_subscriptions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AgentSubscribeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Subscribe Topic V1 Agent Subscriptions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/digest":{"post":{"summary":"Agent Digest","description":"One-shot aggregation: pick top-N articles in the requested\nverticals over the last window, return list + audio URLs. The\nheaviest action — metered first.","operationId":"agent_digest_v1_agent_digest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AgentDigestRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Digest V1 Agent Digest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/faucet/claim":{"post":{"summary":"Agent Faucet Claim","description":"Alpha faucet — grant a 7-day Storyflo+ trial in exchange for one\n\"claim\" per listener per UTC day. The actual on-chain USDC transfer\non Base Sepolia is queued; the tier flip happens immediately so the\nlistener sees premium voices on their next render.\n\nIdempotent on ``(listener_token, claim_day)`` — duplicate clicks\nreturn the existing claim row's ``expires_at`` rather than minting\na new grant. ``ip_hash`` is captured for abuse triage but is not\na primary uniqueness key (a household sharing IP shouldn't\nblock legitimate claims from different listener_tokens).","operationId":"agent_faucet_claim_v1_agent_faucet_claim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_FaucetClaim"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Agent Faucet Claim V1 Agent Faucet Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/oauth/clients":{"post":{"summary":"Admin Register Oauth Client","description":"Register a new OAuth client. Operator hands the credentials to\nthe partner platform (Anthropic, OpenAI, Coinbase, custom). Secret\nis only shown ONCE in the response — there's no way to retrieve it.\n\nAuth: admin token (X-Storyflo-Email-Token). This endpoint mints\ncredentials that grant programmatic access to the platform, so it\nmust require the strongest credential — not the shared api_key\nthat is embedded in browser bundles and partner SDKs.","operationId":"admin_register_oauth_client_v1_admin_oauth_clients_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_OAuthClientCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Register Oauth Client V1 Admin Oauth Clients Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin List Oauth Clients","description":"Auth: admin token (X-Storyflo-Email-Token).","operationId":"admin_list_oauth_clients_v1_admin_oauth_clients_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Oauth Clients V1 Admin Oauth Clients Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render-queue/health":{"get":{"summary":"Admin Render Queue Health","description":"Ad-hoc inspection surface for the render-queue backlog alarm.\n\nThe same metrics dict the daily-brief tick computes every 30 min,\nserved on-demand so the operator can curl during an incident\nwithout waiting for the next polling interval. Calling this also\ntriggers the same Telegram-paging path (once per UTC hour) when a\nthreshold is exceeded, which is intentional: a human-driven probe\nof a wedged queue should not be quieter than the automated one.","operationId":"admin_render_queue_health_v1_admin_render_queue_health_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Render Queue Health V1 Admin Render Queue Health Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/render-queue/reap":{"post":{"summary":"Admin Render Queue Reap","description":"Manually trigger the zombie-job reaper during an incident.\n\nThe reaper also runs on every worker tick (rate-limited) and from\ninside the backlog alarm, but having a curl-able surface means an\noperator can force a sweep without redeploying the worker or\nwaiting for the next alarm fire. Body: ``{stale_minutes?: int}``.\nReturns the reaper result dict (``reset``, ``stale_threshold_minutes``).","operationId":"admin_render_queue_reap_v1_admin_render_queue_reap_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Render Queue Reap V1 Admin Render Queue Reap Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/dashboard/conversions":{"get":{"summary":"Publisher Dashboard Conversions","description":"Per-publisher conversion funnel: narrated articles, plays,\nunique listeners, and paywall click-throughs (the leading indicator\nthat storyflo is driving paid Substack subscriptions for this\nauthor). Auth via ?token= OR X-Storyflo-Publisher-Token header so\nthe page can deep-link from an email without exposing a header API.","operationId":"publisher_dashboard_conversions_v1_publisher__tenant_slug__dashboard_conversions_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}},{"name":"window","in":"query","required":false,"schema":{"type":"string","pattern":"^(7d|30d|90d|all)$","default":"30d","title":"Window"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Dashboard Conversions V1 Publisher  Tenant Slug  Dashboard Conversions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/backfill-pii-scrub":{"post":{"summary":"Admin Pii Backfill","description":"SEV-1 backfill — re-scrub stored articles whose body_text matched\nthe leak detector, enqueue fresh render jobs, surface affected\npublishers for outreach. See ``app.pii_backfill`` for the matcher +\nscrub semantics.\n\nBody params:\n  - dry_run (default True): report counts without writing\n  - limit (default 1000, max 5000): cap rows scanned per call\n  - also_delete_old_audio (default False): currently logged as TODO;\n    re-render writes the new audio to the same R2 key so listeners\n    get the clean version on next play anyway.","operationId":"admin_pii_backfill_v1_admin_intake_backfill_pii_scrub_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PIIBackfillBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pii Backfill V1 Admin Intake Backfill Pii Scrub Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/{slug}/re-sanitize":{"post":{"summary":"Admin Articles Re Sanitize","description":"Apply the current PII + newsletter-chrome sanitizer pipeline to\na single article's ``body_text`` and (optionally) enqueue a fresh\nrender-job so the listen catches up to the cleaned text.\n\nOperator path for the post-hoc \"MAY 14 / stray date stamp at top\nof body\" regression — articles ingested before the\nnewsletter-chrome strip landed still narrate the chrome literally.\nA single curl against this endpoint per affected slug rewrites the\nbody + flushes audio.","operationId":"admin_articles_re_sanitize_v1_admin_articles__slug__re_sanitize_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ReSanitizeArticleBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Re Sanitize V1 Admin Articles  Slug  Re Sanitize Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/{slug}/embeds":{"get":{"summary":"Admin Articles Embeds","description":"Return the structured embed list for an article — either the\ncached ``parsed_json['embeds']`` written by intake, or a freshly\ncomputed list when the article pre-dates the embed_detector\nrollout. Computed fallback is read-only (no DB write) so this stays\na safe inspect endpoint; the canonical persistence path is intake.","operationId":"admin_articles_embeds_v1_admin_articles__slug__embeds_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Embeds V1 Admin Articles  Slug  Embeds Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/intake/pii-sanitizer-scan":{"post":{"summary":"Admin Pii Sanitizer Scan","description":"Dry-run scan over every Article — count how many rows would be\ntouched if ``strip_forwarded_headers`` / ``sanitize_slug`` /\n``sanitize_chapters_json`` were applied. Read-only; never writes.\n\nOperator uses this AFTER the upstream sanitizer lands to confirm\nthat the prior one-shot backfill (``backfill-pii-scrub`` +\n``scrub-leaked-slugs``) caught everything. Healthy steady-state is\nzero for all three counters.","operationId":"admin_pii_sanitizer_scan_v1_admin_intake_pii_sanitizer_scan_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PIISanitizerScanBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pii Sanitizer Scan V1 Admin Intake Pii Sanitizer Scan Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/articles/rerender-stale-audio":{"post":{"summary":"Admin Articles Rerender Stale Audio","description":"Re-enqueue audio render jobs for articles whose ``body_text`` was\nrewritten (typically by the PII sanitizer scrub) after the canonical\naudio was already cached in R2. The audio in R2 still narrates the\npre-scrub text — we need fresh renders so listeners hear the cleaned\nbody.\n\nResolution per article:\n  * Pick the voice_id from the most-recent done RenderJob (the voice\n    the audio listeners are currently hearing). Falls back to the\n    publisher's preferred_voice_id, else \"kira\" — the platform\n    default and a member of VALID_VOICE_IDS.\n  * Use the article's current cleaned ``body_text``.\n  * Inherit ``speed`` from the most-recent done job; default 1.0.\n  * Enqueue with ``job_priority='low'`` so this backfill doesn't\n    starve foreground listener intake.\n\nIdempotent: an article is skipped if its most-recent done RenderJob\nalready rendered the current body_text (i.e. text matches).","operationId":"admin_articles_rerender_stale_audio_v1_admin_articles_rerender_stale_audio_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RerenderStaleBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Articles Rerender Stale Audio V1 Admin Articles Rerender Stale Audio Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/affected-publishers.csv":{"get":{"summary":"Admin Outreach Csv","description":"CSV export of publishers whose articles tripped the leak\ndetector, ranked by an operator-tunable priority score. Returned as\n``text/csv`` so the operator can pipe it straight into a spreadsheet\nor a mailmerge tool.","operationId":"admin_outreach_csv_v1_admin_outreach_affected_publishers_csv_get","parameters":[{"name":"top_n","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Top N"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/embed-stats":{"get":{"summary":"Publisher Embed Stats","description":"Per-publisher embed-badge play analytics, grouped by host.\n\nReturns ``{\"embeds\": [{\"host\", \"play_count_7d\", \"play_count_30d\",\n\"top_articles\": [{\"slug\", \"title\", \"play_count_7d\"}]}, ...]}`` —\nhosts ordered by 7-day play count descending. Empty list when the\npublisher has no badge-origin articles.","operationId":"publisher_embed_stats_v1_publisher__tenant_slug__embed_stats_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Embed Stats V1 Publisher  Tenant Slug  Embed Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embeds/leaderboard":{"get":{"summary":"Admin Embeds Leaderboard","description":"Top embed hosts across ALL publishers, ranked by 7-day play\ncount. ``limit`` defaults to 50 and is clamped to [1, 200].","operationId":"admin_embeds_leaderboard_v1_admin_embeds_leaderboard_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embeds Leaderboard V1 Admin Embeds Leaderboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/funnel":{"get":{"summary":"Admin Publishers Funnel","description":"Five-stage publisher conversion funnel across all publishers.\n\nNULL stages are returned as JSON ``null`` (not omitted) so the\ndashboard can render the funnel as a sparse table without\nbranching on key presence.","operationId":"admin_publishers_funnel_v1_admin_publishers_funnel_get","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publishers Funnel V1 Admin Publishers Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{tenant_slug}/funnel":{"get":{"summary":"Admin Publisher Funnel Single","description":"Funnel snapshot for one publisher. Same shape as the bulk\nendpoint but scoped to one slug — handy when the operator drills\nin from the leaderboard into a single row.","operationId":"admin_publisher_funnel_single_v1_admin_publishers__tenant_slug__funnel_get","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publisher Funnel Single V1 Admin Publishers  Tenant Slug  Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/youtube/upload-now":{"post":{"summary":"Admin Youtube Upload Now","description":"Operator-triggered one-shot YouTube upload. Returns ``{video_id:\nstr | null}``. Returns ``video_id=null`` when YOUTUBE_* creds are\nmissing (dry-run) — the endpoint itself does not require them so a\nsmoke test against the route is safe in any env.","operationId":"admin_youtube_upload_now_v1_admin_youtube_upload_now_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_YouTubeUploadNowBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Youtube Upload Now V1 Admin Youtube Upload Now Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/daily-digests/run-now":{"post":{"summary":"Admin Run Daily Digests","operationId":"admin_run_daily_digests_v1_admin_daily_digests_run_now_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Run Daily Digests V1 Admin Daily Digests Run Now Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{slug}/resolve-x-handle":{"post":{"summary":"Admin Resolve Publisher X Handle","operationId":"admin_resolve_publisher_x_handle_v1_admin_publishers__slug__resolve_x_handle_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Resolve Publisher X Handle V1 Admin Publishers  Slug  Resolve X Handle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/cold-eligible":{"get":{"summary":"Admin Cold Outreach Eligible","description":"Preview eligible cold-outreach candidates WITHOUT sending.","operationId":"admin_cold_outreach_eligible_v1_admin_outreach_cold_eligible_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Cold Outreach Eligible V1 Admin Outreach Cold Eligible Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/cold-batch":{"post":{"summary":"Admin Cold Outreach Batch","description":"Operator-triggered cold-outreach drip. Capped at 100 per call.\n\n``dry_run=True`` returns the candidate list with no sends; the\noperator can preview the batch with ``/cold-eligible`` first.\n``slugs`` narrows to a specific list of publishers (otherwise we\nsweep eligible publishers ordered by ``created_at desc``).","operationId":"admin_cold_outreach_batch_v1_admin_outreach_cold_batch_post","parameters":[{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ColdBatchBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Cold Outreach Batch V1 Admin Outreach Cold Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/cold-outreach/fire-now":{"post":{"summary":"Admin Cold Outreach Fire Now","description":"Manually fire the cold-outreach auto-tick.\n\nSame code path as the 15:00 UTC scheduler tick — vertical-balanced\nselection from the warm pool, REMOVE-handler gate respected,\nPostmark-throttled sends, end-of-run TG digest.\n\nResponse contract\n-----------------\n* ``dry_run=true`` (default) → 200 with the planned selection\n  preview. Synchronous; no kv writes; no Postmark traffic.\n* ``dry_run=false`` → 202 + ``{\"job_id\": \"...\", \"status\": \"queued\",\n  \"candidates_to_attempt\": int}``. The actual fire runs in a\n  FastAPI BackgroundTasks task; poll\n  ``GET /v1/admin/cold-outreach/fire-now/{job_id}/status`` for\n  progress + final counts.\n* ``dry_run=false`` when a previous run is still in-flight → 409\n  with ``{\"job_id\": \"<in-flight uuid>\", \"status\": \"running\"}`` so\n  the caller can pivot to the status endpoint without queueing a\n  duplicate run.\n\nUse ``dry_run=true`` to preview the template + vertical mix before\nflipping ``AUTO_COLD_OUTREACH_ENABLED=1``.\n\nQuery params\n------------\ndry_run:\n    Default ``true``. ``false`` actually sends. Live sends still\n    respect the 23h idempotency window — to override it for a\n    legitimate same-day re-fire, ALSO pass\n    ``skip_idempotency=true``.\nquota:\n    Override the per-run send budget. Capped at the env\n    ``AUTO_COLD_OUTREACH_DAILY_QUOTA`` value inside the worker —\n    the route layer just forwards the param.\nskip_idempotency:\n    Only honored by the worker when also ``dry_run=false``. The\n    scheduler tick NEVER sets this (so the daily idempotency\n    guard stays in force).","operationId":"admin_cold_outreach_fire_now_v1_admin_cold_outreach_fire_now_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"quota","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota"}},{"name":"skip_idempotency","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Skip Idempotency"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/cold-outreach/fire-now/{job_id}/status":{"get":{"summary":"Admin Cold Outreach Fire Now Status","description":"Status poll for a fire-now job created via the async POST path.\n\nReturns the parsed kv blob — fields::\n\n    {\n        \"job_id\": \"<uuid>\",\n        \"status\": \"queued\" | \"running\" | \"done\" | \"failed\",\n        \"started_at\": \"<ISO-8601 UTC>\",\n        \"finished_at\": \"<ISO-8601 UTC>\" | null,\n        \"candidates_to_attempt\": int,\n        \"candidates_attempted\": int,\n        \"candidates_sent\": int,\n        \"errors\": [str, ...],\n        \"result\": {... run_daily_cold_fire blob ...} | null,\n        \"params\": {dry_run, quota, skip_idempotency},\n    }\n\n404 when the job_id has no kv row (typo or already evicted — this\nmodule never evicts but a future GC sweep might).","operationId":"admin_cold_outreach_fire_now_status_v1_admin_cold_outreach_fire_now__job_id__status_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Cold Outreach Fire Now Status V1 Admin Cold Outreach Fire Now  Job Id  Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/discovery/run-now":{"post":{"summary":"Admin Discovery Run Now","description":"Manually fire the discovery pool-replenish tick.\n\nSame code path as the 12:00 UTC scheduler tick — fires each DB-\nwriting discovery rail (Substack + Beehiiv as of 2026-06-01) in\nparallel with a per-rail 3-min timeout, inserts net-new Publisher\nrows, and fires a deduped TG digest.\n\nQuery params\n------------\ndry_run:\n    Default ``true``. ``false`` actually persists net-new\n    Publishers. Live runs respect the 23h idempotency window\n    unless ``skip_idempotency=true``.\nskip_threshold:\n    Default ``false``. Set ``true`` to force-fire the rails even\n    when the eligible pool is above the threshold (the auto-tick's\n    \"pool healthy\" short-circuit is bypassed). Use this when you\n    know the pool is about to drain and want to top up early.\nskip_idempotency:\n    Default ``false``. Set ``true`` to bypass the 23h kv-store\n    guard so the operator can re-fire on the same UTC day.\npool_threshold:\n    Override ``AUTO_DISCOVERY_POOL_THRESHOLD`` env (default 300).\nper_rail_limit:\n    Override ``AUTO_DISCOVERY_PER_RAIL_LIMIT`` env (default 50).\n    Passed as ``limit`` to each rail.","operationId":"admin_discovery_run_now_v1_admin_discovery_run_now_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"skip_threshold","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Skip Threshold"}},{"name":"skip_idempotency","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Skip Idempotency"}},{"name":"pool_threshold","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pool Threshold"}},{"name":"per_rail_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Per Rail Limit"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Discovery Run Now V1 Admin Discovery Run Now Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/fire-batch":{"post":{"summary":"Admin Outreach Fire Batch","description":"Persona-aware cold-outreach blast for the 152-publisher campaign.\n\nSelection (2026-05-16 additive):\n    * ``publisher_ids``  - exact Publisher.id UUID list (legacy).\n    * ``publisher_slugs`` - Publisher.tenant_slug list; resolved to\n      ids server-side, unknown slugs surfaced in ``skipped``.\n    * If BOTH are provided, the resolved set is the UNION (deduped).\n    * If NEITHER is provided, falls back to the default cohort:\n      contact_email IS NOT NULL AND cold_outreach_message_id IS NULL,\n      ordered by tier_band DESC then created_at DESC, limited by\n      ``batch_size``.\n\nSafety:\n    * ``is_book_author=True`` publishers are HARD-SKIPPED at the\n      send layer unless the operator explicitly passes\n      ``allow_book_authors=True``. This is independent of the\n      upstream book_filter narration gate (belt-and-suspenders).\n\nReturns ``{dry_run, selected, sent, failed, monday_flipped,\nmessage_ids, errors, skipped}``.","operationId":"admin_outreach_fire_batch_v1_admin_outreach_fire_batch_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_FireBatchBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Outreach Fire Batch V1 Admin Outreach Fire Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/funnel":{"get":{"summary":"Admin Outreach Funnel","description":"Cold-outreach funnel rollup. Operator-facing: hit this once,\nsee sent → opened → clicked → claimed → paid plus per-vertical +\nper-batch breakdown. ``since`` is ISO-8601 (default: 30 days ago).","operationId":"admin_outreach_funnel_v1_admin_outreach_funnel_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"batch_tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Batch Tag"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Outreach Funnel V1 Admin Outreach Funnel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/digest":{"post":{"summary":"Admin Outreach Digest","description":"Aggregate cold-outreach activity since *since* (default 24h\nback) into a single Telegram alert. Returns the rendered message\n+ structured counts for operator introspection.","operationId":"admin_outreach_digest_v1_admin_outreach_digest_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DigestBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Outreach Digest V1 Admin Outreach Digest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/health":{"get":{"summary":"Admin Outreach Health","description":"One-shot operator outreach-health dashboard.\n\nCombines funnel + digest aggregate + render queue depth + recent\ntips/subscribes/takedowns + platform-config snapshot into a single\nJSON page. Read-only; each section degrades to ``null`` on\nsub-query failure so the operator always gets *something* back.","operationId":"admin_outreach_health_v1_admin_outreach_health_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Outreach Health V1 Admin Outreach Health Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{slug}/intake-policy":{"patch":{"summary":"Admin Set Publisher Intake Policy","description":"Toggle ``intake_strip_signoff`` / ``intake_strip_disclaimer`` for a\npublisher. Either flag set to None leaves the existing value as-is so\ncallers can flip one knob without restating the other.\n\nWired into ``app/intake/bundler.process_inbound_article`` — the\nbundler reads the flags off the Publisher row on every inbound\narticle. Default behavior (both False) is no-op, preserving the\nexisting \"keep author voice intact\" contract.","operationId":"admin_set_publisher_intake_policy_v1_admin_publishers__slug__intake_policy_patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PublisherIntakePolicyUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Set Publisher Intake Policy V1 Admin Publishers  Slug  Intake Policy Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/discover-feed":{"post":{"summary":"Admin Publishers Discover Feed","description":"Run CMS-aware feed discovery for a publisher and optionally\nseed an active IntakeSource row when a feed is found.\n\nLookup uses ``raw_json.site_url`` on the Publisher row (matching\nthe wave-2 author seed convention). Returns the full discovery\nresult + a ``seeded`` flag + the new ``intake_source_id`` when\nauto_seed kicks in.","operationId":"admin_publishers_discover_feed_v1_admin_publishers_discover_feed_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DiscoverFeedBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Publishers Discover Feed V1 Admin Publishers Discover Feed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/monday/sync-eligible":{"post":{"summary":"Admin Monday Sync Eligible","description":"Fire-and-forget bulk mirror of cold-outreach pipeline into the\nmonday.com board. ``dry_run=true`` (default) reports the count of\npublishers that WOULD be upserted without writing — used by the\ndeploy-verify smoke test. ``dry_run=false`` kicks the background\ntask; poll /v1/admin/monday/sync-eligible/status for progress.\n\nState is persisted to ``monday_sync_state`` so a Fly redeploy mid-run\nno longer wipes progress. If a prior run is still flagged\n``running`` but ``started_at`` is older than 10 min, we treat it as\nabandoned and resume from ``last_processed_publisher_id``.","operationId":"admin_monday_sync_eligible_v1_admin_monday_sync_eligible_post","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Dry Run"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":500,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/monday/sync-eligible/status":{"get":{"summary":"Admin Monday Sync Eligible Status","operationId":"admin_monday_sync_eligible_status_v1_admin_monday_sync_eligible_status_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Monday Sync Eligible Status V1 Admin Monday Sync Eligible Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/outreach/funnel.csv":{"get":{"summary":"Admin Outreach Funnel Csv","description":"Per-publisher cold-outreach funnel as CSV. Spreadsheet-friendly\nfor the operator's ad-hoc analysis without JSON parsing. Same\nfilters as the JSON endpoint.","operationId":"admin_outreach_funnel_csv_v1_admin_outreach_funnel_csv_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"vertical","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},{"name":"batch_tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Batch Tag"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/postmark/open-webhook":{"post":{"summary":"Postmark Open Webhook","description":"Postmark Open Tracking webhook → stamps\n``Publisher.raw_json.cold_outreach_opened_at`` on the first open.\n\nAuth: ``?secret=<POSTMARK_OPEN_SECRET>``. Operator wires this up in\nPostmark UI → Servers → Webhooks with the Open event checked.\nReturns 200 unconditionally.","operationId":"postmark_open_webhook_v1_integrations_postmark_open_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Postmark Open Webhook V1 Integrations Postmark Open Webhook Post"}}}}}}},"/v1/integrations/postmark/click-webhook":{"post":{"summary":"Postmark Click Webhook","description":"Postmark Link-Click webhook → stamps\n``Publisher.raw_json.cold_outreach_clicked_at`` + ``..._clicked_url``\non the first click. Auth + idempotency identical to the open\nhandler.","operationId":"postmark_click_webhook_v1_integrations_postmark_click_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Postmark Click Webhook V1 Integrations Postmark Click Webhook Post"}}}}}}},"/v1/postmark/open":{"post":{"summary":"Postmark Open Relay","description":"Postmark Open Tracking → monday.com Opened status + raw_json stamp.\n\nAuth: ``?secret=<POSTMARK_OPEN_SECRET>``. When the env var is empty\nthe endpoint returns 503 — operator must explicitly opt in by\nsetting the secret in Fly.","operationId":"postmark_open_relay_v1_postmark_open_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/postmark/click":{"post":{"summary":"Postmark Click Relay","description":"Postmark Link-Click → monday.com Clicked status + raw_json stamp.\n\nAuth + 503-when-unconfigured semantics mirror /v1/postmark/open.\nClick payload's Link/OriginalLink is appended to monday Notes via\nthe dispatcher's note_append parameter so the operator sees which\nCTA was clicked without leaving the board.","operationId":"postmark_click_relay_v1_postmark_click_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/postmark/inbound":{"post":{"summary":"Postmark Inbound Relay","description":"Postmark inbound webhook → cold-outreach reply detection +\nmonday.com Replied status + operator Telegram ping.\n\nAuth: ``?secret=<POSTMARK_INBOUND_SECRET>``. Mismatch returns 200\nwith ``{ok:true, skipped:\"auth\"}`` (Postmark retry-storm avoidance).\nAll non-success paths return 200 with a ``skipped`` reason so the\nPostmark inbound queue stays drained.","operationId":"postmark_inbound_relay_v1_postmark_inbound_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/admin/takedowns":{"get":{"summary":"Admin Takedowns List","description":"List the most recent platform takedown rows for operator review.","operationId":"admin_takedowns_list_v1_admin_takedowns_get","parameters":[{"name":"platform","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Severity"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Takedowns List V1 Admin Takedowns Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/takedowns/{takedown_id}/acknowledge":{"post":{"summary":"Admin Takedown Acknowledge","description":"Operator marks a takedown row as reviewed.","operationId":"admin_takedown_acknowledge_v1_admin_takedowns__takedown_id__acknowledge_post","parameters":[{"name":"takedown_id","in":"path","required":true,"schema":{"type":"string","title":"Takedown Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Takedown Acknowledge V1 Admin Takedowns  Takedown Id  Acknowledge Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/postmark/partners-reply":{"post":{"summary":"Postmark Partners Reply","description":"Postmark inbound webhook for partners@storyflo.com replies.\n\nAuth: ``?secret=<POSTMARK_PARTNERS_REPLY_SECRET>`` query param —\nPostmark's UI exposes query-string config more reliably than\ncustom headers, mirroring the story@ intake's ``?token=`` shape.","operationId":"postmark_partners_reply_v1_integrations_postmark_partners_reply_post","parameters":[{"name":"secret","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PartnersReplyPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Postmark Partners Reply V1 Integrations Postmark Partners Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/unsubscribe":{"get":{"summary":"Unsubscribe Get","description":"Human-clickable unsubscribe link. Auth = HMAC token validity only;\nintentionally unauthenticated per RFC 8058.","operationId":"unsubscribe_get_v1_unsubscribe_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":4,"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Unsubscribe Post","description":"Gmail/Yahoo one-click POST endpoint. Accepts the token in either\nthe query string or the form body. Returns 200 HTML on success.","operationId":"unsubscribe_post_v1_unsubscribe_post","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/postmark/bounce-webhook":{"post":{"summary":"Postmark Bounce Webhook","description":"Ingest Postmark bounce/complaint payloads into ``EmailSuppression``.\n\nBehavior:\n  * ``HardBounce`` → suppress immediately (bounce_hard)\n  * ``SpamComplaint`` → suppress immediately (complaint)\n  * ``SoftBounce`` / ``Transient`` → bump attempts_count;\n    the suppression gate flips once count >= 3\n  * Anything else → log + return ok so Postmark stops retrying\n\nAlways returns 200 so Postmark's retry queue stays clean.","operationId":"postmark_bounce_webhook_v1_integrations_postmark_bounce_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Postmark Bounce Webhook V1 Integrations Postmark Bounce Webhook Post"}}}}}}},"/v1/webhooks/postmark/bounce":{"post":{"summary":"Postmark Bounce To Publisher Dnc","description":"Stamp ``Publisher.do_not_contact=true`` on permanent Postmark\nbounces so the cold-outreach pool query stops re-selecting the\naddress. See ``app.postmark_bounce_handler`` for categorization +\npayload schema. Always returns 200 (even on auth-fail / bad-json /\nDB error) so Postmark's retry queue stays clean.","operationId":"postmark_bounce_to_publisher_dnc_v1_webhooks_postmark_bounce_post","parameters":[{"name":"x-postmark-webhook-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Postmark-Webhook-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Postmark Bounce To Publisher Dnc V1 Webhooks Postmark Bounce Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/qa/publisher-e2e":{"post":{"summary":"Admin Qa Publisher E2E","description":"Fire the publisher end-to-end QA suite synchronously.\n\nReturns the structured report (~30-60s wall-clock for a real run).\nAlso writes the report to ``AdminAuditLog`` so the operator can\ndiff QA runs over time without re-firing the suite.\n\nSee ``app/qa/publisher_e2e.py`` for the canonical pipeline. Env\nvars consumed:\n\n  * ``STORYFLO_QA_BASE_URL`` — target API base; default\n    ``http://localhost:8000``. Set this on Fly to the public URL\n    so the suite exercises the real edge surface (CORS preflight,\n    TLS, etc.).\n  * ``QA_TEST_WALLET_ADDRESS`` — base-sepolia receive address\n    recorded against the test publisher's payout_wallet_address.\n  * ``X402_TEST_PAYMENT_HEADER`` — pre-signed testnet payment\n    header for the tip stage. When unset, the tip stage skips\n    with an explicit reason instead of failing.","operationId":"admin_qa_publisher_e2e_v1_admin_qa_publisher_e2e_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/_QAPublisherE2ERequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Qa Publisher E2E V1 Admin Qa Publisher E2E Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embed/test":{"post":{"summary":"Admin Embed Test","description":"Validate the merchant-side ``/embed/badge`` flow end-to-end for an\narbitrary URL. Reuses the same intake helpers + DB lookups as\n``/v1/badge/resolve`` and emits a per-stage pass/fail report.\n\nStages: fetch_page, publisher_resolve, article_upsert,\nrender_enqueue, render_complete (polls up to ~60s), embed_html.\n\n``dry_run=true`` stops after publisher_resolve.\n\nAuth: admin token (X-Storyflo-Email-Token). This endpoint fetches\narbitrary URLs server-side, so it must require admin auth, not the\nshared api_key.","operationId":"admin_embed_test_v1_admin_embed_test_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EmbedTestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embed Test V1 Admin Embed Test Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/embed/iframe-preview":{"get":{"summary":"Admin Embed Iframe Preview","description":"Return the exact iframe snippet a merchant would embed for a given\nStoryflo article slug, plus a public preview URL the operator can open\nin a browser to visually confirm the rendered player.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_embed_iframe_preview_v1_admin_embed_iframe_preview_get","parameters":[{"name":"slug","in":"query","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Embed Iframe Preview V1 Admin Embed Iframe Preview Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/publishers/{slug}/payout-preference":{"get":{"summary":"Admin Get Publisher Payout Preference","operationId":"admin_get_publisher_payout_preference_v1_admin_publishers__slug__payout_preference_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Get Publisher Payout Preference V1 Admin Publishers  Slug  Payout Preference Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Admin Patch Publisher Payout Preference","operationId":"admin_patch_publisher_payout_preference_v1_admin_publishers__slug__payout_preference_patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PayoutPreferenceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Patch Publisher Payout Preference V1 Admin Publishers  Slug  Payout Preference Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/publisher/{tenant_slug}/payout-preference":{"patch":{"summary":"Publisher Patch Payout Preference","description":"Publisher self-serve. Token-gated, same body shape as the admin\nendpoint. Switching to usdc/split requires a valid Base mainnet\naddress.","operationId":"publisher_patch_payout_preference_v1_publisher__tenant_slug__payout_preference_patch","parameters":[{"name":"tenant_slug","in":"path","required":true,"schema":{"type":"string","title":"Tenant Slug"}},{"name":"x-storyflo-publisher-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Publisher-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PayoutPreferenceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publisher Patch Payout Preference V1 Publisher  Tenant Slug  Payout Preference Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/circle/balance":{"get":{"summary":"Admin Circle Balance","description":"Auth: admin token (X-Storyflo-Email-Token).","operationId":"admin_circle_balance_v1_admin_circle_balance_get","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Circle Balance V1 Admin Circle Balance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/circle/test-transfer":{"post":{"summary":"Admin Circle Test Transfer","description":"Auth: admin token (X-Storyflo-Email-Token). This endpoint can move\nreal USDC out of the platform Circle wallet — must require admin\ntoken, not the shared api_key.","operationId":"admin_circle_test_transfer_v1_admin_circle_test_transfer_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_CircleTestTransferRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Circle Test Transfer V1 Admin Circle Test Transfer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/circle/webhook":{"post":{"summary":"Circle Webhook","description":"Receive Circle webhook events (transfers.completed / .failed).\n\nSignature header is HMAC-SHA256 over the raw body, keyed on\n``CIRCLE_WEBHOOK_SIGNING_SECRET``. Returns 200 on auth-fail so\nCircle doesn't endlessly retry on a misconfigured secret — we log\n+ no-op.","operationId":"circle_webhook_v1_integrations_circle_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Circle Webhook V1 Integrations Circle Webhook Post"}}}}}}},"/v1/admin/payouts/batches/{batch_id}/route-settle":{"post":{"summary":"Admin Route Settle Batch","description":"Settle a prepared PayoutBatch via the Wave-5 payout_router —\ndispatches to Stripe Connect (USD), Circle Mint (USDC), or both\n(split) per ``publisher.payout_preference``. Replaces the\noperator's manual broadcast for publishers who've opted into a\nprogrammatic rail. The router writes the Circle fee back as a\nplatform_revenue LedgerEntry so the publisher absorbs the delivery\ncost — listener UX is untouched.","operationId":"admin_route_settle_batch_v1_admin_payouts_batches__batch_id__route_settle_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","title":"Batch Id"}},{"name":"api_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"}},{"name":"x-storyflo-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Key"}},{"name":"x-readout-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Readout-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Route Settle Batch V1 Admin Payouts Batches  Batch Id  Route Settle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/listeners/clear-stripe-ids":{"post":{"summary":"Admin Listeners Clear Stripe Ids","description":"One-shot helper to NULL ``stripe_customer_id`` and\n``stripe_subscription_id`` on listener rows that point at the old\ndroplinked Stripe account (``acct_1Odtp1JYpy7bkFtu``) after the\n2026-05-16 cutover to the standalone Storyflo account\n(``acct_1TWIsfQyWEQL0Itr``).\n\nStripe Customers do not transfer between accounts; leaving stale ids\nin place would 500 the next ``/subscription-intent`` call. Listeners\nre-enter card on next checkout — acceptable pre-launch.\n\nBody (all optional)::\n\n    {\"dry_run\": true, \"limit\": 100000}\n\nDefault is ``dry_run=true``. Idempotent: a second real-fire call\nreturns ``rows_updated=0``.","operationId":"admin_listeners_clear_stripe_ids_v1_admin_listeners_clear_stripe_ids_post","parameters":[{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Listeners Clear Stripe Ids V1 Admin Listeners Clear Stripe Ids Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/briefings/{persona_slug}/{briefing_id}/publish":{"post":{"summary":"Publish Briefing","description":"Operator-only: flip status draft → published, stamp published_at.","operationId":"publish_briefing_v1_admin_briefings__persona_slug___briefing_id__publish_post","parameters":[{"name":"persona_slug","in":"path","required":true,"schema":{"type":"string","title":"Persona Slug"}},{"name":"briefing_id","in":"path","required":true,"schema":{"type":"string","title":"Briefing Id"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publish Briefing V1 Admin Briefings  Persona Slug   Briefing Id  Publish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/briefings/render-audio":{"post":{"summary":"Admin Briefings Render Audio","description":"Operator trigger: render audio for up to ``limit`` published briefings\nthat are missing ``audio_keys``.\n\nUseful for one-shot backfill (e.g. ``limit=200`` drains all 145 existing\nrows in a single call) and for testing without waiting for the 10-min\nworker interval. The worker itself runs the same function on schedule so\nthis endpoint adds zero unique logic — it's just an on-demand entry point.\n\nAuth: admin token (X-Storyflo-Email-Token).","operationId":"admin_briefings_render_audio_v1_admin_briefings_render_audio_post","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":10,"title":"Limit"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Briefings Render Audio V1 Admin Briefings Render Audio Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/pipeline/overview":{"get":{"summary":"Admin Pipeline Overview","description":"Aggregate snapshot of the daily-publishing funnel for the\n/admin/pipeline page.\n\nReturns five-stage filter counts (intake → quality → summary →\nrender → published), per-vertical breakdown matched to the 8\nSpotify show slugs, current Fly-secret filter knobs, and the\n9-episode daily target progress. All numbers are computed for the\nrequested ``window`` plus a trailing-7d average baseline.","operationId":"admin_pipeline_overview_v1_admin_pipeline_overview_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","maxLength":8,"default":"24h","title":"Window"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Pipeline Overview V1 Admin Pipeline Overview Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/inbox/threads":{"get":{"summary":"Admin Inbox Threads","description":"Merged outbound+inbound conversation threads.\n\nReturns a list of threads, each thread = one publisher (or one\nstranger email when no publisher match) with all messages from\nboth directions sorted oldest-first inside the thread. The thread\nlist itself is sorted by ``last_message_at`` descending so the\nmost-recently-active conversation lands at the top.\n\nIncludes the cold-outreach invite that opened the conversation,\nany auto follow-up sends, and every inbound reply tied to the\nsame publisher / address.","operationId":"admin_inbox_threads_v1_admin_inbox_threads_get","parameters":[{"name":"publisher_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Publisher Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"x-storyflo-email-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Storyflo-Email-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Inbox Threads V1 Admin Inbox Threads Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AccessRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"AccessRequest"},"AckResponse":{"properties":{"id":{"type":"string","title":"Id"},"acknowledged_at":{"type":"string","title":"Acknowledged At"}},"type":"object","required":["id","acknowledged_at"],"title":"AckResponse"},"AdminMagicLinkRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"AdminMagicLinkRequest"},"AdminPasswordLoginRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":1,"title":"Email"},"password":{"type":"string","maxLength":512,"minLength":1,"title":"Password"}},"type":"object","required":["email","password"],"title":"AdminPasswordLoginRequest"},"AlertListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"items":{"items":{"$ref":"#/components/schemas/AlertOut"},"type":"array","title":"Items"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["total","items","limit","offset"],"title":"AlertListResponse"},"AlertOut":{"properties":{"id":{"type":"string","title":"Id"},"severity":{"type":"string","title":"Severity"},"category":{"type":"string","title":"Category"},"title":{"type":"string","title":"Title"},"summary":{"type":"string","title":"Summary"},"article_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Article Slug"},"job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Id"},"admin_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Admin Url"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta"},"sent_at":{"type":"string","title":"Sent At"},"acknowledged_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acknowledged At"}},"type":"object","required":["id","severity","category","title","summary","sent_at"],"title":"AlertOut"},"AlphaSignupRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"},"source":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Source"},"role":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Role","description":"listener | publisher (defaults to listener)"},"turnstile_token":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Turnstile Token"},"referrer_listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Referrer Listener Token"}},"type":"object","required":["email"],"title":"AlphaSignupRequest"},"ArticleRow":{"properties":{"id":{"type":"string","title":"Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id"],"title":"ArticleRow"},"BedtimeStoryView":{"properties":{"id":{"type":"string","title":"Id"},"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"author":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author"},"source":{"type":"string","title":"Source"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"license":{"type":"string","title":"License"},"age_range_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Age Range Min"},"age_range_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Age Range Max"},"est_duration_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Est Duration Seconds"},"voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Id"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"audio_rendered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Audio Rendered At"},"cover_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image Url"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"}},"type":"object","required":["id","slug","title","author","source","source_id","source_url","license","age_range_min","age_range_max","est_duration_seconds","voice_id","audio_url","audio_rendered_at","cover_image_url","tags","created_at","updated_at","deleted_at"],"title":"BedtimeStoryView"},"Body_publisher_voicebox_enroll_v1_publisher__tenant_slug__voicebox_enroll_post":{"properties":{"sample":{"type":"string","contentMediaType":"application/octet-stream","title":"Sample"},"consent":{"type":"boolean","title":"Consent","default":false},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"consent_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent Ref"},"engine":{"type":"string","title":"Engine","default":"styletts2"}},"type":"object","required":["sample"],"title":"Body_publisher_voicebox_enroll_v1_publisher__tenant_slug__voicebox_enroll_post"},"BusinessApplyRequest":{"properties":{"tenant_slug":{"anyOf":[{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$"},{"type":"null"}],"title":"Tenant Slug"},"publication_name":{"type":"string","maxLength":255,"minLength":1,"title":"Publication Name"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"payout_email":{"type":"string","maxLength":320,"minLength":3,"title":"Payout Email"},"payout_method":{"type":"string","maxLength":40,"minLength":1,"title":"Payout Method"},"payout_address":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Payout Address"},"why_join":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Why Join"}},"type":"object","required":["publication_name","contact_email","payout_email","payout_method"],"title":"BusinessApplyRequest"},"BusinessApplyResponse":{"properties":{"application_id":{"type":"string","title":"Application Id"},"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"auto_approved":{"type":"boolean","title":"Auto Approved","default":false},"hex_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hex Code"},"tenant_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Slug"}},"type":"object","required":["application_id","status","message"],"title":"BusinessApplyResponse"},"CandidateView":{"properties":{"id":{"type":"string","title":"Id"},"publisher_domain":{"type":"string","title":"Publisher Domain"},"post_url":{"type":"string","title":"Post Url"},"title":{"type":"string","title":"Title"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"is_paywalled":{"type":"boolean","title":"Is Paywalled"},"excerpt":{"type":"string","title":"Excerpt"},"has_full_content":{"type":"boolean","title":"Has Full Content"},"status":{"type":"string","title":"Status"},"discovered_at":{"type":"string","format":"date-time","title":"Discovered At"}},"type":"object","required":["id","publisher_domain","post_url","title","published_at","is_paywalled","excerpt","has_full_content","status","discovered_at"],"title":"CandidateView"},"ConfirmSourceRequest":{"properties":{"feed_url":{"type":"string","maxLength":2048,"minLength":4,"title":"Feed Url"}},"type":"object","required":["feed_url"],"title":"ConfirmSourceRequest","description":"The admin's chosen / overridden ingestion feed for a candidate that\nsource discovery flagged ``source_needs_confirm`` (PR-3)."},"DeliveryView":{"properties":{"id":{"type":"string","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"status":{"type":"string","title":"Status"},"attempts":{"type":"integer","title":"Attempts"},"last_response_status":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Response Status"},"last_attempted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Attempted At"},"next_attempt_at":{"type":"string","title":"Next Attempt At"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","event_type","status","attempts","last_response_status","last_attempted_at","next_attempt_at","created_at"],"title":"DeliveryView"},"DetectTestResponse":{"properties":{"language":{"type":"string","title":"Language"},"voice":{"additionalProperties":true,"type":"object","title":"Voice"}},"type":"object","required":["language","voice"],"title":"DetectTestResponse"},"EmailIntakeRequest":{"properties":{"from":{"type":"string","title":"From"},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html"},"headers":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Headers"}},"type":"object","required":["from"],"title":"EmailIntakeRequest","description":"Generic shape that accepts both lowercase (`from`, `text`, `html`) and\nPostmark's capitalized payload (`From`, `TextBody`, `HtmlBody`) without\nany transform. Mailgun / ImprovMX / ForwardEmail fit the lowercase form\nor can be mapped in their console with a 3-line template."},"EmbedRegisterRequest":{"properties":{"publisher":{"type":"string","maxLength":120,"minLength":1,"title":"Publisher"},"url":{"type":"string","maxLength":2048,"minLength":4,"title":"Url"},"title":{"type":"string","maxLength":500,"minLength":1,"title":"Title"}},"type":"object","required":["publisher","url","title"],"title":"EmbedRegisterRequest"},"EmbedTrackRequest":{"properties":{"publisher":{"type":"string","maxLength":120,"minLength":1,"title":"Publisher"},"slug":{"type":"string","maxLength":200,"minLength":1,"title":"Slug"},"event":{"type":"string","pattern":"^(play_started|play_completed)$","title":"Event"},"listened_seconds":{"type":"number","title":"Listened Seconds","default":0.0}},"type":"object","required":["publisher","slug","event"],"title":"EmbedTrackRequest"},"GenerateTeaserRequest":{"properties":{"tagline":{"type":"string","maxLength":240,"title":"Tagline","default":"Storyflo helps you get into the flo of your day."},"voice_id":{"type":"string","maxLength":64,"title":"Voice Id","default":"kira"},"format":{"type":"string","enum":["square","vertical"],"title":"Format","default":"square"},"duration_s":{"type":"integer","maximum":60.0,"minimum":5.0,"title":"Duration S","default":15}},"type":"object","title":"GenerateTeaserRequest"},"GenerateTeaserResponse":{"properties":{"url":{"type":"string","title":"Url"},"size_bytes":{"type":"integer","title":"Size Bytes"},"duration_s":{"type":"integer","title":"Duration S"},"format":{"type":"string","enum":["square","vertical"],"title":"Format"}},"type":"object","required":["url","size_bytes","duration_s","format"],"title":"GenerateTeaserResponse"},"GreenlightRequest":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"},"extra_emails":{"items":{"type":"string"},"type":"array","title":"Extra Emails"},"domain":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Domain"},"website_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Website Url"},"socials":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Socials"},"tier":{"type":"string","title":"Tier","default":"free"},"comp":{"type":"boolean","title":"Comp","default":true},"vertical_hint":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical Hint"},"feed_url_hint":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Feed Url Hint"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"track":{"type":"string","maxLength":20,"title":"Track","default":"publisher"}},"type":"object","required":["name","email"],"title":"GreenlightRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImportGutenbergRequest":{"properties":{"slug":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Slug"},"voice_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Voice Id"}},"type":"object","title":"ImportGutenbergRequest"},"IngestRequest":{"properties":{"manifest_url":{"type":"string","title":"Manifest Url"},"limit":{"type":"integer","title":"Limit","default":10},"skip":{"type":"integer","title":"Skip","default":0},"publish":{"type":"boolean","title":"Publish","default":false},"purge_drafts":{"type":"boolean","title":"Purge Drafts","default":false}},"type":"object","required":["manifest_url"],"title":"IngestRequest"},"IntakeSourceCreate":{"properties":{"publisher_id":{"type":"string","title":"Publisher Id"},"kind":{"type":"string","title":"Kind","description":"rss | atom | api_pull | api_push"},"feed_url":{"type":"string","maxLength":2048,"minLength":1,"title":"Feed Url"},"tier":{"type":"string","title":"Tier","description":"free | paid","default":"free"},"auth_header":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auth Header"},"default_vertical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Vertical"},"poll_interval_minutes":{"type":"integer","maximum":1440.0,"minimum":5.0,"title":"Poll Interval Minutes","default":15}},"type":"object","required":["publisher_id","kind","feed_url"],"title":"IntakeSourceCreate"},"IntakeSourcePatch":{"properties":{"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"},"show_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Show Name"}},"type":"object","title":"IntakeSourcePatch"},"IntakeSourceView":{"properties":{"id":{"type":"string","title":"Id"},"publisher_id":{"type":"string","title":"Publisher Id"},"kind":{"type":"string","title":"Kind"},"tier":{"type":"string","title":"Tier"},"status":{"type":"string","title":"Status"},"feed_url":{"type":"string","title":"Feed Url"},"default_vertical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Vertical"},"poll_interval_minutes":{"type":"integer","title":"Poll Interval Minutes"},"last_polled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Polled At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","publisher_id","kind","tier","status","feed_url","default_vertical","poll_interval_minutes","last_polled_at","last_error","created_at"],"title":"IntakeSourceView"},"JobEnqueueRequest":{"properties":{"voice":{"type":"string","title":"Voice"},"text":{"type":"string","maxLength":200000,"minLength":1,"title":"Text"},"speed":{"type":"number","maximum":2.0,"minimum":0.5,"title":"Speed","default":1.0},"publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"},"article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Article Id"}},"type":"object","required":["voice","text"],"title":"JobEnqueueRequest"},"JobView":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"voice_id":{"type":"string","title":"Voice Id"},"attempts":{"type":"integer","title":"Attempts"},"audio_keys":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Audio Keys"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["id","status","voice_id","attempts"],"title":"JobView"},"ListenProgressBody":{"properties":{"article_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Article Id"},"article_slug":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Article Slug"},"position_seconds":{"type":"number","maximum":86400.0,"minimum":0.0,"title":"Position Seconds","default":0.0},"listened_seconds":{"type":"number","maximum":86400.0,"minimum":0.0,"title":"Listened Seconds","default":0.0},"duration_seconds":{"anyOf":[{"type":"number","maximum":86400.0,"minimum":0.0},{"type":"null"}],"title":"Duration Seconds"},"completed":{"type":"boolean","title":"Completed","default":false},"voice_id":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Voice Id"},"source_embedder_slug":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Source Embedder Slug"},"listener_token":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Listener Token"}},"type":"object","title":"ListenProgressBody","description":"Progress beacon payload.\n\n``article_id`` and ``article_slug`` are both optional but at least one\nis required — the main player knows the UUID, the mini player and the\nembed surfaces only ever have the slug."},"ListenerPreferences":{"properties":{"verticals":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verticals"},"voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Id"},"tone_preference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tone Preference"},"paused":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Paused"},"digest_preference":{"anyOf":[{"type":"string","pattern":"^(all|trending|none|verticals)$"},{"type":"null"}],"title":"Digest Preference"},"digest_verticals":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Digest Verticals"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"flo_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Flo Enabled"},"preferred_lang":{"anyOf":[{"type":"string","pattern":"^(en|es)?$"},{"type":"null"}],"title":"Preferred Lang"},"notif_daily_digest":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notif Daily Digest"}},"type":"object","title":"ListenerPreferences","description":"Mutable preferences. All optional on PATCH; null leaves the field\nunchanged. Empty list / \"\" explicitly clears the field."},"ListenerQueueAddRequest":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":4,"title":"Url"},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source"}},"type":"object","required":["url"],"title":"ListenerQueueAddRequest"},"ListenerSubscribeRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"},"verticals":{"items":{"type":"string"},"type":"array","title":"Verticals"},"referrer_listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Referrer Listener Token"},"referrer_publisher_slug":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Referrer Publisher Slug"},"referrer_embedder_slug":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Referrer Embedder Slug"},"turnstile_token":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Turnstile Token"}},"type":"object","required":["email"],"title":"ListenerSubscribeRequest"},"MagicLinkRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"MagicLinkRequest"},"NewRoundParams":{"properties":{"desk_name":{"type":"string","maxLength":120,"minLength":1,"title":"Desk Name"},"round_title":{"type":"string","maxLength":200,"minLength":1,"title":"Round Title"},"round_url":{"type":"string","maxLength":500,"minLength":1,"title":"Round Url"},"thesis":{"anyOf":[{"type":"string","maxLength":600},{"type":"null"}],"title":"Thesis"},"unsubscribe_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Unsubscribe Url"}},"type":"object","required":["desk_name","round_title","round_url"],"title":"NewRoundParams","description":"Typed parameters for the ``signalflo_new_round`` template."},"NotifyRequest":{"properties":{"template":{"type":"string","const":"signalflo_new_round","title":"Template"},"to":{"type":"string","maxLength":320,"minLength":3,"title":"To"},"params":{"$ref":"#/components/schemas/NewRoundParams"}},"type":"object","required":["template","to","params"],"title":"NotifyRequest"},"NotifyResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"template":{"type":"string","title":"Template"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"}},"type":"object","required":["ok","template"],"title":"NotifyResponse"},"PlayRequest":{"properties":{"article_id":{"type":"string","title":"Article Id"},"publisher_id":{"type":"string","title":"Publisher Id"},"voice":{"type":"string","title":"Voice"},"listened_seconds":{"type":"number","minimum":0.0,"title":"Listened Seconds"},"completed":{"type":"boolean","title":"Completed","default":false},"play_value_micros":{"type":"integer","minimum":0.0,"title":"Play Value Micros"},"currency":{"type":"string","title":"Currency","default":"USD"},"ip_hash":{"anyOf":[{"type":"string","maxLength":64,"minLength":64},{"type":"null"}],"title":"Ip Hash"}},"type":"object","required":["article_id","publisher_id","voice","listened_seconds","play_value_micros"],"title":"PlayRequest"},"PlayResponse":{"properties":{"play_id":{"type":"string","title":"Play Id"},"ledger_total_micros":{"type":"integer","title":"Ledger Total Micros"}},"type":"object","required":["play_id","ledger_total_micros"],"title":"PlayResponse"},"PublishBothBody":{"properties":{"video_url":{"type":"string","minLength":8,"title":"Video Url"},"x_caption":{"type":"string","maxLength":280,"minLength":1,"title":"X Caption"},"yt_title":{"type":"string","maxLength":100,"minLength":1,"title":"Yt Title"},"yt_description":{"type":"string","maxLength":5000,"title":"Yt Description","default":""},"yt_tags":{"items":{"type":"string"},"type":"array","title":"Yt Tags"},"yt_category_id":{"type":"string","title":"Yt Category Id","default":"22"},"yt_privacy_status":{"type":"string","title":"Yt Privacy Status","default":"public"},"yt_is_short":{"type":"boolean","title":"Yt Is Short","default":false},"yt_vertical":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Yt Vertical"}},"type":"object","required":["video_url","x_caption","yt_title"],"title":"PublishBothBody"},"PublishRequest":{"properties":{"publisher":{"type":"string","title":"Publisher"},"article_id":{"type":"string","title":"Article Id"},"voice":{"type":"string","title":"Voice"},"text":{"type":"string","title":"Text"},"speed":{"type":"number","title":"Speed","default":1.0}},"type":"object","required":["publisher","article_id","voice","text"],"title":"PublishRequest"},"PublishResponse":{"properties":{"publisher":{"type":"string","title":"Publisher"},"article_id":{"type":"string","title":"Article Id"},"chunks":{"type":"integer","title":"Chunks"},"total_duration_sec":{"type":"number","title":"Total Duration Sec"},"total_cost_usd":{"type":"number","title":"Total Cost Usd"},"audio_urls":{"items":{"type":"string"},"type":"array","title":"Audio Urls"}},"type":"object","required":["publisher","article_id","chunks","total_duration_sec","total_cost_usd","audio_urls"],"title":"PublishResponse"},"PublishXBody":{"properties":{"video_url":{"type":"string","minLength":8,"title":"Video Url"},"caption":{"type":"string","maxLength":280,"minLength":1,"title":"Caption"}},"type":"object","required":["video_url","caption"],"title":"PublishXBody"},"PublishYouTubeBody":{"properties":{"video_url":{"type":"string","minLength":8,"title":"Video Url"},"title":{"type":"string","maxLength":100,"minLength":1,"title":"Title"},"description":{"type":"string","maxLength":5000,"title":"Description","default":""},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"category_id":{"type":"string","title":"Category Id","default":"22"},"privacy_status":{"type":"string","title":"Privacy Status","default":"public"},"is_short":{"type":"boolean","title":"Is Short","default":false},"vertical":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}},"type":"object","required":["video_url","title"],"title":"PublishYouTubeBody"},"PublisherApplicationApproveRequest":{"properties":{"tenant_slug_override":{"anyOf":[{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$"},{"type":"null"}],"title":"Tenant Slug Override"},"reviewer_notes":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Reviewer Notes"}},"type":"object","title":"PublisherApplicationApproveRequest"},"PublisherApplicationApproveResponse":{"properties":{"application_id":{"type":"string","title":"Application Id"},"publisher_id":{"type":"string","title":"Publisher Id"},"tenant_slug":{"type":"string","title":"Tenant Slug"},"hex_code":{"type":"string","title":"Hex Code"},"welcome_email_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Welcome Email Message Id"}},"type":"object","required":["application_id","publisher_id","tenant_slug","hex_code"],"title":"PublisherApplicationApproveResponse"},"PublisherApplicationView":{"properties":{"id":{"type":"string","title":"Id"},"tenant_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Slug"},"publication_name":{"type":"string","title":"Publication Name"},"contact_email":{"type":"string","title":"Contact Email"},"payout_email":{"type":"string","title":"Payout Email"},"payout_method":{"type":"string","title":"Payout Method"},"payout_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Address"},"why_join":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Why Join"},"status":{"type":"string","title":"Status"},"reviewer_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewer Notes"},"approved_publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Approved Publisher Id"},"submitted_at":{"type":"string","title":"Submitted At"},"reviewed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewed At"},"source_ip_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Ip Bucket"}},"type":"object","required":["id","publication_name","contact_email","payout_email","payout_method","status","submitted_at"],"title":"PublisherApplicationView"},"PublisherCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"tenant_slug":{"type":"string","maxLength":120,"minLength":1,"title":"Tenant Slug"},"payout_rail":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Payout Rail"}},"type":"object","required":["name","tenant_slug"],"title":"PublisherCreate"},"PublisherNamePatch":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"apply_to_sources":{"type":"boolean","title":"Apply To Sources","default":true}},"type":"object","required":["name"],"title":"PublisherNamePatch"},"PublisherOnboardRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"},"tenant_slug":{"anyOf":[{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$"},{"type":"null"}],"title":"Tenant Slug"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"feed_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Feed Url"},"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"},"authorization_accepted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Authorization Accepted"},"terms_version":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Terms Version"}},"type":"object","required":["contact_email"],"title":"PublisherOnboardRequest"},"PublisherOnboardResponse":{"properties":{"publisher_id":{"type":"string","title":"Publisher Id"},"tenant_slug":{"type":"string","title":"Tenant Slug"},"access_token":{"type":"string","title":"Access Token"},"intake_source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Intake Source Id"},"status":{"type":"string","title":"Status","default":"pending"},"message":{"type":"string","title":"Message"},"resolved_feed_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved Feed Url"},"resolved_via":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved Via"}},"type":"object","required":["publisher_id","tenant_slug","access_token","message"],"title":"PublisherOnboardResponse"},"PublisherOptInRequest":{"properties":{"contact_email_override":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Contact Email Override"},"payout_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Payout Email"},"payout_method":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Payout Method"},"payout_address":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Payout Address"}},"type":"object","title":"PublisherOptInRequest"},"PublisherOptInResponse":{"properties":{"publisher_id":{"type":"string","title":"Publisher Id"},"tenant_slug":{"type":"string","title":"Tenant Slug"},"hex_code":{"type":"string","title":"Hex Code"},"opt_in_at":{"type":"string","title":"Opt In At"},"onboarded_at":{"type":"string","title":"Onboarded At"},"already_opted_in":{"type":"boolean","title":"Already Opted In"}},"type":"object","required":["publisher_id","tenant_slug","hex_code","opt_in_at","onboarded_at","already_opted_in"],"title":"PublisherOptInResponse"},"PublisherRemovalPatch":{"properties":{"revoked":{"type":"boolean","title":"Revoked","description":"true = removal is on record; ingestion mints no further rows. false = clear the stamp (does not resume paused sources)."},"reason":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Reason","description":"Operator provenance — where the request came from. e.g. 'C&D via inbox msg 412, 2026-06-05'. Stored on publishers.takedown_reason."}},"type":"object","required":["revoked"],"title":"PublisherRemovalPatch","description":"``revoked`` is required and has no default — recording a removal and\nundoing one must both be typed out, never inferred from an empty body."},"PublisherView":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"tenant_slug":{"type":"string","title":"Tenant Slug"},"payout_rail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Rail"},"created_at":{"type":"string","title":"Created At"},"icon_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Url"}},"type":"object","required":["id","name","tenant_slug","payout_rail","created_at"],"title":"PublisherView"},"RegisterWebhookRequest":{"properties":{"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url","description":"Full URL Telegram should POST updates to. Defaults to `https://api.storyflo.com/v1/telegram/webhook` (env override TELEGRAM_WEBHOOK_PUBLIC_URL)."}},"type":"object","title":"RegisterWebhookRequest","description":"Body of POST /v1/admin/telegram/register-webhook."},"RegisterWebhookResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"webhook_url":{"type":"string","title":"Webhook Url"},"telegram_response":{"additionalProperties":true,"type":"object","title":"Telegram Response"}},"type":"object","required":["ok","webhook_url","telegram_response"],"title":"RegisterWebhookResponse"},"RenderEnqueueResponse":{"properties":{"id":{"type":"string","title":"Id"},"enqueued":{"type":"boolean","title":"Enqueued"},"note":{"type":"string","title":"Note"}},"type":"object","required":["id","enqueued","note"],"title":"RenderEnqueueResponse"},"RenderRequest":{"properties":{"voice":{"type":"string","title":"Voice","description":"Public voice id — atlas, vox, kira, rune"},"text":{"type":"string","maxLength":200000,"minLength":1,"title":"Text"},"speed":{"type":"number","maximum":2.0,"minimum":0.5,"title":"Speed","default":1.0},"force_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Force Provider","description":"Override router; one of: kokoro, elevenlabs, cartesia, styletts2"},"listener_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Listener Token","description":"When supplied, the renderer enforces the listener's tier. Free tier is forced to the kokoro provider regardless of preference order; plus tier uses the configured order."}},"type":"object","required":["voice","text"],"title":"RenderRequest"},"RenderResponse":{"properties":{"audio_url":{"type":"string","title":"Audio Url"},"cache_key":{"type":"string","title":"Cache Key"},"provider":{"type":"string","title":"Provider"},"cache_hit":{"type":"boolean","title":"Cache Hit"},"duration_sec":{"type":"number","title":"Duration Sec"},"cost_usd":{"type":"number","title":"Cost Usd"},"sample_rate":{"type":"integer","title":"Sample Rate"}},"type":"object","required":["audio_url","cache_key","provider","cache_hit","duration_sec","cost_usd","sample_rate"],"title":"RenderResponse"},"RunNowBody":{"properties":{"lookback_hours":{"type":"integer","maximum":336.0,"minimum":1.0,"title":"Lookback Hours","default":24},"dry_run":{"type":"boolean","title":"Dry Run","default":false}},"type":"object","title":"RunNowBody"},"ScanBody":{"properties":{"text":{"type":"string","maxLength":200000,"minLength":1,"title":"Text"},"scrub":{"type":"boolean","title":"Scrub","description":"If true, also return the scrubbed text","default":false}},"type":"object","required":["text"],"title":"ScanBody"},"ScanHit":{"properties":{"category":{"type":"string","title":"Category"},"severity":{"type":"string","title":"Severity"},"pattern_name":{"type":"string","title":"Pattern Name"},"sample":{"type":"string","title":"Sample"},"start":{"type":"integer","title":"Start"},"end":{"type":"integer","title":"End"}},"type":"object","required":["category","severity","pattern_name","sample","start","end"],"title":"ScanHit"},"ScanResponse":{"properties":{"total":{"type":"integer","title":"Total"},"hard":{"type":"integer","title":"Hard"},"soft":{"type":"integer","title":"Soft"},"by_category":{"additionalProperties":true,"type":"object","title":"By Category"},"hits":{"items":{"$ref":"#/components/schemas/ScanHit"},"type":"array","title":"Hits"},"scrubbed_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrubbed Text"}},"type":"object","required":["total","hard","soft","by_category","hits"],"title":"ScanResponse"},"SeedRequest":{"properties":{"domain":{"type":"string","maxLength":255,"minLength":3,"title":"Domain"}},"type":"object","required":["domain"],"title":"SeedRequest"},"SeedResponse":{"properties":{"domain":{"type":"string","title":"Domain"},"discovered":{"type":"integer","title":"Discovered"},"inserted":{"type":"integer","title":"Inserted"},"skipped_existing":{"type":"integer","title":"Skipped Existing"}},"type":"object","required":["domain","discovered","inserted","skipped_existing"],"title":"SeedResponse"},"SetLanguageBody":{"properties":{"language":{"type":"string","maxLength":8,"minLength":2,"title":"Language"}},"type":"object","required":["language"],"title":"SetLanguageBody"},"ShareTokenResponse":{"properties":{"token":{"type":"string","title":"Token","description":"opaque url-safe token"},"expires_at":{"type":"string","title":"Expires At","description":"RFC 3339 timestamp when this token stops working"},"ttl_seconds":{"type":"integer","title":"Ttl Seconds","description":"lifetime in seconds"},"shared_url_path":{"type":"string","title":"Shared Url Path","description":"API path the token-holder can GET to read the snapshot — frontend prepends the storyflo.com origin"}},"type":"object","required":["token","expires_at","ttl_seconds","shared_url_path"],"title":"ShareTokenResponse"},"ShowCreate":{"properties":{"feed_url":{"type":"string","maxLength":2048,"minLength":4,"title":"Feed Url"},"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"},"tenant_slug":{"anyOf":[{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$"},{"type":"null"}],"title":"Tenant Slug"},"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"},"allow_duplicate_feed":{"type":"boolean","title":"Allow Duplicate Feed","default":false}},"type":"object","required":["feed_url"],"title":"ShowCreate"},"ShowNamePatch":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"tenant_slug":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Tenant Slug"}},"type":"object","required":["name"],"title":"ShowNamePatch"},"SubstackClickThroughRequest":{"properties":{"publisher_slug":{"type":"string","title":"Publisher Slug"},"article_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Article Slug"}},"type":"object","required":["publisher_slug"],"title":"SubstackClickThroughRequest"},"TtsRenderBody":{"properties":{"text":{"type":"string","maxLength":9500,"minLength":1,"title":"Text"},"voice_id":{"type":"string","pattern":"^[A-Za-z0-9]{8,64}$","title":"Voice Id","default":"cjVigY5qzO86Huf0OWal"}},"type":"object","required":["text"],"title":"TtsRenderBody"},"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"},"VoiceCommandBody":{"properties":{"listener_token":{"type":"string","maxLength":120,"minLength":8,"title":"Listener Token"},"article_slug":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Article Slug"},"position_seconds":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Position Seconds"}},"type":"object","required":["listener_token"],"title":"VoiceCommandBody"},"VoiceOverrideRequest":{"properties":{"voice_id":{"type":"string","maxLength":64,"minLength":1,"title":"Voice Id"}},"type":"object","required":["voice_id"],"title":"VoiceOverrideRequest"},"WebIntakeRequest":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":4,"title":"Url"}},"type":"object","required":["url"],"title":"WebIntakeRequest"},"WebhookCreate":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url"},"events":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Events"},"publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"}},"type":"object","required":["url","events"],"title":"WebhookCreate"},"WebhookCreateResponse":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string"},"type":"array","title":"Events"},"active":{"type":"boolean","title":"Active"},"publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"},"created_at":{"type":"string","title":"Created At"},"secret":{"type":"string","title":"Secret"}},"type":"object","required":["id","url","events","active","publisher_id","created_at","secret"],"title":"WebhookCreateResponse"},"WebhookView":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string"},"type":"array","title":"Events"},"active":{"type":"boolean","title":"Active"},"publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","url","events","active","publisher_id","created_at"],"title":"WebhookView"},"_AccessRequestBody":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"_AccessRequestBody"},"_AdCreativeCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"advertiser_label":{"type":"string","maxLength":255,"minLength":1,"title":"Advertiser Label"},"audio_url":{"type":"string","maxLength":2048,"minLength":1,"title":"Audio Url"},"duration_sec":{"type":"integer","maximum":60.0,"minimum":1.0,"title":"Duration Sec"},"click_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Click Url"},"target_verticals":{"items":{"type":"string"},"type":"array","title":"Target Verticals"},"cpm_micros":{"type":"integer","minimum":0.0,"title":"Cpm Micros","default":0},"daily_impression_cap":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Daily Impression Cap"},"status":{"anyOf":[{"type":"string","pattern":"^(active|paused|ended)$"},{"type":"null"}],"title":"Status","default":"active"}},"type":"object","required":["name","advertiser_label","audio_url","duration_sec"],"title":"_AdCreativeCreate"},"_AdCreativePatch":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"audio_url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":1},{"type":"null"}],"title":"Audio Url"},"click_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Click Url"},"duration_sec":{"anyOf":[{"type":"integer","maximum":60.0,"minimum":1.0},{"type":"null"}],"title":"Duration Sec"},"target_verticals":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Target Verticals"},"cpm_micros":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Cpm Micros"},"daily_impression_cap":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Daily Impression Cap"},"status":{"anyOf":[{"type":"string","pattern":"^(active|paused|ended)$"},{"type":"null"}],"title":"Status"}},"type":"object","title":"_AdCreativePatch"},"_AdImpressionRequest":{"properties":{"creative_id":{"type":"string","maxLength":64,"minLength":1,"title":"Creative Id"},"article_slug":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Article Slug"},"played_seconds":{"type":"number","minimum":0.0,"title":"Played Seconds","default":0.0},"completed":{"type":"boolean","title":"Completed","default":false}},"type":"object","required":["creative_id"],"title":"_AdImpressionRequest"},"_AdminCancelledAudioCleanupRequest":{"properties":{"publisher_name_pattern":{"type":"string","title":"Publisher Name Pattern","default":"Google News%"},"dry_run":{"type":"boolean","title":"Dry Run","default":true},"limit":{"type":"integer","maximum":50000.0,"minimum":1.0,"title":"Limit","default":5000},"batch_size":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Batch Size","default":1000}},"type":"object","title":"_AdminCancelledAudioCleanupRequest"},"_AdminPreWarmRequest":{"properties":{"count":{"type":"integer","maximum":10.0,"minimum":1.0,"title":"Count","default":5},"force":{"type":"boolean","title":"Force","default":false}},"type":"object","title":"_AdminPreWarmRequest"},"_AdvertiserCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"contact_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Contact Email"},"company_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Company Url"},"desired_verticals":{"items":{"type":"string"},"type":"array","maxItems":32,"title":"Desired Verticals"},"audience_notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Audience Notes"},"budget_usd_monthly":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Budget Usd Monthly"},"target_cpm_micros":{"type":"integer","minimum":0.0,"title":"Target Cpm Micros","default":0},"status":{"anyOf":[{"type":"string","pattern":"^(pending|active|paused|archived)$"},{"type":"null"}],"title":"Status","default":"pending"},"creative_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Creative Id"},"notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Notes"},"source":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Source"}},"type":"object","required":["name"],"title":"_AdvertiserCreate"},"_AdvertiserIntake":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"company_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Company Url"},"desired_verticals":{"items":{"type":"string"},"type":"array","maxItems":32,"title":"Desired Verticals"},"audience_notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Audience Notes"},"budget_usd_monthly":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Budget Usd Monthly"}},"type":"object","required":["name","contact_email"],"title":"_AdvertiserIntake","description":"Public / partner intake — a lighter, un-privileged registration.\nAlways lands as ``pending`` for operator review; ranking + operator\nfields (target_cpm, creative_id, notes) are not settable here."},"_AdvertiserPatch":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"contact_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Contact Email"},"company_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Company Url"},"desired_verticals":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":32},{"type":"null"}],"title":"Desired Verticals"},"audience_notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Audience Notes"},"budget_usd_monthly":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Budget Usd Monthly"},"target_cpm_micros":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Target Cpm Micros"},"status":{"anyOf":[{"type":"string","pattern":"^(pending|active|paused|archived)$"},{"type":"null"}],"title":"Status"},"creative_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Creative Id"},"notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Notes"}},"type":"object","title":"_AdvertiserPatch"},"_AgentDigestRequest":{"properties":{"verticals":{"items":{"type":"string"},"type":"array","title":"Verticals"},"window":{"type":"string","pattern":"^(24h|7d|30d)$","title":"Window","default":"24h"},"limit":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"Limit","default":5}},"type":"object","title":"_AgentDigestRequest"},"_AgentRegister":{"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"contact_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Contact Email"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"recommender_address":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Recommender Address"}},"type":"object","required":["name"],"title":"_AgentRegister"},"_AgentSubscribeRequest":{"properties":{"verticals":{"items":{"type":"string"},"type":"array","title":"Verticals"}},"type":"object","title":"_AgentSubscribeRequest"},"_ArticlePatch":{"properties":{"vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"},"title":{"anyOf":[{"type":"string","maxLength":500,"minLength":1},{"type":"null"}],"title":"Title"},"url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":4},{"type":"null"}],"title":"Url"},"is_landing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Landing"}},"type":"object","title":"_ArticlePatch"},"_ArticleUnlockBody":{"properties":{"tx_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","title":"Tx Hash"},"network":{"type":"string","pattern":"^(base|base-sepolia)$","title":"Network","default":"base"}},"type":"object","required":["tx_hash"],"title":"_ArticleUnlockBody"},"_AssignBody":{"properties":{"experiment_key":{"type":"string","maxLength":80,"minLength":1,"title":"Experiment Key"},"variants":{"items":{"type":"string"},"type":"array","maxItems":10,"minItems":2,"title":"Variants"},"listener_token":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Listener Token"}},"type":"object","required":["experiment_key","variants"],"title":"_AssignBody"},"_AudioCacheSweepBody":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","default":true},"min_bytes":{"type":"integer","maximum":1000000.0,"minimum":0.0,"title":"Min Bytes","default":5000},"deep":{"type":"boolean","title":"Deep","default":false},"deep_min_duration_s":{"type":"number","maximum":60.0,"minimum":0.0,"title":"Deep Min Duration S","default":3.0},"limit":{"type":"integer","maximum":200000.0,"minimum":1.0,"title":"Limit","default":5000},"start_after":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start After"}},"type":"object","title":"_AudioCacheSweepBody","description":"Body for the audio-cache sweep-empty endpoint.\n\n``min_bytes`` defaults to 5000 — anything below that cannot be a\nvalid >1s WAV at 48kHz mono 16-bit (header + ~1s of PCM = 96kB; we\nuse 5kB as a conservative floor that catches truncated streams\nwithout risking any legit short chunk)."},"_AudioQualityAuditBody":{"properties":{"since":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"},"limit":{"type":"integer","maximum":500.0,"minimum":1.0,"title":"Limit","default":50},"dry_run":{"type":"boolean","title":"Dry Run","default":true},"min_bytes":{"type":"integer","maximum":10000000.0,"minimum":0.0,"title":"Min Bytes","default":5000},"bytes_per_sec":{"type":"integer","maximum":10000000.0,"minimum":1.0,"title":"Bytes Per Sec","default":44100},"suspicious_min_words":{"type":"integer","maximum":10000.0,"minimum":0.0,"title":"Suspicious Min Words","default":200},"suspicious_max_duration_sec":{"type":"number","maximum":600.0,"minimum":0.0,"title":"Suspicious Max Duration Sec","default":5.0},"concurrency":{"type":"integer","maximum":16.0,"minimum":1.0,"title":"Concurrency","default":8}},"type":"object","title":"_AudioQualityAuditBody","description":"Operator request shape for the post-rerender audio audit."},"_AuthorOnboardRequest":{"properties":{"sender_email":{"type":"string","title":"Sender Email"},"sender_domain":{"type":"string","title":"Sender Domain"},"forward_count":{"type":"integer","title":"Forward Count"},"days":{"type":"integer","title":"Days","default":30}},"type":"object","required":["sender_email","sender_domain","forward_count"],"title":"_AuthorOnboardRequest"},"_AuthorizeRequest":{"properties":{"response_type":{"type":"string","pattern":"^code$","title":"Response Type"},"client_id":{"type":"string","maxLength":80,"minLength":1,"title":"Client Id"},"redirect_uri":{"type":"string","maxLength":2048,"minLength":1,"title":"Redirect Uri"},"scope":{"type":"string","title":"Scope","default":"articles:read"},"state":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"State"},"code_challenge":{"type":"string","maxLength":255,"minLength":1,"title":"Code Challenge"},"code_challenge_method":{"type":"string","pattern":"^(S256|plain)$","title":"Code Challenge Method","default":"S256"},"listener_token":{"type":"string","maxLength":64,"minLength":1,"title":"Listener Token"},"referral_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Referral Token"}},"type":"object","required":["response_type","client_id","redirect_uri","code_challenge","listener_token"],"title":"_AuthorizeRequest"},"_BackfillIntakeBody":{"properties":{"publisher_slugs":{"items":{"type":"string"},"type":"array","title":"Publisher Slugs"},"dry_run":{"type":"boolean","title":"Dry Run","default":true},"max_articles_per_publisher":{"type":"integer","maximum":50.0,"minimum":1.0,"title":"Max Articles Per Publisher","default":5}},"type":"object","title":"_BackfillIntakeBody"},"_BatchResponse":{"properties":{"checked":{"type":"integer","title":"Checked"},"failed":{"type":"integer","title":"Failed"},"full_verify":{"type":"boolean","title":"Full Verify"},"failures":{"items":{"$ref":"#/components/schemas/_BatchRow"},"type":"array","title":"Failures"}},"type":"object","required":["checked","failed","full_verify","failures"],"title":"_BatchResponse"},"_BatchRow":{"properties":{"article_id":{"type":"string","title":"Article Id"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"ok":{"type":"boolean","title":"Ok"},"issue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Issue"}},"type":"object","required":["article_id","ok"],"title":"_BatchRow"},"_BurstCheckoutRequest":{"properties":{"pack":{"type":"string","pattern":"^(burst25|burst100)$","title":"Pack"}},"type":"object","required":["pack"],"title":"_BurstCheckoutRequest"},"_ByoTtsSetBody":{"properties":{"provider":{"type":"string","pattern":"^(openai|elevenlabs)$","title":"Provider"},"api_key":{"type":"string","maxLength":512,"minLength":8,"title":"Api Key"}},"type":"object","required":["provider","api_key"],"title":"_ByoTtsSetBody"},"_CheckoutRequest":{"properties":{"plan":{"type":"string","pattern":"^(monthly|annual)$","title":"Plan","default":"monthly"},"tier":{"type":"string","pattern":"^(plus|pro)$","title":"Tier","default":"plus"},"ui_mode":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ui Mode","default":"hosted"},"return_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Return Url"}},"type":"object","title":"_CheckoutRequest"},"_CheckoutResponse":{"properties":{"checkout_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checkout Url"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"},"listener_token":{"type":"string","title":"Listener Token"},"plan":{"type":"string","title":"Plan"}},"type":"object","required":["listener_token","plan"],"title":"_CheckoutResponse"},"_CircleTestTransferRequest":{"properties":{"amount_usd":{"type":"number","maximum":10.0,"exclusiveMinimum":0.0,"title":"Amount Usd"},"destination_address":{"type":"string","maxLength":80,"title":"Destination Address"},"dry_run":{"type":"boolean","title":"Dry Run","default":true}},"type":"object","required":["amount_usd","destination_address"],"title":"_CircleTestTransferRequest"},"_ClaimAcceptRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"payout_wallet_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Wallet Address"},"subscriptions_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Subscriptions Enabled"},"authorization_accepted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Authorization Accepted"},"terms_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Terms Version"}},"type":"object","title":"_ClaimAcceptRequest"},"_ClassifyTestBody":{"properties":{"paragraphs":{"items":{"type":"string"},"type":"array","title":"Paragraphs"},"subject":{"type":"string","title":"Subject","default":""},"sender":{"type":"string","title":"Sender","default":""}},"type":"object","title":"_ClassifyTestBody"},"_ClassifyTestResponse":{"properties":{"category":{"type":"string","title":"Category"},"confidence":{"type":"number","title":"Confidence"},"signals_fired":{"items":{"type":"string"},"type":"array","title":"Signals Fired"}},"type":"object","required":["category","confidence","signals_fired"],"title":"_ClassifyTestResponse"},"_ColdBatchBody":{"properties":{"limit":{"type":"integer","title":"Limit","default":25},"dry_run":{"type":"boolean","title":"Dry Run","default":false},"slugs":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Slugs"},"batch_tag":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Batch Tag"},"template_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Key"}},"type":"object","title":"_ColdBatchBody"},"_CommentCreateRequest":{"properties":{"body":{"type":"string","maxLength":1500,"minLength":1,"title":"Body"},"parent_comment_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Parent Comment Id"}},"type":"object","required":["body"],"title":"_CommentCreateRequest"},"_CommentEditRequest":{"properties":{"body":{"type":"string","maxLength":1500,"minLength":1,"title":"Body"}},"type":"object","required":["body"],"title":"_CommentEditRequest"},"_ConvertBody":{"properties":{"experiment_key":{"type":"string","maxLength":80,"minLength":1,"title":"Experiment Key"},"listener_token":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Listener Token"}},"type":"object","required":["experiment_key"],"title":"_ConvertBody"},"_CreatorClaimAcceptRequest":{"properties":{"contact_email_confirm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Email Confirm"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"subscriptions_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Subscriptions Enabled"},"authorization_accepted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Authorization Accepted"},"terms_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Terms Version"}},"type":"object","title":"_CreatorClaimAcceptRequest"},"_CurationApproveBody":{"properties":{"reviewer":{"type":"string","title":"Reviewer"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["reviewer"],"title":"_CurationApproveBody"},"_CurationRejectBody":{"properties":{"reviewer":{"type":"string","title":"Reviewer"},"notes":{"type":"string","title":"Notes"}},"type":"object","required":["reviewer","notes"],"title":"_CurationRejectBody"},"_DCRRequest":{"properties":{"client_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Client Name"},"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"grant_types":{"items":{"type":"string"},"type":"array","title":"Grant Types"},"response_types":{"items":{"type":"string"},"type":"array","title":"Response Types"},"scope":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Scope"},"token_endpoint_auth_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Endpoint Auth Method","default":"none"},"contacts":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Contacts"},"client_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Uri"},"logo_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Uri"},"tos_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tos Uri"},"policy_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Policy Uri"},"software_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Software Id"},"software_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Software Version"}},"type":"object","title":"_DCRRequest","description":"RFC 7591 client metadata. We accept the well-defined fields and\nsilently ignore anything else (RFC 7591 §2 requires forward-compat)."},"_DLQItem":{"properties":{"id":{"type":"string","title":"Id"},"article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Article Id"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"attempt":{"type":"integer","title":"Attempt","default":1},"last_failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure Reason"},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure At"},"next_attempt_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Attempt At"},"dead_lettered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dead Lettered At"},"queued_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Queued At"}},"type":"object","required":["id"],"title":"_DLQItem"},"_DLQListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/_DLQItem"},"type":"array","title":"Items"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","limit","offset"],"title":"_DLQListResponse"},"_DedupeArticlesBody":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","default":true},"limit":{"type":"integer","maximum":5000.0,"minimum":1.0,"title":"Limit","default":100}},"type":"object","title":"_DedupeArticlesBody"},"_DigestBody":{"properties":{"since":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"}},"type":"object","title":"_DigestBody","description":"Optional `since` overrides the default 24h window."},"_DiscoverFeedBody":{"properties":{"slug":{"type":"string","title":"Slug"},"auto_seed":{"type":"boolean","title":"Auto Seed","default":true}},"type":"object","required":["slug"],"title":"_DiscoverFeedBody"},"_DistributionMarkRequest":{"properties":{"status":{"type":"string","title":"Status","description":"pending|submitted|live|failed|skipped"},"listing_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Listing Url"},"notes":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Notes"}},"type":"object","required":["status"],"title":"_DistributionMarkRequest"},"_EditorialPickCreate":{"properties":{"article_slug":{"type":"string","title":"Article Slug"},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position","default":0},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"note":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}],"title":"Note"}},"type":"object","required":["article_slug"],"title":"_EditorialPickCreate"},"_EditorialPickPatch":{"properties":{"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"note":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}],"title":"Note"}},"type":"object","title":"_EditorialPickPatch"},"_EditorialPickReorder":{"properties":{"pick_ids":{"items":{"type":"string"},"type":"array","title":"Pick Ids"}},"type":"object","required":["pick_ids"],"title":"_EditorialPickReorder"},"_EmailRelayToggle":{"properties":{"enabled":{"type":"boolean","title":"Enabled"}},"type":"object","required":["enabled"],"title":"_EmailRelayToggle"},"_EmbedTestRequest":{"properties":{"url":{"type":"string","title":"Url"},"dry_run":{"type":"boolean","title":"Dry Run","default":false}},"type":"object","required":["url"],"title":"_EmbedTestRequest"},"_FaucetClaim":{"properties":{"listener_token":{"type":"string","maxLength":128,"minLength":8,"title":"Listener Token"},"wallet_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},{"type":"null"}],"title":"Wallet Address"}},"type":"object","required":["listener_token"],"title":"_FaucetClaim"},"_FireBatchBody":{"properties":{"batch_size":{"type":"integer","maximum":50.0,"minimum":1.0,"title":"Batch Size","default":20},"persona":{"type":"string","title":"Persona","default":"auto"},"dry_run":{"type":"boolean","title":"Dry Run","default":true},"publisher_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Publisher Ids"},"publisher_slugs":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Publisher Slugs"},"allow_book_authors":{"type":"boolean","title":"Allow Book Authors","default":false},"subject_variant":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Subject Variant"}},"type":"object","title":"_FireBatchBody"},"_FloBlockUpdate":{"properties":{"verticals":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verticals"},"tone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tone"},"voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Id"},"playback_speed":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Playback Speed"},"manual_order":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Manual Order"}},"type":"object","title":"_FloBlockUpdate"},"_FloUpdate":{"properties":{"morning":{"anyOf":[{"$ref":"#/components/schemas/_FloBlockUpdate"},{"type":"null"}]},"midday":{"anyOf":[{"$ref":"#/components/schemas/_FloBlockUpdate"},{"type":"null"}]},"evening":{"anyOf":[{"$ref":"#/components/schemas/_FloBlockUpdate"},{"type":"null"}]},"night":{"anyOf":[{"$ref":"#/components/schemas/_FloBlockUpdate"},{"type":"null"}]}},"type":"object","title":"_FloUpdate"},"_GAReportBody":{"properties":{"metrics":{"items":{"type":"string"},"type":"array","title":"Metrics"},"dimensions":{"items":{"type":"string"},"type":"array","title":"Dimensions"},"days":{"type":"integer","maximum":365.0,"minimum":1.0,"title":"Days","default":30}},"type":"object","title":"_GAReportBody"},"_GeminiGenerateBody":{"properties":{"prompt":{"type":"string","maxLength":40000,"minLength":1,"title":"Prompt"},"max_tokens":{"type":"integer","maximum":32000.0,"minimum":1.0,"title":"Max Tokens","default":2000},"model":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Model"}},"type":"object","required":["prompt"],"title":"_GeminiGenerateBody"},"_HistoryRecord":{"properties":{"article_slug":{"type":"string","maxLength":200,"minLength":1,"title":"Article Slug"},"listened_seconds":{"type":"number","minimum":0.0,"title":"Listened Seconds","default":0.0},"completed":{"type":"boolean","title":"Completed","default":false},"voice_id":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Voice Id"}},"type":"object","required":["article_slug"],"title":"_HistoryRecord"},"_ImpJsonBody":{"properties":{"slug":{"type":"string","maxLength":120,"title":"Slug"},"size":{"type":"string","maxLength":20,"title":"Size","default":"300x250"},"context":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Context"},"ts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ts"}},"type":"object","required":["slug"],"title":"_ImpJsonBody"},"_InboxReplyDraftRequest":{"properties":{"tone":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Tone","default":"warm"}},"type":"object","title":"_InboxReplyDraftRequest"},"_InboxReplySendRequest":{"properties":{"to":{"type":"string","maxLength":320,"title":"To"},"subject":{"type":"string","maxLength":300,"title":"Subject"},"body":{"type":"string","maxLength":5000,"title":"Body"}},"type":"object","required":["to","subject","body"],"title":"_InboxReplySendRequest"},"_IntakeSeedFeed":{"properties":{"name":{"type":"string","title":"Name"},"tenant_slug":{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$","title":"Tenant Slug"},"feed_url":{"type":"string","maxLength":2048,"minLength":8,"title":"Feed Url"},"homepage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Homepage"},"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"},"priority":{"anyOf":[{"type":"string","pattern":"^(tier1|tier2)$"},{"type":"null"}],"title":"Priority"}},"type":"object","required":["name","tenant_slug","feed_url"],"title":"_IntakeSeedFeed"},"_IntakeSeedRequest":{"properties":{"feeds":{"items":{"$ref":"#/components/schemas/_IntakeSeedFeed"},"type":"array","title":"Feeds"}},"type":"object","required":["feeds"],"title":"_IntakeSeedRequest"},"_LazyRenderRequest":{"properties":{"voice_id":{"type":"string","maxLength":64,"minLength":1,"title":"Voice Id"},"listener_token":{"type":"string","maxLength":128,"minLength":8,"title":"Listener Token"}},"type":"object","required":["voice_id","listener_token"],"title":"_LazyRenderRequest"},"_ListenPingBody":{"properties":{"event":{"type":"string","maxLength":40,"title":"Event"},"listened_seconds":{"type":"integer","maximum":86400.0,"minimum":0.0,"title":"Listened Seconds","default":0}},"type":"object","required":["event"],"title":"_ListenPingBody"},"_ListenerFeedback":{"properties":{"article_slug":{"type":"string","maxLength":200,"minLength":1,"title":"Article Slug"},"action":{"type":"string","pattern":"^(save|unsave|dismiss|undismiss|skip)$","title":"Action"}},"type":"object","required":["article_slug","action"],"title":"_ListenerFeedback"},"_ListenerOpmlImportRequest":{"properties":{"opml_xml":{"type":"string","maxLength":2000000,"minLength":8,"title":"Opml Xml"}},"type":"object","required":["opml_xml"],"title":"_ListenerOpmlImportRequest"},"_ListenerPlusCheckoutRequest":{"properties":{"success_url":{"type":"string","title":"Success Url"},"cancel_url":{"type":"string","title":"Cancel Url"},"plan":{"type":"string","pattern":"^(monthly|annual)$","title":"Plan","default":"monthly"}},"type":"object","required":["success_url","cancel_url"],"title":"_ListenerPlusCheckoutRequest"},"_ListenerVerticalPref":{"properties":{"vertical":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}},"type":"object","title":"_ListenerVerticalPref"},"_ListenerVoiceUpdate":{"properties":{"preferred_voice_id":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Preferred Voice Id"}},"type":"object","title":"_ListenerVoiceUpdate"},"_ListenerWalletBind":{"properties":{"wallet_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","title":"Wallet Address"}},"type":"object","required":["wallet_address"],"title":"_ListenerWalletBind"},"_LoginRequestBody":{"properties":{"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"_LoginRequestBody"},"_MarkSentRequest":{"properties":{"tx_hash":{"type":"string","maxLength":80,"minLength":1,"title":"Tx Hash"},"sender_address":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Sender Address"}},"type":"object","required":["tx_hash"],"title":"_MarkSentRequest"},"_MerchantEmbedImpressionBody":{"properties":{"merchant_id":{"type":"string","maxLength":120,"minLength":1,"title":"Merchant Id"},"surface":{"type":"string","pattern":"^(article|feed)$","title":"Surface"},"article_slug":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Article Slug"},"vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"},"listener_token":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Listener Token"}},"type":"object","required":["merchant_id","surface"],"title":"_MerchantEmbedImpressionBody"},"_MintBody":{"properties":{"target_url":{"type":"string","maxLength":2048,"title":"Target Url"},"share_channel":{"type":"string","maxLength":32,"title":"Share Channel","default":"other"},"slug":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Slug"},"kind":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Kind"},"vertical":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Vertical"}},"type":"object","required":["target_url"],"title":"_MintBody"},"_MonetizationSettings":{"properties":{"enabled":{"type":"boolean","title":"Enabled","default":false},"price_per_fetch_usdc":{"type":"number","title":"Price Per Fetch Usdc","default":0.0},"subscriptions_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Subscriptions Enabled"}},"type":"object","title":"_MonetizationSettings"},"_MosSurvey":{"properties":{"article_slug":{"type":"string","maxLength":200,"minLength":1,"title":"Article Slug"},"voice_id":{"type":"string","maxLength":40,"minLength":1,"title":"Voice Id"},"score":{"type":"integer","maximum":5.0,"minimum":1.0,"title":"Score","description":"MOS 1=Bad, 5=Excellent"},"listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Listener Token"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"}},"type":"object","required":["article_slug","voice_id","score"],"title":"_MosSurvey"},"_OAuthClientCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"allowed_scopes":{"items":{"type":"string"},"type":"array","title":"Allowed Scopes"},"tier":{"type":"string","pattern":"^(free|pro|enterprise)$","title":"Tier","default":"free"},"contact_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Contact Email"},"public_client":{"type":"boolean","title":"Public Client","default":false}},"type":"object","required":["name","redirect_uris","allowed_scopes"],"title":"_OAuthClientCreate"},"_OpmlImportRequest":{"properties":{"opml_xml":{"type":"string","maxLength":2000000,"minLength":8,"title":"Opml Xml"},"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"}},"type":"object","required":["opml_xml"],"title":"_OpmlImportRequest"},"_PIIBackfillBody":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","default":true},"limit":{"type":"integer","title":"Limit","default":1000},"also_delete_old_audio":{"type":"boolean","title":"Also Delete Old Audio","default":false}},"type":"object","title":"_PIIBackfillBody"},"_PIISanitizerScanBody":{"properties":{"limit":{"type":"integer","maximum":50000.0,"minimum":1.0,"title":"Limit","default":5000},"dry_run":{"type":"boolean","title":"Dry Run","default":true}},"type":"object","title":"_PIISanitizerScanBody"},"_PageSpeedProbeBody":{"properties":{"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Strategy"}},"type":"object","title":"_PageSpeedProbeBody"},"_PartnersReplyPayload":{"properties":{"MessageID":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Messageid"},"From":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"},"Subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"TextBody":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Textbody"},"HtmlBody":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Htmlbody"},"Headers":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Headers"}},"type":"object","title":"_PartnersReplyPayload"},"_PayoutPreferenceUpdate":{"properties":{"payout_preference":{"type":"string","maxLength":8,"title":"Payout Preference"},"usdc_address":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Usdc Address"},"usdc_split_pct":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Usdc Split Pct"}},"type":"object","required":["payout_preference"],"title":"_PayoutPreferenceUpdate"},"_PaypalPatch":{"properties":{"paypal_payer_id":{"type":"string","maxLength":80,"minLength":1,"title":"Paypal Payer Id"},"payout_settlement_currency":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Payout Settlement Currency","default":"pyusd"}},"type":"object","required":["paypal_payer_id"],"title":"_PaypalPatch"},"_PinBody":{"properties":{"position":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Position","description":"Optional explicit reorder slot — lower numbers sort to the top of the pinned list. Default 0 means 'no explicit order, fall back to pinned_at DESC'."}},"type":"object","title":"_PinBody"},"_PlanCheckoutBody":{"properties":{"tier":{"type":"string","pattern":"^(plus|pro)$","title":"Tier"},"success_url":{"type":"string","maxLength":2048,"minLength":8,"title":"Success Url"},"cancel_url":{"type":"string","maxLength":2048,"minLength":8,"title":"Cancel Url"}},"type":"object","required":["tier","success_url","cancel_url"],"title":"_PlanCheckoutBody"},"_PlayBeaconBody":{"properties":{"resource_ref":{"type":"string","maxLength":255,"minLength":1,"title":"Resource Ref"},"author_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Author Id"},"ms_played":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Ms Played"}},"type":"object","required":["resource_ref"],"title":"_PlayBeaconBody"},"_PublicFeedbackBody":{"properties":{"rating":{"type":"string","pattern":"^(up|down|nps)$","title":"Rating"},"score":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":0.0},{"type":"null"}],"title":"Score"},"comment":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Comment"},"page":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Page"},"listener_token":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Listener Token"}},"type":"object","required":["rating"],"title":"_PublicFeedbackBody","description":"Generic site-wide feedback. Distinct from\n/v1/listener/{token}/feedback which is the per-article save/dismiss\nsignal — this one captures the inline widget's free-text + 👍/👎."},"_PublisherByoTtsBody":{"properties":{"provider":{"type":"string","pattern":"^(openai|elevenlabs)$","title":"Provider"},"api_key":{"type":"string","maxLength":512,"minLength":8,"title":"Api Key"}},"type":"object","required":["provider","api_key"],"title":"_PublisherByoTtsBody"},"_PublisherIconUpdate":{"properties":{"icon_url":{"type":"string","maxLength":2048,"minLength":8,"title":"Icon Url"}},"type":"object","required":["icon_url"],"title":"_PublisherIconUpdate"},"_PublisherIntakePolicyUpdate":{"properties":{"strip_signoff":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Strip Signoff"},"strip_disclaimer":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Strip Disclaimer"}},"type":"object","title":"_PublisherIntakePolicyUpdate","description":"Body for toggling per-publisher intake-stripper opt-ins · 2026-05-16.\n\nBoth flags default OFF for new publishers. Operator opts a publisher\nin after eyeballing forwarded fixtures and deciding the sign-off /\ndisclaimer block is CTA chrome (strip) rather than load-bearing\nbrand voice (keep). PATCH semantics: omitted field = unchanged."},"_PublisherKeyRotate":{"properties":{"confirm":{"type":"boolean","title":"Confirm","default":false}},"type":"object","title":"_PublisherKeyRotate"},"_PublisherKnownBooksUpdate":{"properties":{"books":{"items":{"type":"string"},"type":"array","title":"Books"},"is_book_author":{"type":"boolean","title":"Is Book Author","default":false}},"type":"object","title":"_PublisherKnownBooksUpdate","description":"Body for setting per-author book-IP knobs · 2026-05-16.\n\n`books` is the authoritative list — POSTing replaces (idempotent).\n`is_book_author` flips the publisher into strict mode; when true and\nany generic book signal fires, the filter adds +0.3 to score."},"_PublisherPatch":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"payout_rail":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Payout Rail"},"tier_band":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":0.0},{"type":"null"}],"title":"Tier Band"}},"type":"object","title":"_PublisherPatch"},"_PublisherStripeOnboardRequest":{"properties":{"publisher_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Publisher Email"}},"type":"object","title":"_PublisherStripeOnboardRequest"},"_PublisherVoiceUpdate":{"properties":{"preferred_voice_id":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Preferred Voice Id"}},"type":"object","title":"_PublisherVoiceUpdate"},"_PublisherVoiceboxBody":{"properties":{"engine":{"type":"string","pattern":"^(styletts2|elevenlabs|cartesia|piper|kokoro)$","title":"Engine"},"voice_ref":{"type":"string","maxLength":512,"minLength":1,"title":"Voice Ref"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name"},"consent":{"type":"boolean","title":"Consent","default":false},"consent_ref":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Consent Ref"}},"type":"object","required":["engine","voice_ref"],"title":"_PublisherVoiceboxBody"},"_QAPublisherE2ERequest":{"properties":{"use_testnet":{"type":"boolean","title":"Use Testnet","default":true},"cleanup":{"type":"boolean","title":"Cleanup","default":true}},"type":"object","title":"_QAPublisherE2ERequest","description":"Body for ``POST /v1/admin/qa/publisher-e2e``.\n\nBoth fields default to the operator's most-common case: a real\nend-to-end run with cleanup so the database stays tidy. Pass\n``cleanup=False`` to inspect the synthetic publisher row after the\nsuite — handy when a stage failed and you want to poke at the\npersisted state in the admin UI."},"_ReSanitizeArticleBody":{"properties":{"re_render":{"type":"boolean","title":"Re Render","default":true}},"type":"object","title":"_ReSanitizeArticleBody"},"_ReactionBody":{"properties":{"kind":{"type":"string","pattern":"^(love|skip)$","title":"Kind"}},"type":"object","required":["kind"],"title":"_ReactionBody"},"_RegisterBody":{"properties":{"display_name":{"type":"string","maxLength":255,"minLength":1,"title":"Display Name"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"site_url":{"type":"string","maxLength":2048,"minLength":1,"title":"Site Url"}},"type":"object","required":["display_name","contact_email","site_url"],"title":"_RegisterBody"},"_RegisterDirectBody":{"properties":{"slug":{"type":"string","maxLength":80,"minLength":2,"title":"Slug"},"display_name":{"type":"string","maxLength":255,"minLength":1,"title":"Display Name"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"domain":{"type":"string","maxLength":255,"minLength":3,"title":"Domain"},"site_url":{"type":"string","maxLength":2048,"minLength":1,"title":"Site Url"},"vertical_scope":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical Scope"},"revenue_split_bps":{"type":"integer","maximum":10000.0,"minimum":0.0,"title":"Revenue Split Bps","default":6000},"status":{"type":"string","maxLength":20,"title":"Status","default":"pending_pilot"}},"type":"object","required":["slug","display_name","contact_email","domain","site_url"],"title":"_RegisterDirectBody","description":"Admin-direct create.\n\nNo verify-email round-trip — we trust the operator at the admin-token\nboundary. The row lands in ``status='pending_pilot'`` and the BD team\nflips it to ``approved`` once the pilot agreement is countersigned."},"_RenderCheckBody":{"properties":{"audio_url":{"type":"string","maxLength":4096,"minLength":1,"title":"Audio Url","description":"Local file path OR https:// URL of the rendered MP3 to analyse with ffprobe + ffmpeg. URLs are fetched through the project's SSRF guard."},"expected_word_count":{"anyOf":[{"type":"integer","maximum":1000000.0,"minimum":0.0},{"type":"null"}],"title":"Expected Word Count","description":"Source-text word count. When provided, the duration-ratio check fires (actual vs expected based on ``expected_wpm``)."},"expected_wpm":{"type":"number","maximum":600.0,"exclusiveMinimum":0.0,"title":"Expected Wpm","description":"Narrator words-per-minute. Default 150.","default":150.0}},"type":"object","required":["audio_url"],"title":"_RenderCheckBody"},"_RenderCheckResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"duration_s":{"type":"number","title":"Duration S"},"duration_expected_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration Expected S"},"duration_ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration Ratio"},"rms_db":{"type":"number","title":"Rms Db"},"peak_db":{"type":"number","title":"Peak Db"},"silent_stretches":{"items":{"prefixItems":[{"type":"number"},{"type":"number"}],"type":"array","maxItems":2,"minItems":2},"type":"array","title":"Silent Stretches"},"clipping_pct":{"type":"number","title":"Clipping Pct"},"issues":{"items":{"type":"string"},"type":"array","title":"Issues"}},"type":"object","required":["ok","duration_s","duration_expected_s","duration_ratio","rms_db","peak_db","silent_stretches","clipping_pct","issues"],"title":"_RenderCheckResponse"},"_RenderNowRequest":{"properties":{"listener_token":{"type":"string","maxLength":64,"minLength":8,"title":"Listener Token"}},"type":"object","required":["listener_token"],"title":"_RenderNowRequest","description":"POST body for /v1/articles/{slug}/render-now.\n\n``listener_token`` is required so we can authenticate the request\nand bill / cap to the right listener. A 32-hex token is the\ncanonical shape; the field is bounded to 64 chars to accept legacy\nlonger-form tokens without DoS."},"_RequeueResponse":{"properties":{"requeued":{"type":"boolean","title":"Requeued"},"job_id":{"type":"string","title":"Job Id"}},"type":"object","required":["requeued","job_id"],"title":"_RequeueResponse"},"_RerenderStaleBody":{"properties":{"article_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Article Ids"},"all_stale":{"type":"boolean","title":"All Stale","default":false},"limit":{"type":"integer","maximum":10000.0,"minimum":1.0,"title":"Limit","default":1000},"dry_run":{"type":"boolean","title":"Dry Run","default":true}},"type":"object","title":"_RerenderStaleBody"},"_RescueAsArticleRequest":{"properties":{"listener_email":{"type":"string","title":"Listener Email"},"origin":{"type":"string","title":"Origin","default":"subscribed"}},"type":"object","required":["listener_email"],"title":"_RescueAsArticleRequest"},"_RetryByPatternBody":{"properties":{"pattern":{"type":"string","maxLength":200,"minLength":1,"title":"Pattern"},"max_retry":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Max Retry","default":200}},"type":"object","required":["pattern"],"title":"_RetryByPatternBody"},"_RetryStatsResponse":{"properties":{"since":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"},"by_reason":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"By Reason"},"dead_letter_total":{"type":"integer","title":"Dead Letter Total","default":0}},"type":"object","title":"_RetryStatsResponse"},"_RewardClaimBody":{"properties":{"article_slug":{"type":"string","maxLength":200,"minLength":1,"title":"Article Slug"},"creative_signature":{"type":"string","maxLength":120,"minLength":1,"title":"Creative Signature"},"creative_source":{"type":"string","pattern":"^(direct|ssp:[a-z]+)$","title":"Creative Source"}},"type":"object","required":["article_slug","creative_signature","creative_source"],"title":"_RewardClaimBody"},"_SavedSearchCreate":{"properties":{"query":{"type":"string","maxLength":200,"minLength":2,"title":"Query"},"vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Vertical"}},"type":"object","required":["query"],"title":"_SavedSearchCreate"},"_ScrubLeakedSlugsBody":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","default":true},"limit":{"type":"integer","maximum":5000.0,"minimum":1.0,"title":"Limit","default":200}},"type":"object","title":"_ScrubLeakedSlugsBody"},"_SharedQueueFollowBody":{"properties":{"followee_token":{"type":"string","maxLength":120,"minLength":8,"title":"Followee Token"}},"type":"object","required":["followee_token"],"title":"_SharedQueueFollowBody"},"_SponsoredListing":{"properties":{"article_slug":{"type":"string","maxLength":200,"minLength":1,"title":"Article Slug"},"sponsor_name":{"type":"string","maxLength":120,"minLength":1,"title":"Sponsor Name"},"sponsor_email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Sponsor Email"},"sponsor_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Sponsor Url"},"creative_text":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Creative Text"},"bid_micros_per_play":{"type":"integer","maximum":1000000000.0,"exclusiveMinimum":0.0,"title":"Bid Micros Per Play"},"starts_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ends At"}},"type":"object","required":["article_slug","sponsor_name","bid_micros_per_play"],"title":"_SponsoredListing"},"_SponsorshipReview":{"properties":{"status":{"type":"string","pattern":"^(approved|rejected|paused)$","title":"Status"},"review_note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Review Note"}},"type":"object","required":["status"],"title":"_SponsorshipReview"},"_StripeOnboardingRequest":{"properties":{"publisher_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Publisher Email"}},"type":"object","title":"_StripeOnboardingRequest"},"_StripeOnboardingResponse":{"properties":{"publisher_slug":{"type":"string","title":"Publisher Slug"},"stripe_account_id":{"type":"string","title":"Stripe Account Id"},"onboarding_url":{"type":"string","title":"Onboarding Url"},"charges_enabled":{"type":"boolean","title":"Charges Enabled"},"payouts_enabled":{"type":"boolean","title":"Payouts Enabled"}},"type":"object","required":["publisher_slug","stripe_account_id","onboarding_url","charges_enabled","payouts_enabled"],"title":"_StripeOnboardingResponse"},"_SubscribeBody":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"_SubscribeBody"},"_SubscribeRequest":{"properties":{"tier_id":{"type":"string","title":"Tier Id","default":"paid"},"payment_rail":{"type":"string","title":"Payment Rail","default":"stripe"},"return_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Return Url"}},"type":"object","title":"_SubscribeRequest"},"_SubscriberImportRequest":{"properties":{"csv":{"anyOf":[{"type":"string","maxLength":8000000},{"type":"null"}],"title":"Csv"},"emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Emails"},"source":{"type":"string","maxLength":40,"title":"Source","default":"csv"}},"type":"object","title":"_SubscriberImportRequest"},"_SubscriberImportResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"imported":{"type":"integer","title":"Imported"},"already_present":{"type":"integer","title":"Already Present"},"invalid":{"type":"integer","title":"Invalid"},"total_parsed":{"type":"integer","title":"Total Parsed"},"active_subscribers":{"type":"integer","title":"Active Subscribers"}},"type":"object","required":["ok","imported","already_present","invalid","total_parsed","active_subscribers"],"title":"_SubscriberImportResponse"},"_SubscriptionIntentBody":{"properties":{"tier":{"type":"string","enum":["plus","pro"],"title":"Tier"},"plan":{"type":"string","enum":["monthly","annual"],"title":"Plan","default":"monthly"}},"type":"object","required":["tier"],"title":"_SubscriptionIntentBody"},"_SubstackCrawlAllBody":{"properties":{"limit_per_category":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit Per Category","default":100}},"type":"object","title":"_SubstackCrawlAllBody"},"_SubstackCrawlBody":{"properties":{"categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Categories"},"limit_per_category":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit Per Category","default":25}},"type":"object","title":"_SubstackCrawlBody"},"_SweepResponse":{"properties":{"requeued_count":{"type":"integer","title":"Requeued Count"}},"type":"object","required":["requeued_count"],"title":"_SweepResponse"},"_TierUpgradeBody":{"properties":{"tx_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","title":"Tx Hash"},"plan":{"type":"string","pattern":"^(plus_monthly|plus_yearly)$","title":"Plan"},"network":{"type":"string","pattern":"^(base|base-sepolia)$","title":"Network","default":"base"}},"type":"object","required":["tx_hash","plan"],"title":"_TierUpgradeBody"},"_TipCheckoutRequest":{"properties":{"listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Listener Token"},"amount_usd":{"type":"number","maximum":500.0,"exclusiveMinimum":0.0,"title":"Amount Usd"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"return_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Return Url"},"ui_mode":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ui Mode","default":"hosted"}},"type":"object","required":["amount_usd"],"title":"_TipCheckoutRequest"},"_TipPaymentIntentBody":{"properties":{"amount_usd":{"type":"number","maximum":500.0,"minimum":0.5,"title":"Amount Usd"},"listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Listener Token"},"note":{"anyOf":[{"type":"string","maxLength":280},{"type":"null"}],"title":"Note"}},"type":"object","required":["amount_usd"],"title":"_TipPaymentIntentBody"},"_TipRequest":{"properties":{"listener_token":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Listener Token"},"amount_usd":{"type":"number","maximum":100.0,"exclusiveMinimum":0.0,"title":"Amount Usd"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"tx_hash":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Tx Hash"}},"type":"object","required":["amount_usd"],"title":"_TipRequest"},"_TranslateBody":{"properties":{"text":{"type":"string","maxLength":20000,"minLength":1,"title":"Text"},"target":{"type":"string","maxLength":8,"minLength":2,"title":"Target"},"source":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Source"}},"type":"object","required":["text","target"],"title":"_TranslateBody"},"_UnSuppressRequest":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Reason"}},"type":"object","title":"_UnSuppressRequest","description":"Body for ``POST /v1/admin/declined/{tenant_slug}/un-suppress``.\n\n``reason`` is recorded in the operator audit log (server logs only,\nnot stored on the publisher row); kept under 500 chars to keep the\nlog line bounded."},"_VerdictResponse":{"properties":{"article_id":{"type":"string","title":"Article Id"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"ok":{"type":"boolean","title":"Ok"},"expected_size":{"type":"integer","title":"Expected Size"},"actual_size":{"type":"integer","title":"Actual Size"},"expected_sha256":{"type":"string","title":"Expected Sha256"},"actual_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Actual Sha256"},"issue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Issue"},"full_verify":{"type":"boolean","title":"Full Verify"}},"type":"object","required":["article_id","ok","expected_size","actual_size","expected_sha256","full_verify"],"title":"_VerdictResponse"},"_VerifyBody":{"properties":{"audio_url":{"type":"string","maxLength":4096,"minLength":1,"title":"Audio Url","description":"Local file path OR https:// URL of the rendered MP3 to transcribe and scan. URLs are fetched through the project's SSRF guard."},"language":{"type":"string","maxLength":8,"minLength":2,"title":"Language","description":"BCP-47 language hint. Selects the Whisper model variant (``tiny.en`` for English, ``tiny`` multilingual otherwise).","default":"en"}},"type":"object","required":["audio_url"],"title":"_VerifyBody"},"_VerifyResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"transcript":{"type":"string","title":"Transcript"},"pii_hits":{"items":{"type":"string"},"type":"array","title":"Pii Hits"},"duration_s":{"type":"number","title":"Duration S"},"audio_bytes":{"type":"integer","title":"Audio Bytes"},"model":{"type":"string","title":"Model"}},"type":"object","required":["ok","transcript","pii_hits","duration_s","audio_bytes","model"],"title":"_VerifyResponse"},"_VerticalPatch":{"properties":{"vertical":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Vertical"}},"type":"object","title":"_VerticalPatch"},"_VerticalSettingPatch":{"properties":{"min_cpm_micros":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Min Cpm Micros"},"exclusive_creative_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Exclusive Creative Id"},"notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Notes"}},"type":"object","title":"_VerticalSettingPatch"},"_ViewBeaconBody":{"properties":{"article_id":{"type":"string","maxLength":255,"minLength":1,"title":"Article Id"},"ip":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Ip"}},"type":"object","required":["article_id"],"title":"_ViewBeaconBody"},"_VoiceRerenderRequest":{"properties":{"voice_id":{"type":"string","maxLength":64,"minLength":1,"title":"Voice Id"}},"type":"object","required":["voice_id"],"title":"_VoiceRerenderRequest"},"_WalletPatch":{"properties":{"payout_wallet_address":{"type":"string","maxLength":64,"minLength":1,"title":"Payout Wallet Address"},"payout_currency":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Payout Currency","default":"usdc_base"},"payout_settlement_currency":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Payout Settlement Currency"}},"type":"object","required":["payout_wallet_address"],"title":"_WalletPatch"},"_WebhookSubCreate":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":8,"title":"Url"},"events":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Events"},"description":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Description"}},"type":"object","required":["url","events"],"title":"_WebhookSubCreate"},"_X402ConfirmRequest":{"properties":{"subscription_id":{"type":"string","title":"Subscription Id"},"tx_hash":{"type":"string","title":"Tx Hash"},"chain":{"type":"string","title":"Chain","default":"base-mainnet"}},"type":"object","required":["subscription_id","tx_hash"],"title":"_X402ConfirmRequest"},"_X402DigestRequest":{"properties":{"verticals":{"items":{"type":"string"},"type":"array","title":"Verticals"},"window":{"type":"string","pattern":"^(24h|7d|30d)$","title":"Window","default":"24h"},"limit":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"Limit","default":5}},"type":"object","title":"_X402DigestRequest"},"_YouTubeTrendingBody":{"properties":{"top_n":{"type":"integer","maximum":10.0,"minimum":1.0,"title":"Top N","default":3},"days":{"type":"integer","maximum":30.0,"minimum":1.0,"title":"Days","default":1},"dry_run":{"type":"boolean","title":"Dry Run","default":true}},"type":"object","title":"_YouTubeTrendingBody"},"_YouTubeUploadNowBody":{"properties":{"audio_url":{"type":"string","title":"Audio Url","description":"https URL of the source mp3"},"title":{"type":"string","maxLength":100,"title":"Title"},"description":{"type":"string","maxLength":5000,"title":"Description","default":""},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"}},"type":"object","required":["audio_url","title"],"title":"_YouTubeUploadNowBody"},"app__intake__ad_router__DetectTestBody":{"properties":{"paragraphs":{"items":{"type":"string"},"type":"array","maxItems":500,"minItems":1,"title":"Paragraphs"},"publisher_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publisher Id"}},"type":"object","required":["paragraphs"],"title":"DetectTestBody"},"app__lang__router__DetectTestBody":{"properties":{"text":{"type":"string","maxLength":20000,"minLength":1,"title":"Text"}},"type":"object","required":["text"],"title":"DetectTestBody"}}}}