{"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","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/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/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.","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/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\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":"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/{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. Accepted slugs are the\ncanonical 7 from vertical_classifier.ALL_VERTICALS.","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/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.","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/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.","operationId":"storyflo_public_podcast_v1_podcasts_storyflo_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/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 / yoga / science /\nfinance / news). Each gets its own Spotify directory listing\nsubmission with vertical-flavored channel title + iTunes category.","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"}}}}}}},"/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.","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/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.","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/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/trending":{"get":{"summary":"Trending List","description":"Cross-vertical trending: recent published persona briefings grouped\nby vertical + most-active source publishers in the last 24h. Edge-cached\n60s — trending flips slowly enough that a minute of staleness is fine.","operationId":"trending_list_v1_trending_get","parameters":[{"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 Trending List V1 Trending Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/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.","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"}}],"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/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.","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"}}],"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/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}/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}/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.","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/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.","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/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/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 via base RPC + amount + memo,\nthen mark the subscription active.\n\nFor this initial wedge we accept the tx_hash as a trusted client\nsignal and only verify minimum-amount + recipient on a follow-up\nsweep. Adversarial replays are bounded by the unique constraint\non (publisher_id, listener_email) — a malicious tx_hash can't\ncreate duplicate subscriptions.","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\nCost = sum(render_jobs.cost_micros) for completed jobs in window\nRevenue = sum(plays.ad_revenue_micros) + projected Plus ARPU + x402\n\nPer-publisher breakdown shows margin-per-publisher so the operator\ncan see which publishers earn vs which cost. Unrealized projections\nuse ``app.economics.REVENUE_DEFAULTS_MICROS_PER_LISTEN`` as the\nforward-looking unit value.","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-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-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/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/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}/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}/articles":{"get":{"summary":"Publisher Articles","description":"Publisher's articles, newest first. Each row carries play counts\nin the last 30 days so the dashboard can show momentum without\nextra round-trips.","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.","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.","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/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\nScaffold: no queue-resolution endpoint is wired yet, so we return\n404 with a clear stub message. Follow-up PR will resolve via the\nexisting ``/v1/listeners/{token}/queue`` selector and push the new\n``article_slug`` here.","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``).","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/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/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`) and free-text search\nover from/subject/body.","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":"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 Inbox List V1 Admin Inbox Get"}}}},"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.","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","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\nFive 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\nThe point is signal: how many invitees actually came back? The\ndrop-off between steps tells you which gate to invest in next\n(intake quality, queue UX, audio quality, repeat-listening hooks).","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/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``.","operationId":"admin_daily_brief_run_v1_admin_daily_brief_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 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/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).","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.","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/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).","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/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.","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.\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"}}],"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.","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/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/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.","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":50,"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/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 `vertical_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/voices":{"get":{"summary":"List Voices","description":"Public voice catalog — drives the voice picker on /listen/preferences.\n\nAggregated from the inference router's enabled providers. Read\nstatically here so the listener UI doesn't have to know about\nprovider quirks; if a voice goes offline we keep showing it but\nthe render worker silently routes through Kokoro.","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/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.","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/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/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/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://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":{}}}}}}},"/.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.","operationId":"oauth_dynamic_client_register_oauth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DCRRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","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/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/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/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/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/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"}}}}}}}},"components":{"schemas":{"AckResponse":{"properties":{"id":{"type":"string","title":"Id"},"acknowledged_at":{"type":"string","title":"Acknowledged At"}},"type":"object","required":["id","acknowledged_at"],"title":"AckResponse"},"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"},"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"}},"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"},"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"},"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"},"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"},"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"},"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"}},"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"},"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"},"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"},"PublisherOnboardRequest":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"tenant_slug":{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$","title":"Tenant Slug"},"contact_email":{"type":"string","maxLength":320,"minLength":3,"title":"Contact Email"},"feed_url":{"type":"string","maxLength":2048,"minLength":8,"title":"Feed Url"},"default_vertical":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Default Vertical"}},"type":"object","required":["name","tenant_slug","contact_email","feed_url"],"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":{"type":"string","title":"Intake Source Id"},"status":{"type":"string","title":"Status","default":"pending"},"message":{"type":"string","title":"Message"}},"type":"object","required":["publisher_id","tenant_slug","access_token","intake_source_id","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"},"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"},"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"},"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"},"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"},"_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"},"_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"}},"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"}},"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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"},"_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"}}}}