{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/public/preview-author":{"post":{"tags":["public"],"summary":"Preview Author","description":"Create a preview session for an unauthenticated user.\nScrapes only the author home page and returns preview data.\n\n**Protection**: Cloudflare Turnstile + Rate limiting\n**Rate Limit**: 3/minute and 10/hour, hardcoded in the decorators above.\nNOT configurable per environment — the only thing `settings.rate_limit`\ncontrols is `enabled`, which turns limiting off wholesale for E2E runs.\n\n**Flow**:\n1. User submits Amazon URL\n2. Server validates Turnstile token\n3. Server scrapes author home page only\n4. Server creates temporary session (1-hour TTL)\n5. Returns session_id + preview data\n6. Frontend displays preview and prompts user to sign up\n7. User signs up with session_id in OAuth flow\n8. Server retrieves session and pre-fills author data\n\n**Session Lifecycle**:\n- Created with 1-hour expiration\n- Consumed once during OAuth callback\n- Auto-deleted after 1 hour if not consumed","operationId":"preview_author_public_preview_author_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewAuthorRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewAuthorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/public/health":{"get":{"tags":["public"],"summary":"Public Health","description":"Public health check endpoint","operationId":"public_health_public_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/google/authorize":{"get":{"tags":["auth"],"summary":"Oauth:Google.Auth Backend.Authorize","operationId":"oauth_google_auth_backend_authorize_auth_google_authorize_get","parameters":[{"name":"scopes","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"title":"Scopes"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuth2AuthorizeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/google/callback":{"get":{"tags":["auth"],"summary":"Oauth:Google.Auth Backend.Callback","description":"The response varies based on the authentication backend used.","operationId":"oauth_google_auth_backend_callback_auth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"code_verifier","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Verifier"}},{"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":{}}}},"400":{"content":{"application/json":{"examples":{"INVALID_STATE_TOKEN":{"summary":"Invalid state token."},"LOGIN_BAD_CREDENTIALS":{"summary":"User is inactive.","value":{"detail":"LOGIN_BAD_CREDENTIALS"}}},"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/users/me":{"get":{"tags":["users"],"summary":"Get User","description":"Get current user details.","operationId":"get_user_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSchema"}}}}}}},"/authors/me":{"get":{"tags":["authors"],"summary":"Get My Profile","description":"Get the current user's author profile.","operationId":"get_my_profile_authors_me_get","parameters":[{"name":"include_stats","in":"query","required":false,"schema":{"type":"boolean","description":"Include statistics","default":true,"title":"Include Stats"},"description":"Include statistics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorWithStatsSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["authors"],"summary":"Create My Profile","description":"Create an author profile for the current user.","operationId":"create_my_profile_authors_me_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorCreateSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["authors"],"summary":"Update My Profile","description":"Update the current user's author profile.\n\nRate limited: 10 requests per minute, 100 requests per hour","operationId":"update_my_profile_authors_me_patch","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorUpdateSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/authors/me/onboarding":{"get":{"tags":["authors"],"summary":"Get Onboarding State","description":"Get the current onboarding state with state machine information for recovery.","operationId":"get_onboarding_state_authors_me_onboarding_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnhancedOnboardingStateSchema"}}}}}},"post":{"tags":["authors"],"summary":"Update Onboarding State","description":"Update the onboarding state for the current user's profile.","operationId":"update_onboarding_state_authors_me_onboarding_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingStateSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/authors/claim-session":{"post":{"tags":["authors"],"summary":"Claim Public Session","description":"Claim a public onboarding session after authentication.\nThis endpoint is called by the frontend after OAuth completes.\n\n**Requires Authentication**\n\n**Flow**:\n1. User previews their data (creates public session)\n2. User signs up/logs in (standard OAuth)\n3. Frontend calls this endpoint with session_id\n4. Backend retrieves session and pre-fills author data\n5. User's onboarding is set to skip to SCRAPING step\n\n**Returns**:\n- Success message and redirect destination","operationId":"claim_public_session_authors_claim_session_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimSessionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/authors/me/import/start":{"post":{"tags":["authors"],"summary":"Start Import","description":"Start an import process for the current user's profile.\n\nRate limited: 3 requests per minute, 10 requests per hour","operationId":"start_import_authors_me_import_start_post","parameters":[{"name":"source","in":"query","required":true,"schema":{"$ref":"#/components/schemas/ImportSource"}},{"name":"source_url","in":"query","required":true,"schema":{"type":"string","title":"Source Url"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/authors/me/import/status":{"get":{"tags":["authors"],"summary":"Get Import Status Route","description":"Get the import status for the current user's profile.","operationId":"get_import_status_route_authors_me_import_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportStatusResponse"}}}}}}},"/authors/me/reviews/recent":{"get":{"tags":["authors"],"summary":"Get Recent Reviews","description":"Get recent reviews for the current user's author profile.","operationId":"get_recent_reviews_authors_me_reviews_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"Number of recent reviews to fetch","default":6,"title":"Limit"},"description":"Number of recent reviews to fetch"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites":{"get":{"tags":["websites"],"summary":"Get User Website","description":"Get the current user's website (1:1 relationship).","operationId":"get_user_website_websites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteSchema"}}}}}},"put":{"tags":["websites"],"summary":"Update Website","description":"Update the current user's website.","operationId":"update_website_websites_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteUpdateSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["websites"],"summary":"Create Website","description":"Create a new website for the current user.","operationId":"create_website_websites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteCreateSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["websites"],"summary":"Delete Website","description":"Delete the current user's website.","operationId":"delete_website_websites_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Delete Website Websites Delete"}}}}}}},"/websites/check-path":{"get":{"tags":["websites"],"summary":"Check Path Availability","description":"Check if a website path is available.","operationId":"check_path_availability_websites_check_path_get","parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string","description":"Path to check for availability","title":"Path"},"description":"Path to check for availability"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PathAvailabilitySchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/autosave":{"post":{"tags":["websites"],"summary":"Autosave Website","description":"Autosave website changes without publishing.","operationId":"autosave_website_websites_autosave_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteUpdateSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Autosave Website Websites Autosave Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/preview":{"post":{"tags":["websites"],"summary":"Preview Website","description":"Generate preview HTML for a website without publishing.","operationId":"preview_website_websites_preview_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Preview Website Websites Preview Post"}}}}}}},"/websites/unpublish":{"post":{"tags":["websites"],"summary":"Unpublish Website","description":"Take the author's site offline — a REAL takedown.\n\nDeletes the KV pointer and the R2 tree, so the site stops serving. Flipping\n``is_published`` alone (what this used to do) left the published files\nserving from the CDN with nothing to indicate the author had unpublished.\n\nThe author's data is untouched: pressing Publish rebuilds the site.","operationId":"unpublish_website_websites_unpublish_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Unpublish Website Websites Unpublish Post"}}}}}}},"/websites/publish/start":{"post":{"tags":["websites"],"summary":"Start Async Publish","description":"Start an async publish job - returns immediately.","operationId":"start_async_publish_websites_publish_start_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Start Async Publish Websites Publish Start Post"}}}}}}},"/websites/publish/status":{"get":{"tags":["websites"],"summary":"Get Publish Status","description":"Get the status of a publish job.","operationId":"get_publish_status_websites_publish_status_get","parameters":[{"name":"job_id","in":"query","required":false,"schema":{"type":"string","format":"uuid","description":"Specific job ID to check, or latest if not provided","title":"Job Id"},"description":"Specific job ID to check, or latest if not provided"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Publish Status Websites Publish Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/publish/cancel":{"post":{"tags":["websites"],"summary":"Cancel Publish","description":"Cancel an ongoing publish job.","operationId":"cancel_publish_websites_publish_cancel_post","parameters":[{"name":"job_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"Job ID to cancel","title":"Job Id"},"description":"Job ID to cancel"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Publish Websites Publish Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/has-changes":{"get":{"tags":["websites"],"summary":"Check For Changes","description":"Check if the website has pending changes that need publishing.","operationId":"check_for_changes_websites_has_changes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Check For Changes Websites Has Changes Get"}}}}}}},"/websites/domain/status":{"get":{"tags":["websites"],"summary":"Get Domain Status","description":"Get current domain configuration and verification status.","operationId":"get_domain_status_websites_domain_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainStatusResponse"}}}}}}},"/websites/domain/initiate-verification":{"post":{"tags":["websites"],"summary":"Initiate Domain Verification","description":"Initiate domain verification process. **Requires Premium subscription**","operationId":"initiate_domain_verification_websites_domain_initiate_verification_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateDomainVerificationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateDomainVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/domain/check":{"post":{"tags":["websites"],"summary":"Check Domain","description":"Unified DNS + SSL + apex-probe check; drives approval. **Requires Premium subscription**","operationId":"check_domain_websites_domain_check_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCheckResponse"}}}}}}},"/websites/domain":{"delete":{"tags":["websites"],"summary":"Remove Domain","description":"Remove custom domain from website.","operationId":"remove_domain_websites_domain_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveDomainResponse"}}}}}}},"/websites/domain/check-availability":{"post":{"tags":["websites"],"summary":"Check Domain Availability","description":"Check if a domain is available for use.","operationId":"check_domain_availability_websites_domain_check_availability_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainAvailabilityRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainAvailabilityResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/websites/domain/start-verifying":{"post":{"tags":["websites"],"summary":"Start Verifying","description":"Transition from records to verifying phase. **Requires Premium subscription**","operationId":"start_verifying_websites_domain_start_verifying_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainStatusResponse"}}}}}}},"/billing/dodopayments/webhook":{"post":{"tags":["billing","dodopayments"],"summary":"Dodo Payments Webhook","description":"Webhook endpoint for Dodo Payments events.\nHandles subscription, payment, refund, and dispute events.\n\nSecurity measures:\n- HMAC signature verification (webhook-signature header)\n- Timestamp validation for replay attack prevention (webhook-timestamp header)\n- Rate limiting (100/min, 1000/hour per IP)\n- Webhook ID verification (webhook-id header)\n- standardwebhooks library handles timing attack protection\n\nRate limited: 100 requests per minute, 1000 requests per hour\n\nStatus-code contract:\n- ``401`` ONLY when signature verification (``unwrap``) fails. A bad\n  signature must never reach processing.\n- ``200`` for a verified event that is a duplicate delivery (deduped on the\n  ``webhook-id`` header) — no reprocessing.\n- ``200`` for a successfully processed event (incl. unknown/ignored types).\n- ``500`` when a verified event fails DURING PROCESSING, so Dodo retries.\n  (The previous code returned 401 for processing errors, which told Dodo the\n  signature was bad and silently dropped real events — that was the bug.)","operationId":"dodo_payments_webhook_billing_dodopayments_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/dodopayments/create-checkout":{"post":{"tags":["billing","dodopayments"],"summary":"Create Checkout Endpoint","description":"Create a DodoPayments checkout session for Pro plan upgrade.\n\nRequires authentication.\nReturns a checkout URL to redirect the user to.\n\nArgs:\n    body: Optional request body with billing_cycle (\"monthly\" or \"yearly\", defaults to \"yearly\")\n\nSecurity checks:\n- User must be authenticated\n- User must not already have an active Pro subscription\n- User must have a DodoPayments customer ID","operationId":"create_checkout_endpoint_billing_dodopayments_create_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutRequestSchema","default":{"billing_cycle":"yearly"}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponseSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/dodopayments/preview-change-plan":{"post":{"tags":["billing","dodopayments"],"summary":"Preview Change Plan Endpoint","description":"Preview switching the user's Pro subscription to the other billing cycle.\n\nReturns Dodo's AUTHORITATIVE preview (``is_estimate=False``): today's\nimmediate charge, the new recurring amount, currency, any credit applied, and\nwhen the change takes effect. Does NOT mutate anything.\n\nThe preview is requested with the SAME ``effective_at`` the change will use,\nso the figures match the actual outcome: monthly->yearly is an immediate\nprorated upgrade (``effective='immediate'``, charged now); yearly->monthly is\ndeferred to renewal (``effective='next_billing_date'``, no charge today). NO\ncredit-card details are returned. See the service docstring.","operationId":"preview_change_plan_endpoint_billing_dodopayments_preview_change_plan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingCycleRequestSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewChangePlanResponseSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/dodopayments/change-plan":{"post":{"tags":["billing","dodopayments"],"summary":"Change Plan Endpoint","description":"Change the user's Pro subscription billing cycle in place.\n\nPerforms an in-place plan change via the Dodo SDK (NEVER a new checkout);\nthe subscription id is unchanged. The two directions differ in WHEN they\nland (owner decision, passed via the native ``effective_at`` parameter):\nMonthly->Yearly is an immediate prorated upgrade (``effective='immediate'``,\ncharged now); Yearly->Monthly is DEFERRED to renewal\n(``effective='next_billing_date'``, no charge today — the switch and the new\nmonthly price apply at the next billing date). Final state is reconciled by\nthe ``subscription.plan_changed`` (upgrade) / ``subscription.updated``\n(deferred downgrade) webhook.\n\nRate-limited (and the service serializes on a FOR UPDATE row lock + an\noptimistic cycle marker) so a double-click / retry / two-tab burst cannot\nfire a second change-plan charge or duplicate scheduled change. The marker\nalso SURVIVES the reconciling ``subscription.updated`` webhook: a deferred\ndowngrade is still billed yearly until renewal, so that webhook carries\ninterval='Year', but ``handle_subscription_updated`` keeps the optimistic\nmonthly intent while a monthly ``scheduled_change`` is pending — so a\nduplicate downgrade AFTER the webhook still short-circuits on ``same_cycle``.","operationId":"change_plan_endpoint_billing_dodopayments_change_plan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingCycleRequestSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResponseSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/dodopayments/cancel":{"post":{"tags":["billing","dodopayments"],"summary":"Cancel Subscription Endpoint","description":"Schedule cancellation of the user's Pro subscription at the period end.\n\nCancels at the next billing date (not immediately). Pro access is retained\nthrough ``current_period_end``; the response returns that date. Reflected\nlocally right away and reconciled by the ``subscription.cancelled`` webhook.","operationId":"cancel_subscription_endpoint_billing_dodopayments_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelSubscriptionResponseSchema"}}}}}}},"/billing/dodopayments/resume":{"post":{"tags":["billing","dodopayments"],"summary":"Resume Subscription Endpoint","description":"Un-cancel a scheduled cancellation on the user's Pro subscription.\n\nClears the scheduled cancellation at Dodo and locally restores\n``status=ACTIVE`` with ``cancel_at_period_end=False``. Only valid while the\ncancellation is still scheduled and the paid period is still running.","operationId":"resume_subscription_endpoint_billing_dodopayments_resume_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeSubscriptionResponseSchema"}}}}}}},"/billing/dodopayments/portal-session":{"post":{"tags":["billing","dodopayments"],"summary":"Portal Session Endpoint","description":"Create a Dodo customer-portal session for the user; return its URL.\n\nThe portal lets the user manage payment methods and view invoices. Requires\na Dodo customer id (created at first checkout).","operationId":"portal_session_endpoint_billing_dodopayments_portal_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalSessionResponseSchema"}}}}}}},"/amazon-data":{"post":{"tags":["amazon-data"],"summary":"Scrape Amazon Author Page","description":"Scrape an Amazon author page and return formatted data for the UI.\n\nThis endpoint:\n1. Extracts the author ASIN from the provided URL\n2. Fetches the author's home page data (including all books)\n3. Fetches detailed information for each book\n4. Transforms and returns the data in a UI-friendly format\n\nThe response includes:\n- Author information (name, bio, social links)\n- Series with their books\n- Standalone books\n- Purchase options for each book","operationId":"scrape_amazon_author_page_amazon_data_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmazonScraperRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoreDataResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/amazon-data/scrape-book":{"post":{"tags":["amazon-data"],"summary":"Scrape And Create Book","description":"Scrape a single Amazon book and create it in the database.\n\nThis endpoint:\n1. Extracts the book ASIN from the provided URL\n2. Fetches the book's detailed information\n3. Creates the book in the database with detected genres\n4. Creates Amazon purchase link\n5. Returns the created book data\n\nThis is used in the book form's \"Quick Import from Amazon\" feature.\nSimilar to onboarding flow but for individual books.","operationId":"scrape_and_create_book_amazon_data_scrape_book_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleBookScraperRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleBookScraperResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/amazon-data/start-async":{"post":{"tags":["amazon-data"],"summary":"Start Async Import","description":"Start async import of author data from Amazon.\n\nThis endpoint:\n1. Creates author record with import_status='pending'\n2. Fetches basic author information immediately and updates record\n3. Starts background scraping of all books + series (persists to DB)\n4. Returns immediately with author info and import status\n5. Books/series are created in the background with progress updates\n\nThe frontend should poll /authors/{author_id}/import-status for progress.\nFrontend does NOT create any database records - backend handles everything.\n\nRate limited: 3 requests per minute, 10 requests per hour","operationId":"start_async_import_amazon_data_start_async_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AsyncImportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AsyncImportStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/amazon-data/reset-import":{"post":{"tags":["amazon-data"],"summary":"Reset Import","description":"Force reset a stuck import for the current user.\n\nThis endpoint allows users to manually reset their import status\nif it gets stuck in PENDING or IMPORTING state.","operationId":"reset_import_amazon_data_reset_import_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseModel"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/recommendations":{"get":{"tags":["recommendations"],"summary":"Get Template Recommendations","description":"Get recommendation signals for the current user's author.\n\nAnalyzes the author's books, genres, and series to produce a lightweight\nauthor profile plus the author's books with extracted cover colors. Template\nand page-structure selection happens in the frontend from these signals.\n\nReturns:\n    RecommendationsResponse with the author profile and books-with-colors.","operationId":"get_template_recommendations_recommendations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationsResponse"}}}}}}},"/newsletter/{newsletter_id}/subscribe":{"post":{"tags":["newsletter"],"summary":"Subscribe To Newsletter","description":"Public endpoint: Subscribe to a newsletter using the hashed newsletter_id.\n\nCreates the subscriber as **pending** and sends one confirm-and-deliver\nemail. Nothing counts against the author's plan, syncs to their ESP, or\nappears in their default CSV export until the reader clicks that link.\n\nSecurity measures:\n- Cloudflare Turnstile (verified when supplied; required only when\n  ``newsletter.turnstile_required`` is on — see the service for why)\n- Rate limited (5/minute, 20/hour, 5/15 minutes per IP)\n- Email validation and disposable email detection\n- Honeypot field for bot detection\n- Per-website daily send budget\n- Consent trail recorded (IP, user agent, timestamp, source)","operationId":"subscribe_to_newsletter_newsletter__newsletter_id__subscribe_post","parameters":[{"name":"newsletter_id","in":"path","required":true,"schema":{"type":"string","title":"Newsletter Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterSubscribeRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterSubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/confirm":{"get":{"tags":["newsletter"],"summary":"Confirm Prompt","description":"Public endpoint: the page a confirmation link lands on. **Never mutates.**\n\nThis used to BE the confirmation, and that was a hole straight through the\nmiddle of double opt-in. Corporate mail security (Defender Safe Links,\nProofpoint, Barracuda) fetches every URL in inbound mail before the\nrecipient ever sees the message, so submitting a stranger's work address to\nany author's public form got them subscribed by their own employer's\nscanner — complete with a consent trail showing a confirmed click. Nobody\nconsented to anything.\n\nSo the state change lives on POST now, behind a button a human has to\npress. The old links still work: they land here, and the reader taps once.\n\nNo database access at all, deliberately: the response is byte-identical for\na real token, a used one and a fabricated one, so this endpoint cannot be\nused to test whether an address is on a list. Validity is resolved at POST.","operationId":"confirm_prompt_newsletter_confirm_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["newsletter"],"summary":"Confirm Subscription","description":"Public endpoint: perform the double opt-in confirmation.\n\nIdempotent — a second submit, or a double-tapped button, renders the same\nsuccess page. An unknown or expired token renders a friendly \"this link has\nexpired\" page, never a raw 404 JSON body: the person on the other end is a\nreader who clicked a link in an email.\n\nReturns 200 for every page it renders — the status code is not carrying\ninformation (the page is), and a 404 would make some mail clients and\nlink-scanners render their own error chrome over ours. The one non-200 is\nthe 302 that hands a reader-magnet reader their download.\n\nTHIS HANDLER IS THE MAGNET↔NEWSLETTER JOIN. It is the only place a reader\nwho signed up for a book is ever given one, and it hangs off the POST and\nnever the GET: the GET is an interstitial that mail scanners prefetch by\nthe thousand, and a prefetch that spent a download would burn a real\nreader's link before they ever clicked it.","operationId":"confirm_subscription_newsletter_confirm_post","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/unsubscribe":{"get":{"tags":["newsletter"],"summary":"Unsubscribe Confirm Page","description":"Public endpoint: render the unsubscribe confirmation page.\n\nGET never mutates. Mail clients and security scanners prefetch links in\nemail, and a GET that unsubscribed on sight would silently drop readers who\nnever clicked anything.","operationId":"unsubscribe_confirm_page_newsletter_unsubscribe_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["newsletter"],"summary":"Unsubscribe Perform","description":"Public endpoint: perform the unsubscribe. Idempotent.\n\nServes two callers with one handler:\n- a human submitting the form on the GET page;\n- a mailbox provider honouring ``List-Unsubscribe-Post: List-Unsubscribe=\n  One-Click`` (RFC 8058), which POSTs to the header URL with no auth, no\n  cookies, and a body we deliberately ignore. Gmail and Yahoo's bulk-sender\n  rules require this to work unattended, so the token in the query string is\n  the only credential.","operationId":"unsubscribe_perform_newsletter_unsubscribe_post","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/subscribers":{"get":{"tags":["newsletter"],"summary":"Get Newsletter Subscribers","description":"Authenticated endpoint: Get paginated list of newsletter subscribers.\n\n``search`` matches email or name (case-insensitive substring). ``sort_by``\nis one of ``newest`` (default), ``oldest``, ``email``, ``name``, ``status``.\n``status`` filters to one of pending/confirmed/unsubscribed/bounced.\n\n**Requires Premium subscription**","operationId":"get_newsletter_subscribers_newsletter_subscribers_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Size"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","default":"newest","title":"Sort By"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterSubscribersListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/export":{"get":{"tags":["newsletter"],"summary":"Export Newsletter Subscribers","description":"Authenticated endpoint: Export newsletter subscribers as CSV.\n\nExports **confirmed subscribers only** by default. Those are the rows with a\nprovable double opt-in, and an export usually goes straight into an ESP\nimport — handing an author unconfirmed addresses by default would hand them\na list they cannot lawfully mail.\n\nPass ``?status=`` to widen it: a single status\n(``pending``/``confirmed``/``unsubscribed``/``bounced``), a comma-separated\nlist, or ``all``.","operationId":"export_newsletter_subscribers_newsletter_export_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/subscribers/{subscriber_id}":{"delete":{"tags":["newsletter"],"summary":"Delete Newsletter Subscriber","description":"Authenticated endpoint: Delete one of the author's subscribers.\n\nThe service already existed and the dashboard already called this path\n(``engagement-actions.ts``); only the route was missing, so every delete\nfrom the UI 404'd.","operationId":"delete_newsletter_subscriber_newsletter_subscribers__subscriber_id__delete","parameters":[{"name":"subscriber_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Subscriber Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/image/upload":{"post":{"tags":["image"],"summary":"Upload Image","description":"Upload an image and convert to WebP.\n\nRate limited: 20 uploads/minute, 100/hour\n\nReturns:\n    Image metadata including public URL","operationId":"upload_image_image_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_image_image_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/image":{"get":{"tags":["image"],"summary":"List Images","description":"List all images for current user.\n\nQuery Parameters:\n- page: Page number (default: 1)\n- size: Items per page (default: 50, max: 100)\n\nReturns:\n    List of images with limit information","operationId":"list_images_image_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/image/{public_id}":{"get":{"tags":["image"],"summary":"Get Image","description":"Get single image details by public ID.","operationId":"get_image_image__public_id__get","parameters":[{"name":"public_id","in":"path","required":true,"schema":{"type":"string","title":"Public Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["image"],"summary":"Delete Image","description":"Delete an image by public ID.","operationId":"delete_image_image__public_id__delete","parameters":[{"name":"public_id","in":"path","required":true,"schema":{"type":"string","title":"Public Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/magnet-file/upload":{"post":{"tags":["reader-magnets"],"summary":"Upload Magnet File","description":"Upload a reader-magnet file (EPUB, PDF, or MOBI; 50 MB max).\n\nThe file lands in a private R2 prefix that is not reachable from the public\nCDN — it is only ever served through a short-lived presigned URL minted\nagainst a delivery token.\n\nRate limited: 5 uploads/minute, 30/hour. Tighter than image uploads because\neach request can carry 50 MB.\n\nReturns ``{fileKey, fileName, fileSize, contentType, fileId, attached,\ncreatedAt}``.","operationId":"upload_magnet_file_magnet_file_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_magnet_file_magnet_file_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/magnet-file":{"get":{"tags":["reader-magnets"],"summary":"List Magnet Files","description":"List the current website's reader-magnet files, newest first.","operationId":"list_magnet_files_magnet_file_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/magnet-file/{file_id}":{"delete":{"tags":["reader-magnets"],"summary":"Delete Magnet File","description":"Delete a reader-magnet file and revoke every outstanding download link.\n\nNot behind the freeze gate: deleting content is how an author gets back under\ntheir caps, so it must stay available while frozen (same rule as the rest of\nthe delete endpoints).\n\nSafe to call twice — an already-deleted id is a 404, and a key that has\nalready vanished from R2 still reports success.","operationId":"delete_magnet_file_magnet_file__file_id__delete","parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"File Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/free/{slug}/download":{"get":{"tags":["reader-magnets"],"summary":"Download Reader Magnet","description":"Public: spend one use of a delivery token and redirect to the file.\n\n``slug`` is not used to resolve anything — see the module docstring.\n\nReturns 302 to a 15-minute presigned R2 URL on success, and an identical\n200 HTML page for every failure. 204 for a prefetch or a HEAD, neither of\nwhich spends a use.","operationId":"download_reader_magnet_free__slug__download_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/providers":{"get":{"tags":["esp"],"summary":"List Providers","description":"Every provider the connect screen renders, capability flags and all.\n\nIncludes Substack with ``connectable: false`` — see ``registry.py`` for why\na provider we cannot support is still listed.\n\nNo database access and no provider call: this is the static registry, which\nis why it is safe to serve on every dashboard load.","operationId":"list_providers_esp_providers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Providers Esp Providers Get"}}}}}}},"/esp/connections":{"get":{"tags":["esp"],"summary":"List Connections","description":"The author's connection, as a list.\n\nA list of zero or one rather than a nullable object: the UI renders a\ncollection, and one shape for \"none yet\" and \"one\" removes a null branch\nfrom every caller. The unique constraint on ``website_id`` is what makes the\nupper bound one.","operationId":"list_connections_esp_connections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Connections Esp Connections Get"}}}}}},"post":{"tags":["esp"],"summary":"Create Connection","description":"Verify and store. Replaces any existing connection for this website.","operationId":"create_connection_esp_connections_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/connections/verify":{"post":{"tags":["esp"],"summary":"Verify Credentials","description":"Check a key and report what we found. **Stores nothing.**\n\nSeparate from connect so the author can recognise their own account before\nanything is committed — \"3 groups, 1,240 subscribers\" is what makes a green\ntick believable. A combined save-then-show would leave a working credential\nbehind for an account the author then decides against.","operationId":"verify_credentials_esp_connections_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/connections/{connection_id}":{"patch":{"tags":["esp"],"summary":"Update Connection","description":"Change the audience or tag. Takes no credentials, by design.","operationId":"update_connection_esp_connections__connection_id__patch","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Connection Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConnectionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["esp"],"summary":"Delete Connection","description":"Disconnect: blank the credential, keep the audience choice.\n\nThe row survives so reconnecting is one field rather than a re-selection.\nDeliberately NOT a hard delete — see ``ESPConnectionStatus.DISCONNECTED``.","operationId":"delete_connection_esp_connections__connection_id__delete","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/connections/{connection_id}/reconnect":{"post":{"tags":["esp"],"summary":"Reconnect Connection","description":"Replace the stored key, keeping the audience choice.","operationId":"reconnect_connection_esp_connections__connection_id__reconnect_post","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Connection Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconnectRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/connections/{connection_id}/audiences":{"get":{"tags":["esp"],"summary":"List Connection Audiences","description":"Re-read the provider's lists.\n\nRate-limited like a write, not like a read: every call is an outbound\nrequest against the author's own provider quota.","operationId":"list_connection_audiences_esp_connections__connection_id__audiences_get","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/esp/connections/{connection_id}/test":{"post":{"tags":["esp"],"summary":"Send Test Subscriber","description":"Push one real record so the author can watch it arrive.\n\nReturns 200 with the full step breakdown even when the push failed, because\nthe interesting answers are not binary: \"the reader landed but the tag\ndidn't\" is a different thing to show than \"nothing happened\", and both are\nsuccessful executions of this endpoint. The outcome is in the body.","operationId":"send_test_subscriber_esp_connections__connection_id__test_post","parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter/list-health":{"get":{"tags":["list-health"],"summary":"Get List Health","description":"Everything wrong with this author's list, plus what we could not check.\n\nAlways 200 with a body. A source that fails is named in ``unreadable``\nrather than raising, because a 500 here renders as *nothing is wrong* — the\nexact substitution this surface exists to prevent. The one thing that does\nfail loudly is auth, which is not a health answer at all.","operationId":"get_list_health_newsletter_list_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get List Health Newsletter List Health Get"}}}}}}},"/contact/{contact_id}/submit":{"post":{"tags":["contact"],"summary":"Submit Contact Message","description":"Public endpoint: Submit a contact message using the hashed contact_id.\n\nSecurity measures:\n- Rate limited (5 requests per 15 minutes per IP)\n- Email validation and disposable email detection\n- Honeypot field for bot detection\n- Message sanitization to prevent XSS\n- Spam detection (excessive URLs, suspicious patterns)","operationId":"submit_contact_message_contact__contact_id__submit_post","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMessageRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/messages":{"get":{"tags":["contact"],"summary":"Get Contact Messages","description":"Authenticated endpoint: Get paginated list of contact messages.\n\n**Requires Premium subscription**","operationId":"get_contact_messages_contact_messages_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Size"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","default":"newest","title":"Sort By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMessagesListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/export":{"get":{"tags":["contact"],"summary":"Export Contact Messages","description":"Authenticated endpoint: Export all contact messages as CSV.","operationId":"export_contact_messages_contact_export_get","responses":{"200":{"description":"Successful Response"}}}},"/dashboard/stats":{"get":{"tags":["dashboard"],"summary":"Get Dashboard Stats","description":"Get lightweight dashboard statistics.\n\nUses service-to-service pattern:\n- DashboardService receives injected services\n- Services call each other instead of making direct SQL queries\n- UserContext is used instead of User/Author/Website models","operationId":"get_dashboard_stats_dashboard_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardStatsSchema"}}}}}}},"/analytics/summary":{"get":{"tags":["analytics"],"summary":"Get Analytics Summary","description":"Headline views + delta, views-over-time, and top facet breakdowns.\n\nAll values come from Postgres (``analytics_daily`` / facets), never WAE\nlive. The response carries ``clamped`` / ``effective_days`` /\n``max_retention_days`` so the UI can show an upgrade prompt when a free\nuser's window was clamped.","operationId":"get_analytics_summary_analytics_summary_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":3650,"minimum":1,"description":"Requested lookback in days. Clamped server-side to the plan's retention window (Free 30 / Pro 365) — over-cap requests are clamped, not rejected.","default":30,"title":"Days"},"description":"Requested lookback in days. Clamped server-side to the plan's retention window (Free 30 / Pro 365) — over-cap requests are clamped, not rejected."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSummarySchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/content/import":{"post":{"tags":["content-engine"],"summary":"Import Content Items","description":"Bulk decomposition import — create/replace a batch in one transaction.\n\nIdempotent via deterministic ids; writes 'migration'-origin revisions.\nReferences (incl. intra-batch menuRef etc.) resolve after the whole batch\nis staged. Used by the Stage 5 cutover + dev seeding.","operationId":"import_content_items_api_v1_content_import_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportRequestSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/content/page/validate-slug":{"post":{"tags":["content-engine"],"summary":"Validate Page Slug","description":"Live, non-throwing page-slug validation for the builder.\n\nMirrors the write-time reserved-namespace + per-author-uniqueness rules so\nthe Add/rename-page dialog gives instant inline feedback. The actual\ncreate/update remains the authoritative gate (this can race another tab).","operationId":"validate_page_slug_api_v1_content_page_validate_slug_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageSlugValidateRequestSchema"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageSlugValidateResponseSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/content/{type_name}":{"get":{"tags":["content-engine"],"summary":"List Content Items","description":"List all live items of a type for the current author, ordered by sort_order.","operationId":"list_content_items_api_v1_content__type_name__get","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"enum":["draft","active"],"type":"string"},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["content-engine"],"summary":"Create Content Item","description":"Create a content item; writes a 'manual' revision row.","operationId":"create_content_item_api_v1_content__type_name__post","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemCreateSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/content/{type_name}/order":{"put":{"tags":["content-engine"],"summary":"Reorder Content Items","description":"Reorder items: sort_order = position in payload.ids; unlisted items go after.","operationId":"reorder_content_items_api_v1_content__type_name__order_put","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemReorderSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/content/{type_name}/{item_id}":{"get":{"tags":["content-engine"],"summary":"Get Content Item","description":"Fetch one item (soft-deleted rows 404).","operationId":"get_content_item_api_v1_content__type_name___item_id__get","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}},{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["content-engine"],"summary":"Update Content Item","description":"Update an item; optional baseRevisionId → 409 {currentRevisionId} when stale.","operationId":"update_content_item_api_v1_content__type_name___item_id__patch","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}},{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentItemUpdateSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["content-engine"],"summary":"Delete Content Item","description":"Soft-delete an item; optional ?baseRevisionId= → 409 {currentRevisionId} when stale.","operationId":"delete_content_item_api_v1_content__type_name___item_id__delete","parameters":[{"name":"type_name","in":"path","required":true,"schema":{"type":"string","title":"Type Name"}},{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}},{"name":"baseRevisionId","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Baserevisionid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/publish/commits":{"get":{"tags":["publish-versioning"],"summary":"List Publish Commits","description":"Retained restore points for the author, newest first, plan-limited.\n\nFree plan (depth 0) returns an EMPTY ``commits`` list — but the\n``last_published_id`` is still reported so the client can diff draft vs the\nlast published state on every plan.","operationId":"list_publish_commits_api_v1_publish_commits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishCommitListResponse"}}}}}}},"/api/v1/publish/current-docs":{"get":{"tags":["publish-versioning"],"summary":"Get Current Docs","description":"The CURRENT live decomposed doc set — read-only; records nothing.\n\nReturns the same ``docs`` shape as ``GET /commits/{id}/docs`` but for the\nlive draft state (no publish commit is written). The client reconstructs the\nlast-published commit's docs and these current docs into the same shape and\ndiffs them to show \"what changed since last publish\".\n\nStatic path — registered before ``/commits/{commit_id}`` so it never matches\nthe dynamic commit segment.","operationId":"get_current_docs_api_v1_publish_current_docs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentDocsResponse"}}}}}}},"/api/v1/publish/commits/{commit_id}/docs":{"get":{"tags":["publish-versioning"],"summary":"Get Publish Commit Docs","description":"Reconstruct the decomposed doc set at a commit (revision snapshots).\n\nAlways available for the LAST-PUBLISHED commit regardless of plan (the diff\nsource). Cross-author access 404s.","operationId":"get_publish_commit_docs_api_v1_publish_commits__commit_id__docs_get","parameters":[{"name":"commit_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Commit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishCommitDocsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/publish/commits/{commit_id}/restore":{"post":{"tags":["publish-versioning"],"summary":"Restore Publish Commit","description":"Restore a commit: write each doc snapshot back as a NEW restore revision.\n\nAppend-only — history never rewinds. Restore is plan-gated only at the UI\n(a free author has no history LIST), but the endpoint itself authorizes any\ncommit the author owns; cross-author commits 404.","operationId":"restore_publish_commit_api_v1_publish_commits__commit_id__restore_post","parameters":[{"name":"commit_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Commit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishCommitRestoreResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscription/plans":{"get":{"tags":["subscription"],"summary":"Get Subscription Plans","description":"Get all available subscription plans","operationId":"get_subscription_plans_subscription_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SubscriptionPlanSchema"},"type":"array","title":"Response Get Subscription Plans Subscription Plans Get"}}}}}}},"/subscription/me":{"get":{"tags":["subscription"],"summary":"Get My Subscription","description":"Get current user's subscription details","operationId":"get_my_subscription_subscription_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/UserSubscriptionSchema"},{"type":"null"}],"title":"Response Get My Subscription Subscription Me Get"}}}}}}},"/subscription/me/limits":{"get":{"tags":["subscription"],"summary":"Get My Feature Limits","description":"Get current user's feature limits and usage","operationId":"get_my_feature_limits_subscription_me_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLimitsSchema"}}}}}}},"/subscription/freeze-status":{"get":{"tags":["subscription"],"summary":"Get My Freeze Status","description":"Over-cap editing-freeze status for the current author.\n\nDrives the dashboard editing freeze: when a FREE author exceeds an\nauthored-content cap (books / blog posts / images) the editing surfaces are\npaused (no add/edit/publish) until they delete content back under the caps\nor upgrade to Pro. Pro authors and within-cap Free authors are never frozen.\n\nRead-only and non-destructive — mirrors ``/subscription/me/limits``.","operationId":"get_my_freeze_status_subscription_freeze_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezeStatusSchema"}}}}}}},"/subscription/{user_id}":{"patch":{"tags":["subscription"],"summary":"Update User Subscription","description":"Update a user's subscription (ADMIN ONLY)\n\nThis endpoint allows administrators to manually assign or update subscriptions.","operationId":"update_user_subscription_subscription__user_id__patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSubscriptionSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSubscriptionSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscription/check-feature/{feature_key}":{"get":{"tags":["subscription"],"summary":"Check Feature Access","description":"Check if current user has access to a specific feature","operationId":"check_feature_access_subscription_check_feature__feature_key__get","parameters":[{"name":"feature_key","in":"path","required":true,"schema":{"type":"string","title":"Feature Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Check Feature Access Subscription Check Feature  Feature Key  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscription/admin/plans":{"post":{"tags":["subscription"],"summary":"Create Subscription Plan","description":"Create a new subscription plan (ADMIN ONLY)\n\nThis endpoint allows administrators to create new subscription plans.","operationId":"create_subscription_plan_subscription_admin_plans_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePlanSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPlanSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["subscription"],"summary":"Get All Subscription Plans","description":"Get all subscription plans (ADMIN ONLY)\n\nThis endpoint allows administrators to view all subscription plans,\nincluding inactive ones if include_inactive=true.","operationId":"get_all_subscription_plans_subscription_admin_plans_get","parameters":[{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Inactive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionPlanSchema"},"title":"Response Get All Subscription Plans Subscription Admin Plans Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscription/admin/plans/{plan_id}":{"put":{"tags":["subscription"],"summary":"Update Subscription Plan","description":"Update a subscription plan (ADMIN ONLY)\n\nThis endpoint allows administrators to update subscription plans.","operationId":"update_subscription_plan_subscription_admin_plans__plan_id__put","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Plan Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePlanSchema"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPlanSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["subscription"],"summary":"Delete Subscription Plan","description":"Delete a subscription plan (ADMIN ONLY)\n\nThis endpoint allows administrators to delete subscription plans.\nPlans with active subscriptions cannot be deleted.","operationId":"delete_subscription_plan_subscription_admin_plans__plan_id__delete","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Plan Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscription/admin/plans/{plan_id}/default":{"patch":{"tags":["subscription"],"summary":"Set Default Plan","description":"Set a plan as the default plan (ADMIN ONLY)\n\nThis endpoint allows administrators to set a plan as the default plan\nthat will be assigned to new users.","operationId":"set_default_plan_subscription_admin_plans__plan_id__default_patch","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Plan Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPlanSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users":{"get":{"tags":["admin"],"summary":"List Users","description":"List users with pagination, filters, and sort.\n\nEvery filter is optional and they combine. **published** is tri-state:\n- **live**: the site is published right now\n- **unpublished**: not published, but it was live at some point\n- **never**: no website, or one that never went live\n\nEach row carries its site state (website path, publish state, custom domain)\nso the table can render a Site column and gate the destructive actions.","operationId":"list_users_admin_users_get","parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Number of items to skip","default":0,"title":"Offset"},"description":"Number of items to skip"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Number of items to return","default":20,"title":"Limit"},"description":"Number of items to return"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by email, first name, or last name","title":"Search"},"description":"Search by email, first name, or last name"},{"name":"plan","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by plan slug","title":"Plan"},"description":"Filter by plan slug"},{"name":"subscription_status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by subscription status (active, on_hold, ...)","title":"Subscription Status"},"description":"Filter by subscription status (active, on_hold, ...)"},{"name":"published","in":"query","required":false,"schema":{"anyOf":[{"enum":["live","unpublished","never"],"type":"string"},{"type":"null"}],"description":"Site state: live | unpublished | never","title":"Published"},"description":"Site state: live | unpublished | never"},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},{"name":"is_verified","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Verified"}},{"name":"is_superuser","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Superuser"}},{"name":"created_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Created on or after this date (inclusive)","title":"Created After"},"description":"Created on or after this date (inclusive)"},{"name":"created_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Created on or before this date (inclusive)","title":"Created Before"},"description":"Created on or before this date (inclusive)"},{"name":"sort","in":"query","required":false,"schema":{"enum":["created_at","email"],"type":"string","default":"created_at","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"enum":["asc","desc"],"type":"string","default":"desc","title":"Order"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin"],"summary":"Create User","description":"Create a new user.\n\nAdmin-created users are automatically:\n- Marked as verified\n- Set to active status\n\nRequired fields:\n- **email**: Valid email address\n- **password**: Minimum 8 characters\n\nOptional fields:\n- **first_name**: User's first name\n- **last_name**: User's last name\n\nNote: Super user privileges cannot be granted via this endpoint.\nSuper users must be created directly in the database.","operationId":"create_user_admin_users_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserCreateSchema"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}":{"get":{"tags":["admin"],"summary":"Get User","description":"Get detailed user information.\n\nReturns extended user data including:\n- Author profile information (if exists)\n- Website state: path, publish state, custom domain (if a site exists)\n- Subscription status\n- Active session count","operationId":"get_user_admin_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetailSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["admin"],"summary":"Update User","description":"Update user details (partial update).\n\nAllows updating user profile information:\n- **first_name**: User's first name\n- **last_name**: User's last name\n- **email**: User's email address (must be unique)\n- **plan_slug**: User's subscription plan\n\nAll fields are optional. Only provided fields will be updated.\n\n**Restrictions:**\n- Cannot edit your own user details via admin panel (use settings page instead)\n- Email must be unique (will fail if another user has the same email)\n- Cannot modify superuser status or verification status via this endpoint\n\n**Returns:** Updated user details with all extended information","operationId":"update_user_admin_users__user_id__patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetailSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin"],"summary":"Delete User","description":"Delete a user and all their data permanently.\n\n**THIS ACTION IS IRREVERSIBLE.**\n\nSafety requirements:\n- **confirm_email**: Must match the target user's email exactly\n- **secret_key**: Must match the ADMIN__DELETE_SECRET environment variable\n\n**Restrictions:**\n- Cannot delete superusers (admin accounts)\n- Cannot delete users with active paid subscriptions (downgrade them first)\n\n**What gets removed** — external artifacts are purged FIRST, and a failed\npurge aborts the delete (the user is left intact rather than orphaning a\nlive site):\n- The published site: KV pointer + the R2 tree, so it stops serving\n- The R2 publish manifests for the site's publish commits\n- Every uploaded image object in R2\n- The Cloudflare custom hostname + KV domain mappings, if any\n- Then the DB rows: user, author, website, all content, revisions, publish\n  commits, visitor data, sessions, and OAuth connections\n\nContent-addressed blobs are NOT deleted — they are deduped across sites and\nare reclaimed by the blob GC job.","operationId":"delete_user_admin_users__user_id__delete","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteUserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteUserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/unpublish":{"post":{"tags":["admin"],"summary":"Unpublish User Site","description":"Take a user's site offline.\n\nA real takedown: the KV pointer and the R2 tree are deleted, so the site\nstops serving. **The author's data is untouched** — they can put the site\nback by pressing Publish, which is why this needs no secret key.\n\nIdempotent: unpublishing an already-offline site succeeds.","operationId":"unpublish_user_site_admin_users__user_id__unpublish_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUnpublishResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/change-website-path":{"post":{"tags":["admin"],"summary":"Change Website Path","description":"Change a user's website URL path.\n\nThis operation:\n- Updates the website path and author slug in the database\n- Deletes old R2 files (generated HTML has hardcoded absolute paths)\n- Deletes publish manifests to force full regeneration\n- Updates Cloudflare KV mapping if custom domain exists\n- Triggers an async republish to regenerate all HTML with the new path","operationId":"change_website_path_admin_users__user_id__change_website_path_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminChangeWebsitePathRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminChangeWebsitePathResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions":{"get":{"tags":["admin"],"summary":"List Sessions","description":"List all active sessions with pagination.\n\nSessions are returned with truncated tokens for security.\nThe `is_current` flag indicates the requesting admin's session.","operationId":"list_sessions_admin_sessions_get","parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Number of items to skip","default":0,"title":"Offset"},"description":"Number of items to skip"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Number of items to return","default":20,"title":"Limit"},"description":"Number of items to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions/user/{user_id}":{"get":{"tags":["admin"],"summary":"List User Sessions","description":"List all sessions for a specific user.\n\nReturns all active sessions belonging to the specified user.","operationId":"list_user_sessions_admin_sessions_user__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionSchema"},"title":"Response List User Sessions Admin Sessions User  User Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin"],"summary":"Revoke All User Sessions","description":"Revoke all sessions for a user.\n\nUseful for forcing a user to re-authenticate on all devices.\nIf the admin is revoking their own sessions, the current session is preserved.","operationId":"revoke_all_user_sessions_admin_sessions_user__user_id__delete","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions/{session_id}":{"delete":{"tags":["admin"],"summary":"Revoke Session","description":"Revoke a specific session.\n\nThe `session_id` is the unique identifier returned in the sessions list.\nCannot revoke your own current session.","operationId":"revoke_session_admin_sessions__session_id__delete","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/impersonate/{user_id}":{"post":{"tags":["admin"],"summary":"Start Impersonation","description":"Start impersonation of a user.\n\nReturns a session token for the target user. Use this token for API calls\nto experience the application as that user (true \"walk in their shoes\" behavior).\n\n**Restrictions:**\n- Cannot impersonate yourself\n- Cannot impersonate other superusers\n- Cannot impersonate inactive users\n\n**To stop impersonating:** Switch back to using your original admin token.","operationId":"start_impersonation_admin_impersonate__user_id__post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImpersonationStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/transfer-site/preview":{"post":{"tags":["admin"],"summary":"Preview Site Transfer","description":"Preview a site transfer before executing it.\n\nReturns details about what the transfer would do, including:\n- Transfer scenario (email change vs account transfer)\n- Source and target user details\n- Warnings about potential issues","operationId":"preview_site_transfer_admin_transfer_site_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferSitePreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferSitePreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/transfer-site":{"post":{"tags":["admin"],"summary":"Execute Site Transfer","description":"Execute a site transfer from a demo account to an author's real account.\n\n**Requires admin secret key for safety.**\n\nTwo scenarios:\n- **Email change**: If target email has no account, the demo user's email is updated.\n- **Account transfer**: If target email has an account, the author is re-pointed\n  to the target user and the demo account is deleted.","operationId":"execute_site_transfer_admin_transfer_site_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferSiteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferSiteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdminChangeWebsitePathRequest":{"properties":{"new_path":{"type":"string","maxLength":100,"minLength":1,"title":"New Path"}},"type":"object","required":["new_path"],"title":"AdminChangeWebsitePathRequest","description":"Request to change a user's website URL path."},"AdminChangeWebsitePathResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"old_path":{"type":"string","title":"Old Path"},"new_path":{"type":"string","title":"New Path"},"message":{"type":"string","title":"Message"}},"type":"object","required":["success","old_path","new_path","message"],"title":"AdminChangeWebsitePathResponse","description":"Response after changing a website path."},"AdminUnpublishResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"type":"string","title":"Message"},"website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Path"},"kv_pointer_removed":{"type":"boolean","title":"Kv Pointer Removed","default":false},"r2_objects_removed":{"type":"integer","title":"R2 Objects Removed","default":0}},"type":"object","required":["message"],"title":"AdminUnpublishResponse","description":"Response after taking a user's site offline."},"AdminUserCreateSchema":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":8,"title":"Password","description":"User password (minimum 8 characters)"},"first_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Last Name"}},"type":"object","required":["email","password"],"title":"AdminUserCreateSchema","description":"Schema for admin creating a user.\n\nNote: Super users cannot be created via the admin panel.\nSuper user privileges must be granted directly via the database."},"AdminUserDetailSchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"is_active":{"type":"boolean","title":"Is Active"},"is_superuser":{"type":"boolean","title":"Is Superuser"},"is_verified":{"type":"boolean","title":"Is Verified"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"plan_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Slug"},"author_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Author Id"},"website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Path"},"is_published":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Published"},"last_published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Published At"},"custom_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Domain"},"subscription_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Status"},"has_paid_access":{"type":"boolean","title":"Has Paid Access"},"author_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author Name"},"active_sessions_count":{"type":"integer","title":"Active Sessions Count","default":0}},"type":"object","required":["id","email","is_active","is_superuser","is_verified","created_at","has_paid_access"],"title":"AdminUserDetailSchema","description":"Extended user details for single user view."},"AdminUserListResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"Total number of items"},"offset":{"type":"integer","title":"Offset","description":"Current offset"},"limit":{"type":"integer","title":"Limit","description":"Items per page"},"users":{"items":{"$ref":"#/components/schemas/AdminUserSchema"},"type":"array","title":"Users"}},"type":"object","required":["total","offset","limit","users"],"title":"AdminUserListResponse","description":"Response for user list endpoint."},"AdminUserSchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"is_active":{"type":"boolean","title":"Is Active"},"is_superuser":{"type":"boolean","title":"Is Superuser"},"is_verified":{"type":"boolean","title":"Is Verified"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"plan_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Slug"},"author_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Author Id"},"website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Path"},"is_published":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Published"},"last_published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Published At"},"custom_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Domain"},"subscription_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Status"},"has_paid_access":{"type":"boolean","title":"Has Paid Access"}},"type":"object","required":["id","email","is_active","is_superuser","is_verified","created_at","has_paid_access"],"title":"AdminUserSchema","description":"User details for admin view.\n\nCarries the site state (``author_id`` .. ``subscription_status``) the users\ntable needs to render a Site column and gate the destructive actions —\nwithout it the list would need one detail fetch per row."},"AdminUserUpdateRequest":{"properties":{"first_name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"plan_slug":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Plan Slug"}},"type":"object","title":"AdminUserUpdateRequest","description":"Schema for updating user details.\n\nAll fields are optional for partial updates.\nOnly provided fields will be updated."},"AmazonScraperRequest":{"properties":{"author_url":{"type":"string","title":"Author Url","description":"Amazon author page URL","example":"https://www.amazon.com/stores/author/B0B4HB6YNV?ref_=ast_author_cabib"},"domain":{"type":"string","title":"Domain","description":"Amazon domain (only amazon.com is currently supported by the scraper)","default":"amazon.com"}},"type":"object","required":["author_url"],"title":"AmazonScraperRequest","description":"Request schema for scraping Amazon author page"},"AnalyticsSummarySchema":{"properties":{"requested_days":{"type":"integer","title":"Requested Days"},"effective_days":{"type":"integer","title":"Effective Days"},"clamped":{"type":"boolean","title":"Clamped"},"max_retention_days":{"type":"integer","title":"Max Retention Days"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"type":"string","format":"date","title":"End Date"},"views":{"type":"integer","title":"Views","default":0},"buy_clicks":{"type":"integer","title":"Buy Clicks","default":0},"previous_views":{"type":"integer","title":"Previous Views","default":0},"views_delta":{"type":"integer","title":"Views Delta","default":0},"views_delta_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Views Delta Pct"},"sampled":{"type":"boolean","title":"Sampled","default":false},"last_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Updated At"},"series":{"items":{"$ref":"#/components/schemas/TimeSeriesPoint"},"type":"array","title":"Series","default":[]},"top_pages":{"items":{"$ref":"#/components/schemas/FacetItem"},"type":"array","title":"Top Pages","default":[]},"top_referrers":{"items":{"$ref":"#/components/schemas/FacetItem"},"type":"array","title":"Top Referrers","default":[]},"top_countries":{"items":{"$ref":"#/components/schemas/FacetItem"},"type":"array","title":"Top Countries","default":[]},"top_devices":{"items":{"$ref":"#/components/schemas/FacetItem"},"type":"array","title":"Top Devices","default":[]}},"type":"object","required":["requested_days","effective_days","clamped","max_retention_days","start_date","end_date"],"title":"AnalyticsSummarySchema","description":"The full analytics payload for one author over a (clamped) window.\n\n``requested_days`` is what the client asked for; ``effective_days`` is what\nwas actually served after the server-side retention clamp. When they differ,\n``clamped`` is true and the UI can surface an upgrade prompt (show-don't-\nhide) — a free user asking for 200 days is clamped to 30, NOT 403'd."},"AsyncImportRequest":{"properties":{"author_url":{"type":"string","title":"Author Url"},"domain":{"type":"string","title":"Domain","default":"amazon.com"}},"type":"object","required":["author_url"],"title":"AsyncImportRequest","description":"Request to start async import"},"AsyncImportStartResponse":{"properties":{"author_id":{"type":"string","title":"Author Id"},"import_status":{"$ref":"#/components/schemas/ImportStatusEnum"},"author":{"$ref":"#/components/schemas/AuthorInfo"},"total_books":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Books"},"message":{"type":"string","title":"Message"}},"type":"object","required":["author_id","import_status","author","message"],"title":"AsyncImportStartResponse","description":"Response when starting async import"},"AuthorCreateSchema":{"properties":{"primary_genre":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Primary Genre","description":"Primary genre"}},"type":"object","title":"AuthorCreateSchema","description":"Schema for creating a new author account.\n\nNOTE (profile-singleton cutover): the author's presentational identity\n(name/bio/photoUrl) now lives in the ``profile`` content item, NOT on the\nauthor account. This schema only carries account/system fields.","example":{"primary_genre":"Fiction"}},"AuthorInfo":{"properties":{"name":{"type":"string","title":"Name"},"bio":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bio"},"imageUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Imageurl"},"socialLinks":{"items":{"$ref":"#/components/schemas/SocialLink"},"type":"array","title":"Sociallinks","default":[]}},"type":"object","required":["name"],"title":"AuthorInfo","description":"Author information for UI"},"AuthorProfileSchema":{"properties":{"book_count":{"type":"integer","title":"Book Count","description":"Total number of books"},"primary_genre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Genre","description":"Primary genre ID"},"primary_genre_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Genre Name","description":"Primary genre display name"},"detected_genres":{"additionalProperties":{"type":"integer"},"type":"object","title":"Detected Genres","description":"Genre distribution (genre_id: count)"},"has_series":{"type":"boolean","title":"Has Series","description":"Whether author has book series"},"series_count":{"type":"integer","title":"Series Count","description":"Number of series","default":0},"publication_span":{"type":"string","title":"Publication Span","description":"Publication span: debut, emerging, established, or prolific"},"characterization":{"type":"string","title":"Characterization","description":"Human-readable characterization of the author"}},"type":"object","required":["book_count","has_series","publication_span","characterization"],"title":"AuthorProfileSchema","description":"Schema for author profile analysis."},"AuthorSchema":{"properties":{"slug":{"type":"string","title":"Slug"},"primary_genre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Genre"},"genres":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Genres"},"genre_confirmed":{"type":"boolean","title":"Genre Confirmed","default":false},"import_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Source"},"import_source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Source Url"},"import_status":{"type":"string","title":"Import Status"},"import_progress":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Import Progress"},"import_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Error"},"import_started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Import Started At"},"import_completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Import Completed At"},"onboarding_flow":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Onboarding Flow"},"onboarding_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Onboarding Step"},"onboarding_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Onboarding Data"},"onboarding_completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Onboarding Completed At"},"tour_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tour State","description":"Dashboard tour progress and completion state"},"checklist":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Checklist","description":"Dashboard checklist state: hasBooks, hasSeries, hasReviews, hasBio, hasImage, hasCustomizedDesign, isPublished"},"stats":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Stats","description":"Pre-calculated stats: bookCount"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"has_website":{"type":"boolean","title":"Has Website","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["slug","import_status","is_active","is_verified","created_at","updated_at"],"title":"AuthorSchema","description":"Complete author account schema.\n\nNOTE (profile-singleton cutover): name/bio/image_url moved to the ``profile``\ncontent item and are no longer part of the author account response."},"AuthorUpdateSchema":{"properties":{"primary_genre":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Primary Genre"},"genre_confirmed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Genre Confirmed"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"is_verified":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Verified"},"tour_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tour State","description":"Dashboard tour progress and completion state"}},"type":"object","title":"AuthorUpdateSchema","description":"Schema for updating an author account.\n\nNOTE (profile-singleton cutover): name/bio/photoUrl moved to the ``profile``\ncontent item (PATCH /api/v1/content/profile/...). This schema only carries\naccount/system fields."},"AuthorWithStatsSchema":{"properties":{"slug":{"type":"string","title":"Slug"},"primary_genre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Genre"},"genres":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Genres"},"genre_confirmed":{"type":"boolean","title":"Genre Confirmed","default":false},"import_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Source"},"import_source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Source Url"},"import_status":{"type":"string","title":"Import Status"},"import_progress":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Import Progress"},"import_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Import Error"},"import_started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Import Started At"},"import_completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Import Completed At"},"onboarding_flow":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Onboarding Flow"},"onboarding_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Onboarding Step"},"onboarding_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Onboarding Data"},"onboarding_completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Onboarding Completed At"},"tour_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tour State","description":"Dashboard tour progress and completion state"},"checklist":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Checklist","description":"Dashboard checklist state: hasBooks, hasSeries, hasReviews, hasBio, hasImage, hasCustomizedDesign, isPublished"},"stats":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Stats","description":"Pre-calculated stats: bookCount"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"has_website":{"type":"boolean","title":"Has Website","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"book_count":{"type":"integer","title":"Book Count","default":0},"series_count":{"type":"integer","title":"Series Count","default":0},"website_count":{"type":"integer","title":"Website Count","default":0}},"type":"object","required":["slug","import_status","is_active","is_verified","created_at","updated_at"],"title":"AuthorWithStatsSchema","description":"Author profile with additional statistics"},"BaseModel":{"properties":{},"type":"object","title":"BaseModel"},"BillingCycleRequestSchema":{"properties":{"billing_cycle":{"type":"string","enum":["monthly","yearly"],"title":"Billing Cycle"}},"type":"object","required":["billing_cycle"],"title":"BillingCycleRequestSchema","description":"Request body for preview-change-plan and change-plan.\n\n``billing_cycle`` is the cycle the user wants to switch TO (the OTHER cycle\nfrom their current Pro subscription)."},"Body_upload_image_image_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_image_image_upload_post"},"Body_upload_magnet_file_magnet_file_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_magnet_file_magnet_file_upload_post"},"BookResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"coverImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coverimageurl"},"publishDate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publishdate"},"purchaseOptions":{"items":{"$ref":"#/components/schemas/PurchaseOption"},"type":"array","title":"Purchaseoptions","default":[]},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"ratingCount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ratingcount"},"isKindleUnlimited":{"type":"boolean","title":"Iskindleunlimited","default":false}},"type":"object","required":["id","name"],"title":"BookResponse","description":"Book information for UI"},"BookWithColorsSchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Book UUID"},"title":{"type":"string","title":"Title","description":"Book title"},"cover_image":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image","description":"Cover image URL"},"extracted_colors":{"anyOf":[{"$ref":"#/components/schemas/ExtractedColorsSchema"},{"type":"null"}],"description":"Colors extracted from cover"}},"type":"object","required":["id","title"],"title":"BookWithColorsSchema","description":"Book schema with extracted colors for personalization."},"CancelSubscriptionResponseSchema":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"access_until":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Until"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End","default":true}},"type":"object","title":"CancelSubscriptionResponseSchema","description":"Result of scheduling a cancellation at the period end."},"ChangePlanResponseSchema":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"subscription_id":{"type":"string","title":"Subscription Id"},"new_billing_cycle":{"type":"string","enum":["monthly","yearly"],"title":"New Billing Cycle"},"effective":{"type":"string","enum":["immediate","next_billing_date"],"title":"Effective"},"effective_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Effective Date"}},"type":"object","required":["subscription_id","new_billing_cycle","effective"],"title":"ChangePlanResponseSchema","description":"Result of an in-place billing-cycle change.\n\nThe subscription id is unchanged (in-place change, never a new checkout).\nFinal state is reconciled by the ``subscription.plan_changed`` (immediate\nupgrade) / ``subscription.updated`` (deferred downgrade) webhook, so this\nechoes the REQUESTED target plus when it takes effect."},"CheckoutResponseSchema":{"properties":{"checkout_url":{"type":"string","title":"Checkout Url"},"success":{"type":"boolean","title":"Success","default":true}},"type":"object","required":["checkout_url"],"title":"CheckoutResponseSchema"},"ClaimSessionRequest":{"properties":{"session_id":{"type":"string","title":"Session Id","description":"Session ID from preview"}},"type":"object","required":["session_id"],"title":"ClaimSessionRequest","description":"Request schema for claiming a public session."},"Classification":{"type":"string","enum":["ok","missing","parking","extra_records","wrong","proxied_cloudflare","wrong_target"],"title":"Classification","description":"What a record set actually contains, relative to what we expect."},"ConnectRequest":{"properties":{"provider":{"type":"string","maxLength":50,"minLength":1,"title":"Provider"},"credentials":{"additionalProperties":true,"type":"object","title":"Credentials"},"audience_id":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Audience Id"},"tag":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Tag"}},"type":"object","required":["provider"],"title":"ConnectRequest"},"ContactMessageRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name","description":"Sender's name"},"email":{"type":"string","format":"email","title":"Email","description":"Sender's email address"},"message":{"type":"string","maxLength":5000,"minLength":10,"title":"Message","description":"Message content"},"honeypot":{"type":"string","title":"Honeypot","description":"Honeypot field for bot detection","default":""}},"type":"object","required":["name","email","message"],"title":"ContactMessageRequest","description":"Schema for contact message submission from public website.","example":{"email":"john@example.com","honeypot":"","message":"I really enjoyed your latest book! When is the next one coming out?","name":"John Doe"}},"ContactMessageResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"email":{"type":"string","title":"Email"},"message":{"type":"string","title":"Message"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","email","message","created_at"],"title":"ContactMessageResponse","description":"Schema for contact message response."},"ContactMessagesListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactMessageResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"showing":{"type":"integer","title":"Showing"},"has_more":{"type":"boolean","title":"Has More"},"hidden_new_count":{"type":"integer","title":"Hidden New Count"},"page":{"type":"integer","title":"Page"},"size":{"type":"integer","title":"Size"},"pages":{"type":"integer","title":"Pages"}},"type":"object","required":["items","total","limit","showing","has_more","hidden_new_count","page","size","pages"],"title":"ContactMessagesListResponse","description":"Schema for paginated contact messages list with plan limits."},"ContentImportItemSchema":{"properties":{"type":{"type":"string","title":"Type"},"id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Id"},"slug":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Slug"},"status":{"type":"string","enum":["draft","active"],"title":"Status","default":"active"},"sortOrder":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sortorder"},"data":{"additionalProperties":true,"type":"object","title":"Data"}},"type":"object","required":["type"],"title":"ContentImportItemSchema","description":"One entry in an import batch: {type, id?, slug?, status?, sortOrder?, data}.\n\n``id`` supplied → deterministic, idempotent replace (re-running the batch\nwith the same ids yields the same rows). ``data`` is the full per-type\npayload (camelCase wire keys, validated against the generated model in the\nservice)."},"ContentImportRequestSchema":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContentImportItemSchema"},"type":"array","minItems":1,"title":"Items"}},"type":"object","required":["items"],"title":"ContentImportRequestSchema","description":"POST /api/v1/content/import body: {items: [...]}. One transaction."},"ContentItemCreateSchema":{"properties":{"slug":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Slug"},"status":{"type":"string","enum":["draft","active"],"title":"Status","default":"active"},"sortOrder":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sortorder"},"data":{"additionalProperties":true,"type":"object","title":"Data"}},"type":"object","title":"ContentItemCreateSchema","description":"POST /api/v1/content/{type} body: {slug?, status?, sortOrder?, data}."},"ContentItemListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContentItemSchema"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"ContentItemListResponse","description":"GET /api/v1/content/{type} response."},"ContentItemReorderSchema":{"properties":{"ids":{"items":{"type":"string","format":"uuid"},"type":"array","minItems":1,"title":"Ids"}},"type":"object","required":["ids"],"title":"ContentItemReorderSchema","description":"PUT /api/v1/content/{type}/order body."},"ContentItemSchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"type":{"type":"string","title":"Type"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"status":{"type":"string","title":"Status"},"sortOrder":{"type":"integer","title":"Sortorder"},"data":{"additionalProperties":true,"type":"object","title":"Data"},"schemaVersion":{"type":"integer","title":"Schemaversion"},"revisionId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Revisionid"},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"}},"type":"object","required":["id","type","status","sortOrder","data","schemaVersion","createdAt","updatedAt"],"title":"ContentItemSchema","description":"Full content item as returned by every endpoint."},"ContentItemUpdateSchema":{"properties":{"slug":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Slug"},"status":{"anyOf":[{"type":"string","enum":["draft","active"]},{"type":"null"}],"title":"Status"},"sortOrder":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sortorder"},"data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Data"},"baseRevisionId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Baserevisionid"}},"type":"object","title":"ContentItemUpdateSchema","description":"PATCH body — data is the FULL payload document (replace, not deep-merge)."},"CoreDataResponse":{"properties":{"author":{"$ref":"#/components/schemas/AuthorInfo"},"series":{"items":{"$ref":"#/components/schemas/SeriesResponse"},"type":"array","title":"Series","default":[]},"books":{"items":{"$ref":"#/components/schemas/BookResponse"},"type":"array","title":"Books","default":[]},"reviews":{"items":{"$ref":"#/components/schemas/ReviewResponse"},"type":"array","title":"Reviews","default":[]}},"type":"object","required":["author"],"title":"CoreDataResponse","description":"Core data response for UI matching the TypeScript interface"},"CreateCheckoutRequestSchema":{"properties":{"billing_cycle":{"type":"string","enum":["monthly","yearly"],"title":"Billing Cycle","default":"yearly"},"affonso_referral":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Affonso Referral"}},"type":"object","title":"CreateCheckoutRequestSchema"},"CreatePlanSchema":{"properties":{"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"features":{"additionalProperties":true,"type":"object","title":"Features"},"max_books":{"type":"integer","title":"Max Books"},"max_custom_domains":{"type":"integer","title":"Max Custom Domains"},"max_newsletter_subscribers":{"type":"integer","title":"Max Newsletter Subscribers"},"max_contact_messages":{"type":"integer","title":"Max Contact Messages"},"max_blog_posts":{"type":"integer","title":"Max Blog Posts"},"max_image_uploads":{"type":"integer","title":"Max Image Uploads"},"is_active":{"type":"boolean","title":"Is Active","default":true},"is_default":{"type":"boolean","title":"Is Default","default":false}},"type":"object","required":["name","slug","features","max_books","max_custom_domains","max_newsletter_subscribers","max_contact_messages","max_blog_posts","max_image_uploads"],"title":"CreatePlanSchema","description":"Schema for creating a subscription plan"},"CurrentDocsResponse":{"properties":{"docs":{"additionalProperties":true,"type":"object","title":"Docs"}},"type":"object","required":["docs"],"title":"CurrentDocsResponse","description":"GET /api/v1/publish/current-docs — the CURRENT live decomposed doc set.\n\nSame ``docs`` shape as ``PublishCommitDocsResponse`` (pointer-key →\nfull revision snapshot), but for the live draft state and WITHOUT a commit\nid (nothing is recorded). The client diffs this against the last-published\ncommit's docs to surface \"what changed since last publish\"."},"DashboardStatsSchema":{"properties":{"books_count":{"type":"integer","title":"Books Count","default":0},"series_count":{"type":"integer","title":"Series Count","default":0},"reviews_count":{"type":"integer","title":"Reviews Count","default":0},"subscribers_count":{"type":"integer","title":"Subscribers Count","default":0},"messages_count":{"type":"integer","title":"Messages Count","default":0},"is_published":{"type":"boolean","title":"Is Published","default":false},"has_pending_changes":{"type":"boolean","title":"Has Pending Changes","default":false},"pending_scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pending Scope"}},"type":"object","title":"DashboardStatsSchema","description":"Lightweight dashboard statistics."},"DeleteUserRequest":{"properties":{"confirm_email":{"type":"string","format":"email","title":"Confirm Email","description":"Must match the target user's email exactly"},"secret_key":{"type":"string","title":"Secret Key","description":"Admin secret key for delete operations"}},"type":"object","required":["confirm_email","secret_key"],"title":"DeleteUserRequest","description":"Request body for deleting a user.\n\nRequires multiple confirmations for safety:\n- confirm_email: Must exactly match the target user's email\n- secret_key: Must match the ADMIN__DELETE_SECRET environment variable"},"DeleteUserResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"type":"string","title":"Message"},"deleted_user_id":{"type":"string","format":"uuid","title":"Deleted User Id"},"deleted_user_email":{"type":"string","title":"Deleted User Email"},"purge":{"anyOf":[{"$ref":"#/components/schemas/PurgeReportSchema"},{"type":"null"}]}},"type":"object","required":["message","deleted_user_id","deleted_user_email"],"title":"DeleteUserResponse","description":"Response after successfully deleting a user."},"DnsRecordSpec":{"properties":{"type":{"type":"string","enum":["A","CNAME"],"title":"Type"},"name":{"type":"string","title":"Name"},"value":{"type":"string","title":"Value"},"purpose":{"type":"string","enum":["apex-a","www-cname","subdomain-cname"],"title":"Purpose"}},"type":"object","required":["type","name","value","purpose"],"title":"DnsRecordSpec","description":"A server-authored record the author must create.\n\nEmbedded in DomainStatusResponse so the frontend never hardcodes the\nredirector IP or CNAME target — the backend is the single source of truth."},"DomainAvailabilityRequest":{"properties":{"domain":{"type":"string","maxLength":253,"title":"Domain"}},"type":"object","required":["domain"],"title":"DomainAvailabilityRequest","description":"Request to check if a domain is available."},"DomainAvailabilityResponse":{"properties":{"domain":{"type":"string","title":"Domain"},"available":{"type":"boolean","title":"Available"},"message":{"type":"string","title":"Message"},"is_root_domain":{"type":"boolean","title":"Is Root Domain","default":false},"served_hostname":{"type":"string","title":"Served Hostname","default":""}},"type":"object","required":["domain","available","message"],"title":"DomainAvailabilityResponse","description":"Response for domain availability check.\n\n``is_root_domain`` and ``served_hostname`` let the wizard preview the\ncanonical hostname (www.<domain> for roots) without re-implementing TLD\nlogic on the frontend."},"DomainCheckResponse":{"properties":{"domain":{"type":"string","title":"Domain"},"records":{"items":{"$ref":"#/components/schemas/RecordCheckResult"},"type":"array","title":"Records"},"aaaa_present":{"type":"boolean","title":"Aaaa Present","default":false},"caa_blocks":{"type":"boolean","title":"Caa Blocks","default":false},"ownership_verified":{"type":"boolean","title":"Ownership Verified"},"ssl_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ssl Status"},"ssl_active":{"type":"boolean","title":"Ssl Active"},"apex_probe":{"type":"string","enum":["not_applicable","pending","passed","failed"],"title":"Apex Probe"},"apex_probe_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Apex Probe Reason"},"dns_ok":{"type":"boolean","title":"Dns Ok"},"fully_approved":{"type":"boolean","title":"Fully Approved"},"checks_remaining":{"type":"integer","title":"Checks Remaining"},"rate_limited":{"type":"boolean","title":"Rate Limited","default":false},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["domain","records","ownership_verified","ssl_active","apex_probe","dns_ok","fully_approved","checks_remaining"],"title":"DomainCheckResponse","description":"Full DNS + SSL + probe diagnostics returned by POST /domain/check."},"DomainStatusResponse":{"properties":{"domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Domain"},"served_hostname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Served Hostname"},"verification_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verification Status"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"last_checked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Checked At"},"attempts":{"type":"integer","title":"Attempts","default":0},"has_domain":{"type":"boolean","title":"Has Domain","default":false},"is_verified":{"type":"boolean","title":"Is Verified","default":false},"cloudflare_hostname_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cloudflare Hostname Id"},"ssl_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ssl Status"},"records":{"items":{"$ref":"#/components/schemas/DnsRecordSpec"},"type":"array","title":"Records","default":[]},"www_enabled":{"type":"boolean","title":"Www Enabled","default":false},"www_ssl_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Www Ssl Status"},"verification_phase":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verification Phase"},"root_cname_verified":{"type":"boolean","title":"Root Cname Verified","default":false},"www_cname_verified":{"type":"boolean","title":"Www Cname Verified","default":false},"cname_checks_remaining":{"type":"integer","title":"Cname Checks Remaining","default":10}},"type":"object","title":"DomainStatusResponse","description":"Current domain configuration status."},"EnhancedOnboardingStateSchema":{"properties":{"flow":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow","description":"Onboarding flow type"},"step":{"type":"string","title":"Step","description":"Current onboarding step"},"data":{"additionalProperties":true,"type":"object","title":"Data","description":"Step-specific data"},"completed_steps":{"items":{"type":"string"},"type":"array","title":"Completed Steps","description":"List of completed steps"},"version":{"type":"integer","title":"Version","description":"Onboarding state version for optimistic locking","default":1},"current_state":{"type":"string","title":"Current State","description":"Current state machine state"},"can_go_back":{"type":"boolean","title":"Can Go Back","description":"Whether user can navigate backwards"},"allowed_actions":{"items":{"type":"string"},"type":"array","title":"Allowed Actions","description":"List of allowed actions from current state"},"state_completed_steps":{"items":{"type":"string"},"type":"array","title":"State Completed Steps","description":"Steps completed according to state machine"}},"type":"object","required":["step","current_state","can_go_back"],"title":"EnhancedOnboardingStateSchema","description":"Enhanced onboarding state with state machine information for recovery","example":{"allowed_actions":["change_template","select_path"],"can_go_back":true,"completed_steps":["welcome","amazon-url","scraping"],"current_state":"TEMPLATE_SELECTED","data":{"amazon_url":"https://www.amazon.com/author/xyz","selected_template":"modern-minimal"},"flow":"amazon_import","state_completed_steps":["import","template"],"step":"genre-confirmation","version":1}},"ErrorModel":{"properties":{"detail":{"anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"title":"Detail"}},"type":"object","required":["detail"],"title":"ErrorModel"},"ExtractedColorsSchema":{"properties":{"primary":{"type":"string","title":"Primary","description":"Primary color in hex format"},"secondary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secondary","description":"Secondary color in hex format"}},"type":"object","required":["primary"],"title":"ExtractedColorsSchema","description":"Colors extracted from book cover."},"FacetItem":{"properties":{"value":{"type":"string","title":"Value"},"count":{"type":"integer","title":"Count","default":0}},"type":"object","required":["value"],"title":"FacetItem","description":"One row of a top-N breakdown (page / referrer / country / device).\n\n``value`` is a coarse aggregate only (host / ISO country / coarse device /\nnormalized path) or the ``__other__`` bucket — never per-visitor data."},"FeatureLimitsSchema":{"properties":{"max_books":{"type":"integer","title":"Max Books"},"current_books":{"type":"integer","title":"Current Books"},"max_custom_domains":{"type":"integer","title":"Max Custom Domains"},"current_custom_domains":{"type":"integer","title":"Current Custom Domains"},"max_newsletter_subscribers":{"type":"integer","title":"Max Newsletter Subscribers"},"current_newsletter_subscribers":{"type":"integer","title":"Current Newsletter Subscribers"},"max_contact_messages":{"type":"integer","title":"Max Contact Messages"},"current_contact_messages":{"type":"integer","title":"Current Contact Messages"},"max_blog_posts":{"type":"integer","title":"Max Blog Posts"},"current_blog_posts":{"type":"integer","title":"Current Blog Posts"},"max_image_uploads":{"type":"integer","title":"Max Image Uploads"},"current_image_uploads":{"type":"integer","title":"Current Image Uploads"}},"type":"object","required":["max_books","current_books","max_custom_domains","current_custom_domains","max_newsletter_subscribers","current_newsletter_subscribers","max_contact_messages","current_contact_messages","max_blog_posts","current_blog_posts","max_image_uploads","current_image_uploads"],"title":"FeatureLimitsSchema","description":"Feature limits and current usage schema"},"FreezeOverLimitSchema":{"properties":{"type":{"type":"string","title":"Type"},"current":{"type":"integer","title":"Current"},"max":{"type":"integer","title":"Max"},"over_by":{"type":"integer","title":"Over By"}},"type":"object","required":["type","current","max","over_by"],"title":"FreezeOverLimitSchema","description":"A single authored-content cap the author has exceeded.\n\nFields are snake_case on the wire; the frontend transforms them to\ncamelCase (``type`` / ``current`` / ``max`` / ``overBy``)."},"FreezeStatusSchema":{"properties":{"is_frozen":{"type":"boolean","title":"Is Frozen"},"over_limits":{"items":{"$ref":"#/components/schemas/FreezeOverLimitSchema"},"type":"array","title":"Over Limits"},"message":{"type":"string","title":"Message"}},"type":"object","required":["is_frozen","over_limits","message"],"title":"FreezeStatusSchema","description":"Over-cap editing-freeze status for the authenticated author.\n\n``is_frozen`` is True iff the author is NOT Pro AND exceeds at least one\nauthored-content cap (books / blog posts / images). Pro users and Free\nusers within every cap report ``is_frozen=False`` with an empty\n``over_limits`` and an empty ``message``.\n\nWire shape is snake_case; the frontend transforms to camelCase\n(``isFrozen`` / ``overLimits`` / ``message``)."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImpersonationStartResponse":{"properties":{"target_user_id":{"type":"string","format":"uuid","title":"Target User Id"},"target_user_email":{"type":"string","title":"Target User Email"},"token":{"type":"string","title":"Token","description":"Session token for the impersonated user"},"message":{"type":"string","title":"Message"}},"type":"object","required":["target_user_id","target_user_email","token","message"],"title":"ImpersonationStartResponse","description":"Response when starting impersonation."},"ImportProgressData":{"properties":{"total_books":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Books"},"scraped_books":{"type":"integer","title":"Scraped Books","default":0},"total_reviews":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Reviews"},"scraped_reviews":{"type":"integer","title":"Scraped Reviews","default":0},"current_step":{"type":"string","title":"Current Step"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"estimated_completion":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Estimated Completion"},"progress_percentage":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Progress Percentage"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["current_step"],"title":"ImportProgressData","description":"Progress data for import operation"},"ImportSource":{"type":"string","enum":["amazon","goodreads","manual","ai_assisted"],"title":"ImportSource"},"ImportStatusEnum":{"type":"string","enum":["not_started","pending","importing","complete","failed"],"title":"ImportStatusEnum","description":"Import status enum matching the database model"},"ImportStatusResponse":{"properties":{"author_id":{"type":"string","title":"Author Id"},"import_status":{"$ref":"#/components/schemas/ImportStatusEnum"},"import_progress":{"anyOf":[{"$ref":"#/components/schemas/ImportProgressData"},{"type":"null"}]},"author":{"$ref":"#/components/schemas/AuthorInfo"},"scraped_books_count":{"type":"integer","title":"Scraped Books Count","default":0}},"type":"object","required":["author_id","import_status","author"],"title":"ImportStatusResponse","description":"Response for import status endpoint"},"InitiateDomainVerificationRequest":{"properties":{"domain":{"type":"string","maxLength":253,"title":"Domain","description":"Custom domain to verify"}},"type":"object","required":["domain"],"title":"InitiateDomainVerificationRequest","description":"Request to initiate domain verification."},"InitiateDomainVerificationResponse":{"properties":{"domain":{"type":"string","title":"Domain"},"verification_status":{"type":"string","title":"Verification Status"},"cloudflare_hostname_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cloudflare Hostname Id"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["domain","verification_status","expires_at","message"],"title":"InitiateDomainVerificationResponse","description":"Response after initiating domain verification.\n\nNo TXT records — the www-canonical flow uses HTTP validation. The records\nthe author must create come from DomainStatusResponse.records."},"NewsletterSubscribeRequest":{"properties":{"email":{"type":"string","maxLength":200,"format":"email","title":"Email","description":"Email address for newsletter subscription"},"name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Name","description":"Optional reader name — ESPs segment on it and the confirmation email reads better with it"},"honeypot":{"type":"string","title":"Honeypot","description":"Honeypot field for bot detection","default":""},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token","description":"Cloudflare Turnstile token. Optional: already-published sites post without one. When present it is always verified; when absent it is only fatal if newsletter.turnstile_required is on."},"source":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Source","description":"Capture point: 'section' (default), 'magnet-id:{uuid}', legacy 'magnet:{slug}', or 'blog'"}},"type":"object","required":["email"],"title":"NewsletterSubscribeRequest","description":"Schema for newsletter subscription request from public website.","example":{"email":"reader@example.com","honeypot":"","name":"Jane Reader","source":"section","turnstile_token":"0.abc123"}},"NewsletterSubscribeResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"type":"string","title":"Message","default":"Please check your email to confirm your subscription."}},"type":"object","title":"NewsletterSubscribeResponse","description":"Public response to a subscribe attempt. BYTE-IDENTICAL on every branch.\n\nNew signup, resend, cooldown-suppressed, resurrected, already-confirmed and\nhoneypot-trapped all produce exactly this body and a 201. Nothing about the\nrequest's outcome is observable.\n\nThis used to return the row's real ``id``, ``status``, ``created_at`` and an\n``already_subscribed`` flag, which was wrong twice over:\n\n1. **Enumeration.** ``status`` and ``already_subscribed`` told any anonymous\n   caller with curl whether a given address was on a given author's list.\n   The frontend rendering one message for both branches was never a defence —\n   the frontend is not the security boundary, the API is.\n2. **Credential disclosure.** The real row UUID plus the old\n   ``md5(id||salt)`` backfilled unsubscribe token was a complete\n   unauthenticated unsubscribe exploit: submit a victim's address, read their\n   id from the response, compute their token offline, unsubscribe them. Both\n   halves are fixed (tokens are now CSPRNG), but a public endpoint has no\n   business handing out internal row ids regardless.\n\nThere is no timing channel either: no branch sends mail on the request path.\nEvery accepted signup is queued with ``confirm_sent_at`` NULL and dispatched\nby the backlog sweeper, so all branches cost the same and none of them are\ndistinguishable by how long the response took.\n\nDo not add outcome-bearing fields here. If a caller genuinely needs subscriber\nstate, that is an authenticated endpoint."},"NewsletterSubscriberResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"status":{"type":"string","title":"Status"},"source":{"type":"string","title":"Source"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Confirmed At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","status","source","created_at"],"title":"NewsletterSubscriberResponse","description":"Schema for newsletter subscriber response (dashboard list + export)."},"NewsletterSubscribersListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/NewsletterSubscriberResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"confirmed_total":{"type":"integer","title":"Confirmed Total"},"status_counts":{"additionalProperties":{"type":"integer"},"type":"object","title":"Status Counts"},"limit":{"type":"integer","title":"Limit"},"showing":{"type":"integer","title":"Showing"},"has_more":{"type":"boolean","title":"Has More"},"hidden_new_count":{"type":"integer","title":"Hidden New Count"},"page":{"type":"integer","title":"Page"},"size":{"type":"integer","title":"Size"},"pages":{"type":"integer","title":"Pages"}},"type":"object","required":["items","total","confirmed_total","status_counts","limit","showing","has_more","hidden_new_count","page","size","pages"],"title":"NewsletterSubscribersListResponse","description":"Schema for paginated newsletter subscribers list with plan limits.\n\nTHREE counts, and they are not interchangeable. Once a status filter exists,\na single ``total`` has to mean two incompatible things at once, and the\nplan-usage badge ends up reading \"3 of 500\" the moment an author filters to\nUnsubscribed — a visible, trust-destroying bug.\n\n- ``total`` follows the current filter: it drives \"Showing X–Y of N\" and\n  pagination, and it is the ONLY count that moves when the filter changes.\n- ``confirmed_total`` ignores the filter entirely: it drives the plan-limit\n  indicator, the limited-view banner and the \"Confirmed subscribers\" stat.\n  It must equal what ``check_limit`` counts and what the CSV export emits by\n  default, or three surfaces will quietly disagree about the same number.\n- ``status_counts`` also ignores the filter: status -> count for the filter\n  chips and the \"Awaiting confirmation\" stat. Every status is present, 0 if\n  it has no rows."},"OAuth2AuthorizeResponse":{"properties":{"authorization_url":{"type":"string","title":"Authorization Url"}},"type":"object","required":["authorization_url"],"title":"OAuth2AuthorizeResponse"},"OnboardingStateSchema":{"properties":{"flow":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow","description":"Onboarding flow type"},"step":{"type":"string","title":"Step","description":"Current onboarding step"},"data":{"additionalProperties":true,"type":"object","title":"Data","description":"Step-specific data"},"completed_steps":{"items":{"type":"string"},"type":"array","title":"Completed Steps","description":"List of completed steps"},"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version","description":"Version for optimistic locking (required for updates)"}},"type":"object","required":["step"],"title":"OnboardingStateSchema","description":"Schema for onboarding state management","example":{"completed_steps":["welcome","amazon-url","scraping"],"data":{"amazon_url":"https://www.amazon.com/author/xyz","selected_template":"modern-minimal"},"flow":"amazon_import","step":"genre-confirmation","version":1}},"PageSlugValidateRequestSchema":{"properties":{"slug":{"type":"string","maxLength":80,"title":"Slug"},"excludeId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Excludeid"}},"type":"object","required":["slug"],"title":"PageSlugValidateRequestSchema","description":"POST /api/v1/content/page/validate-slug body — the builder's live check."},"PageSlugValidateResponseSchema":{"properties":{"valid":{"type":"boolean","title":"Valid"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["valid"],"title":"PageSlugValidateResponseSchema","description":"Non-throwing validation result for the builder's Add/rename-page dialog."},"PathAvailabilitySchema":{"properties":{"path":{"type":"string","title":"Path"},"available":{"type":"boolean","title":"Available"},"suggestion":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suggestion"}},"type":"object","required":["path","available"],"title":"PathAvailabilitySchema","example":{"available":true,"path":"john-smith"}},"PortalSessionResponseSchema":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"PortalSessionResponseSchema","description":"A Dodo customer-portal session URL for self-service billing management."},"PreviewAuthorRequest":{"properties":{"author_url":{"type":"string","title":"Author Url","description":"Amazon author page URL","example":"https://www.amazon.com/stores/author/B0B4HB6YNV"},"domain":{"type":"string","title":"Domain","description":"Amazon domain (amazon.com, amazon.co.uk, etc.)","default":"amazon.com"},"redirect_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Redirect Url","description":"Optional redirect destination after authentication"},"turnstile_token":{"type":"string","minLength":1,"title":"Turnstile Token","description":"Cloudflare Turnstile verification token"}},"type":"object","required":["author_url","turnstile_token"],"title":"PreviewAuthorRequest","description":"Request schema for previewing author data"},"PreviewAuthorResponse":{"properties":{"session_id":{"type":"string","title":"Session Id","description":"Session ID to pass through OAuth"},"preview":{"$ref":"#/components/schemas/PreviewDataResponse"}},"type":"object","required":["session_id","preview"],"title":"PreviewAuthorResponse","description":"Response schema for preview endpoint"},"PreviewChangePlanResponseSchema":{"properties":{"current_billing_cycle":{"type":"string","enum":["monthly","yearly"],"title":"Current Billing Cycle"},"new_billing_cycle":{"type":"string","enum":["monthly","yearly"],"title":"New Billing Cycle"},"immediate_charge":{"type":"integer","title":"Immediate Charge"},"new_recurring_amount":{"type":"integer","title":"New Recurring Amount"},"currency":{"type":"string","title":"Currency"},"effective":{"type":"string","enum":["immediate","next_billing_date"],"title":"Effective"},"credit":{"type":"integer","title":"Credit","default":0},"next_billing_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Billing Date"},"is_estimate":{"type":"boolean","title":"Is Estimate","default":false}},"type":"object","required":["current_billing_cycle","new_billing_cycle","immediate_charge","new_recurring_amount","currency","effective"],"title":"PreviewChangePlanResponseSchema","description":"AUTHORITATIVE preview of switching the user's Pro subscription to the\nother cycle, sourced from Dodo's native ``preview_change_plan`` endpoint.\n\nThese are Dodo-computed figures (``is_estimate`` is ``False``), matching what\n``change_plan`` will settle because the preview is requested with the SAME\n``effective_at`` / proration mode the change uses:\n\n- monthly -> yearly (UPGRADE): ``effective='immediate'`` — Dodo charges the\n  prorated ``immediate_charge`` today.\n- yearly -> monthly (DOWNGRADE): ``effective='next_billing_date'`` — NO charge\n  today (``immediate_charge == 0``); the new monthly price begins at the next\n  billing date (owner decision: \"no charge today, switches at renewal\").\n\nThe client renders a 3-line breakdown from ``immediate_charge`` (today's\ncharge), ``new_recurring_amount`` (the new plan price), and ``credit`` (any\ncredit Dodo applies). NO credit-card details are present anywhere."},"PreviewDataResponse":{"properties":{"author_name":{"type":"string","title":"Author Name"},"author_image":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author Image"},"bio":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bio"},"total_books":{"type":"integer","title":"Total Books"},"sample_covers":{"items":{"type":"string"},"type":"array","title":"Sample Covers"}},"type":"object","required":["author_name","author_image","bio","total_books","sample_covers"],"title":"PreviewDataResponse","description":"Preview data returned to frontend"},"PublishCommitDocsResponse":{"properties":{"commitId":{"type":"string","format":"uuid","title":"Commitid"},"docs":{"additionalProperties":true,"type":"object","title":"Docs"}},"type":"object","required":["commitId","docs"],"title":"PublishCommitDocsResponse","description":"GET /api/v1/publish/commits/{id}/docs — reconstructed doc set at a commit.\n\n``docs`` keys are pointer keys (``\"<entityKind>:<entityId>\"``); values are\nthe full revision snapshots (camelCase content-item shape). The client diffs\nits current draft docs against these."},"PublishCommitListResponse":{"properties":{"commits":{"items":{"$ref":"#/components/schemas/PublishCommitSummarySchema"},"type":"array","title":"Commits"},"versionHistoryDepth":{"type":"integer","title":"Versionhistorydepth"},"lastPublishedId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Lastpublishedid"}},"type":"object","required":["commits","versionHistoryDepth"],"title":"PublishCommitListResponse","description":"GET /api/v1/publish/commits — plan-limited restore points, newest first.\n\n``version_history_depth`` is the plan-gated visible window (free → 0,\npaid → N). ``last_published_id`` is the last-published commit id, ALWAYS\npresent when the author has ever published (powers diff even on free)."},"PublishCommitRestoreResponse":{"properties":{"commitId":{"type":"string","format":"uuid","title":"Commitid"},"restoredCount":{"type":"integer","title":"Restoredcount"},"newRevisionIds":{"additionalProperties":{"type":"string"},"type":"object","title":"Newrevisionids"},"skipped":{"items":{"type":"string"},"type":"array","title":"Skipped"}},"type":"object","required":["commitId","restoredCount","newRevisionIds"],"title":"PublishCommitRestoreResponse","description":"POST /api/v1/publish/commits/{id}/restore result.\n\n``new_revision_ids`` maps each restored content-item id to the NEW\n``origin:\"restore\"`` revision id created (append-only; history never\nrewinds). ``skipped`` lists pointer keys whose live doc no longer exists."},"PublishCommitSummarySchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"docCount":{"type":"integer","title":"Doccount"},"createdAt":{"type":"string","format":"date-time","title":"Createdat"}},"type":"object","required":["id","docCount","createdAt"],"title":"PublishCommitSummarySchema","description":"One restore point in the version-history list (no heavy doc payload)."},"PurchaseOption":{"properties":{"platform":{"type":"string","title":"Platform"},"url":{"type":"string","title":"Url"},"price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price"},"format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Format"}},"type":"object","required":["platform","url"],"title":"PurchaseOption","description":"Purchase option for a book"},"PurgeReportSchema":{"properties":{"website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Path"},"kv_pointer_removed":{"type":"boolean","title":"Kv Pointer Removed","default":false},"r2_objects_removed":{"type":"integer","title":"R2 Objects Removed","default":0},"manifests_removed":{"type":"integer","title":"Manifests Removed","default":0},"images_removed":{"type":"integer","title":"Images Removed","default":0},"image_storage_purged":{"type":"boolean","title":"Image Storage Purged","default":false},"custom_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Domain"},"custom_domain_removed":{"type":"boolean","title":"Custom Domain Removed","default":false},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","default":[]}},"type":"object","title":"PurgeReportSchema","description":"What the external-artifact purge actually removed (see services/site_takedown)."},"RecommendationsResponse":{"properties":{"author_profile":{"$ref":"#/components/schemas/AuthorProfileSchema","description":"Analysis of the author"},"books":{"items":{"$ref":"#/components/schemas/BookWithColorsSchema"},"type":"array","title":"Books","description":"Books with extracted colors for frontend clustering"}},"type":"object","required":["author_profile"],"title":"RecommendationsResponse","description":"Response schema for the recommendations signals endpoint.","example":{"author_profile":{"book_count":3,"characterization":"Emerging author writing Fantasy with 3 books in 1 series","detected_genres":{"adventure":1,"fantasy":2},"has_series":true,"primary_genre":"fantasy","primary_genre_name":"Fantasy","publication_span":"emerging","series_count":1},"books":[{"cover_image":"https://example.com/cover1.jpg","extracted_colors":{"primary":"#2C3E50","secondary":"#E74C3C"},"id":"550e8400-e29b-41d4-a716-446655440000","title":"Dragon's Quest"},{"cover_image":"https://example.com/cover2.jpg","extracted_colors":{"primary":"#1A5276","secondary":"#F39C12"},"id":"550e8400-e29b-41d4-a716-446655440001","title":"Dragon's Fury"}]}},"ReconnectRequest":{"properties":{"credentials":{"additionalProperties":true,"type":"object","title":"Credentials"}},"type":"object","title":"ReconnectRequest"},"RecordCheckResult":{"properties":{"purpose":{"type":"string","enum":["apex-a","www-cname","subdomain-cname"],"title":"Purpose"},"classification":{"$ref":"#/components/schemas/Classification"},"found_values":{"items":{"type":"string"},"type":"array","title":"Found Values","default":[]},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"}},"type":"object","required":["purpose","classification"],"title":"RecordCheckResult","description":"Diagnostic result for a single record the author was asked to create."},"RemoveDomainResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"previous_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Domain"}},"type":"object","required":["success","message"],"title":"RemoveDomainResponse","description":"Response after removing custom domain."},"ResumeSubscriptionResponseSchema":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End","default":false}},"type":"object","title":"ResumeSubscriptionResponseSchema","description":"Result of un-cancelling a scheduled cancellation."},"ReviewResponse":{"properties":{"id":{"type":"string","title":"Id"},"book_id":{"type":"string","title":"Book Id"},"name":{"type":"string","title":"Name"},"rating":{"type":"integer","title":"Rating"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"text":{"type":"string","title":"Text"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","book_id","name","rating","text","created_at","updated_at"],"title":"ReviewResponse","description":"Minimal review shape for the scraper's CoreDataResponse preview.\n\n(v2 cutover, DEPLOY-2: the legacy ``review`` CRUD schema was deleted; the\nscraper preview only ever populates ``reviews`` as an empty list, so a small\nlocal shape is enough.)"},"SeriesResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"books":{"items":{"$ref":"#/components/schemas/BookResponse"},"type":"array","title":"Books","default":[]}},"type":"object","required":["id","name"],"title":"SeriesResponse","description":"Series information for UI"},"SessionListResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"Total number of items"},"offset":{"type":"integer","title":"Offset","description":"Current offset"},"limit":{"type":"integer","title":"Limit","description":"Items per page"},"sessions":{"items":{"$ref":"#/components/schemas/SessionSchema"},"type":"array","title":"Sessions"}},"type":"object","required":["total","offset","limit","sessions"],"title":"SessionListResponse","description":"Response for session list endpoint."},"SessionRevokeResponse":{"properties":{"revoked_count":{"type":"integer","title":"Revoked Count"},"message":{"type":"string","title":"Message"}},"type":"object","required":["revoked_count","message"],"title":"SessionRevokeResponse","description":"Response for session revocation."},"SessionSchema":{"properties":{"session_id":{"type":"string","title":"Session Id","description":"Unique session identifier for API operations"},"token_preview":{"type":"string","title":"Token Preview","description":"Truncated token for display only"},"user_id":{"type":"string","format":"uuid","title":"User Id"},"user_email":{"type":"string","title":"User Email"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","description":"True if this is the requesting admin's session","default":false}},"type":"object","required":["session_id","token_preview","user_id","user_email","created_at"],"title":"SessionSchema","description":"Session/token details for admin view."},"SingleBookScraperRequest":{"properties":{"url":{"type":"string","title":"Url","description":"Amazon book product URL","example":"https://www.amazon.com/dp/B08N5WRWNW"},"domain":{"type":"string","title":"Domain","description":"Amazon domain (only amazon.com is currently supported by the scraper)","default":"amazon.com"}},"type":"object","required":["url"],"title":"SingleBookScraperRequest","description":"Request schema for scraping a single Amazon book"},"SingleBookScraperResponse":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"blurb":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blurb"},"cover_image":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image"},"release_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Release Date"},"genres":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Genres"},"is_active":{"type":"boolean","title":"Is Active","default":true}},"type":"object","required":["id","title"],"title":"SingleBookScraperResponse","description":"Response schema for single book scraping - returns created book data"},"SocialLink":{"properties":{"platform":{"type":"string","title":"Platform"},"url":{"type":"string","title":"Url"},"icon":{"type":"string","title":"Icon"}},"type":"object","required":["platform","url","icon"],"title":"SocialLink","description":"Social media link"},"SubscriptionPlanSchema":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"features":{"additionalProperties":true,"type":"object","title":"Features"},"max_books":{"type":"integer","title":"Max Books"},"max_custom_domains":{"type":"integer","title":"Max Custom Domains"},"max_newsletter_subscribers":{"type":"integer","title":"Max Newsletter Subscribers"},"max_contact_messages":{"type":"integer","title":"Max Contact Messages"},"max_blog_posts":{"type":"integer","title":"Max Blog Posts"},"max_image_uploads":{"type":"integer","title":"Max Image Uploads"},"is_active":{"type":"boolean","title":"Is Active"},"is_default":{"type":"boolean","title":"Is Default"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","slug","features","max_books","max_custom_domains","max_newsletter_subscribers","max_contact_messages","max_blog_posts","max_image_uploads","is_active","is_default","created_at"],"title":"SubscriptionPlanSchema","description":"Subscription plan response schema"},"TimeSeriesPoint":{"properties":{"day":{"type":"string","format":"date","title":"Day"},"views":{"type":"integer","title":"Views","default":0},"buy_clicks":{"type":"integer","title":"Buy Clicks","default":0}},"type":"object","required":["day"],"title":"TimeSeriesPoint","description":"One day on the views-over-time series."},"TransferSitePreviewRequest":{"properties":{"source_user_id":{"type":"string","format":"uuid","title":"Source User Id"},"target_email":{"type":"string","format":"email","title":"Target Email"}},"type":"object","required":["source_user_id","target_email"],"title":"TransferSitePreviewRequest","description":"Request to preview a site transfer before executing it."},"TransferSitePreviewResponse":{"properties":{"scenario":{"type":"string","enum":["email_change","account_transfer"],"title":"Scenario"},"source_user_email":{"type":"string","title":"Source User Email"},"source_author_name":{"type":"string","title":"Source Author Name"},"source_book_count":{"type":"integer","title":"Source Book Count"},"source_website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Website Path"},"target_email":{"type":"string","title":"Target Email"},"target_user_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Target User Id"},"target_user_exists":{"type":"boolean","title":"Target User Exists"},"target_has_content":{"type":"boolean","title":"Target Has Content"},"target_plan_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Plan Slug"},"target_created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Created At"},"warnings":{"items":{"$ref":"#/components/schemas/TransferWarning"},"type":"array","title":"Warnings","default":[]}},"type":"object","required":["scenario","source_user_email","source_author_name","source_book_count","target_email","target_user_exists","target_has_content"],"title":"TransferSitePreviewResponse","description":"Preview of what a site transfer would do."},"TransferSiteRequest":{"properties":{"source_user_id":{"type":"string","format":"uuid","title":"Source User Id"},"target_email":{"type":"string","format":"email","title":"Target Email"},"secret_key":{"type":"string","title":"Secret Key","description":"Admin secret key for transfer operations"}},"type":"object","required":["source_user_id","target_email","secret_key"],"title":"TransferSiteRequest","description":"Request to execute a site transfer."},"TransferSiteResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"scenario":{"type":"string","enum":["email_change","account_transfer"],"title":"Scenario"},"message":{"type":"string","title":"Message"},"target_user_id":{"type":"string","format":"uuid","title":"Target User Id"},"author_id":{"type":"string","format":"uuid","title":"Author Id"},"website_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Path"}},"type":"object","required":["success","scenario","message","target_user_id","author_id"],"title":"TransferSiteResponse","description":"Response after executing a site transfer."},"TransferWarning":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"severity":{"type":"string","enum":["info","warning","blocker"],"title":"Severity"}},"type":"object","required":["code","message","severity"],"title":"TransferWarning","description":"Structured warning for site transfer operations."},"UpdateConnectionRequest":{"properties":{"audience_id":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Audience Id"},"tag":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Tag"},"clear_tag":{"type":"boolean","title":"Clear Tag","default":false}},"type":"object","title":"UpdateConnectionRequest","description":"Change WHERE we write. Deliberately carries no credential field.\n\nChanging which list to sync to must never require an author to dig their API\nkey out again, and a credential field here would invite a UI that asks."},"UpdatePlanSchema":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"features":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Features"},"max_books":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Books"},"max_custom_domains":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Custom Domains"},"max_newsletter_subscribers":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Newsletter Subscribers"},"max_contact_messages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Contact Messages"},"max_blog_posts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Blog Posts"},"max_image_uploads":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Image Uploads"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default"}},"type":"object","title":"UpdatePlanSchema","description":"Schema for updating a subscription plan"},"UpdateSubscriptionSchema":{"properties":{"plan_slug":{"type":"string","title":"Plan Slug"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"billing_cycle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Cycle"},"current_period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Current Period End"}},"type":"object","required":["plan_slug"],"title":"UpdateSubscriptionSchema","description":"Schema for updating user subscription (admin only)"},"UserSchema":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"type":"string","title":"Email"},"is_superuser":{"type":"boolean","title":"Is Superuser","default":false}},"type":"object","required":["email"],"title":"UserSchema"},"UserSubscriptionSchema":{"properties":{"id":{"type":"integer","title":"Id"},"plan":{"$ref":"#/components/schemas/SubscriptionPlanSchema"},"status":{"type":"string","title":"Status"},"billing_cycle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Cycle"},"current_period_start":{"type":"string","format":"date-time","title":"Current Period Start"},"current_period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Current Period End"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End"},"cancelled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cancelled At"},"on_hold_since":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"On Hold Since"},"recurring_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Recurring Amount"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"payment_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Provider"},"payment_provider_subscription_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Provider Subscription Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","plan","status","current_period_start","cancel_at_period_end","created_at"],"title":"UserSubscriptionSchema","description":"User subscription response schema"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyRequest":{"properties":{"provider":{"type":"string","maxLength":50,"minLength":1,"title":"Provider"},"credentials":{"additionalProperties":true,"type":"object","title":"Credentials"}},"type":"object","required":["provider"],"title":"VerifyRequest"},"WebsiteCreateSchema":{"properties":{"path":{"type":"string","maxLength":100,"minLength":1,"title":"Path","description":"URL path for the website (e.g., 'john-smith')"},"title":{"type":"string","maxLength":500,"minLength":1,"title":"Title","description":"Title of the website"},"analyticsId":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Analyticsid","description":"Google Analytics ID"},"customDomain":{"anyOf":[{"type":"string","maxLength":253},{"type":"null"}],"title":"Customdomain","description":"Custom domain"},"metaTitle":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Metatitle","description":"SEO meta title"},"metaDescription":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Metadescription","description":"SEO meta description"},"metaKeywords":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Metakeywords","description":"SEO keywords"},"faviconUrl":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Faviconurl","description":"CDN URL of uploaded favicon source image"},"initialDocs":{"items":{"$ref":"#/components/schemas/WebsiteInitialDocSchema"},"type":"array","minItems":1,"title":"Initialdocs","description":"Complete initial decomposed page/chrome/theme/menu document set"}},"additionalProperties":false,"type":"object","required":["path","title","initialDocs"],"title":"WebsiteCreateSchema"},"WebsiteInitialDocSchema":{"properties":{"type":{"type":"string","enum":["page","header","footer","theme","menu"],"title":"Type"},"id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Id"},"slug":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Slug"},"status":{"type":"string","const":"active","title":"Status","default":"active"},"sortOrder":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sortorder"},"data":{"additionalProperties":true,"type":"object","title":"Data"}},"additionalProperties":false,"type":"object","required":["type"],"title":"WebsiteInitialDocSchema","description":"One decomposed design document created atomically with a website."},"WebsiteSchema":{"properties":{"path":{"type":"string","title":"Path"},"title":{"type":"string","title":"Title"},"is_published":{"type":"boolean","title":"Is Published"},"is_active":{"type":"boolean","title":"Is Active"},"completion_percentage":{"type":"integer","title":"Completion Percentage"},"last_published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Published At"},"newsletter_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"analytics_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Analytics Id"},"custom_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Domain"},"meta_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Title"},"meta_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Description"},"meta_keywords":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Keywords"},"favicon_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Favicon Url"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"has_pending_changes":{"type":"boolean","title":"Has Pending Changes","default":false},"pending_scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pending Scope"}},"type":"object","required":["path","title","is_published","is_active","completion_percentage","created_at","updated_at"],"title":"WebsiteSchema"},"WebsiteUpdateSchema":{"properties":{"path":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Path"},"title":{"anyOf":[{"type":"string","maxLength":500,"minLength":1},{"type":"null"}],"title":"Title"},"is_published":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Published"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"completion_percentage":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Completion Percentage"},"analytics_id":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Analytics Id"},"custom_domain":{"anyOf":[{"type":"string","maxLength":253},{"type":"null"}],"title":"Custom Domain"},"meta_title":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Meta Title"},"meta_description":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Meta Description"},"meta_keywords":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Meta Keywords"},"favicon_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Favicon Url"}},"type":"object","title":"WebsiteUpdateSchema","example":{"analytics_id":"G-XXXXXXXXXX","is_published":true,"title":"Updated Title"}}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/email/login"}}}}}}