{"openapi": "3.1.0", "info": {"title": "Lenz API", "version": "1.0.0", "description": "API built with Django Ninja"}, "paths": {"/api/api-keys": {"post": {"operationId": "lenz_api_api_keys_create_api_key", "summary": "Create Api Key", "parameters": [], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyOut"}}}}}, "description": "Create a new API key. The full key is returned only once.", "tags": ["api-keys"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyCreateIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}, "get": {"operationId": "lenz_api_api_keys_list_api_keys", "summary": "List Api Keys", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyListOut"}}}}}, "description": "List all API keys for the current user (prefix only).", "tags": ["api-keys"], "security": [{"SessionAuth": []}]}}, "/api/api-keys/{key_id}": {"patch": {"operationId": "lenz_api_api_keys_update_api_key", "summary": "Update Api Key", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Toggle is_active or rename an API key.", "tags": ["api-keys"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyUpdateIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}, "delete": {"operationId": "lenz_api_api_keys_delete_api_key", "summary": "Delete Api Key", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Permanently delete an API key.", "tags": ["api-keys"], "security": [{"SessionAuth": []}]}}, "/api/api-keys/{key_id}/webhook-url": {"put": {"operationId": "lenz_api_api_keys_set_webhook_url", "summary": "Set Webhook Url", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Set or clear the per-key default webhook URL.\n\nEmpty string clears (webhooks disabled until set again or until a\nPOST /verify supplies a per-request override). On set, the URL is\nvalidated for SSRF safety; the same validator runs again at delivery\ntime as a DNS-rebinding defense.", "tags": ["api-keys"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyWebhookURLIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/api-keys/{key_id}/rotate-secret": {"post": {"operationId": "lenz_api_api_keys_rotate_webhook_secret", "summary": "Rotate Webhook Secret", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyRotateSecretOut"}}}}}, "description": "Generate a fresh HMAC secret for webhook signing.\n\nReturns the new secret ONCE \u2014 it's not stored in any client-readable\ncache and won't appear in subsequent list responses. The customer\nmust persist it on their side and use it to verify the\n``X-Lenz-Signature`` header on incoming webhook POSTs.\n\nRotating invalidates the previous secret immediately (any in-flight\nwebhook signed with the old secret will fail verification on the\ncustomer side \u2014 accepted tradeoff; rotations should be rare).", "tags": ["api-keys"], "security": [{"SessionAuth": []}]}}, "/api/api-keys/{key_id}/webhook-test": {"post": {"operationId": "lenz_api_api_keys_send_webhook_test", "summary": "Send Webhook Test", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}, {"in": "query", "name": "version", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Version"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIKeyWebhookTestOut"}}}}}, "description": "Deliver one signed test payload to this key's webhook URL, synchronously.\n\nBefore this existed the only way to see a webhook arrive was to submit a\nreal verification: ten credits and about a minute, against an endpoint you\nhad already written and deployed on faith. That is a poor first five\nminutes for the one feature whose whole value is not having to poll, and\nit is the likeliest reason webhook adoption sat at zero.\n\nThe delivery is the real one \u2014 same signing, same SSRF check, same\n``WebhookDelivery`` row \u2014 so a green result means a real verification's\nwebhook will arrive too. It carries `event: \"webhook.test\"` and no\n`result`, so a consumer that switches on `event` ignores it safely.\n\n``version`` (query): the API version to build the payload in, read like\nthe ``X-Lenz-API-Version`` header's value (a date before the current\nversion, or a malformed one, is ``2026-05-13``). Omitted: the version a\nheader-less call from this account resolves to (rule 3,\n``versions.account_version``), as a real webhook of theirs would be.", "tags": ["api-keys"], "security": [{"SessionAuth": []}]}}, "/api/api-keys/{key_id}/reveal": {"get": {"operationId": "lenz_api_api_keys_reveal_api_key", "summary": "Reveal Api Key", "parameters": [{"in": "path", "name": "key_id", "schema": {"title": "Key Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Return the raw key for an auto-provisioned test key.\n\nTest keys ONLY. A deliberately-created key is hash-only and shown once \u2014\nthis endpoint cannot return it, and the 404 for those rows is the same\n404 as a key that doesn't exist, so it leaks nothing either way.\n\nOwner-scoping (``request.user.api_keys``) plus session auth is the whole\naccess control; there is no additional rate limit. That is a considered\nposture, not an omission: the caller must already hold the user's session,\nat which point they can read the key from the API credentials page anyway.\n\n``Cache-Control: no-store`` is set explicitly. ``ApiNoStoreMiddleware``\ndefaults every ``/api/`` response to it anyway (since 2026-09-05), but a\ncredential must never land in a shared cache, and that must not depend\non a middleware list.\n\nBoth failure modes render as a distinct 409, never a 500: a rotated or\ncorrupt secret (``InvalidToken``) and a row with no ciphertext (issued\nwhile encryption was unconfigured). The API credentials page turns both into a\n\"re-provision test key\" affordance, because in both cases the key still\nworks \u2014 only the ability to re-read it is gone.", "tags": ["api-keys"], "security": [{"SessionAuth": []}]}}, "/api/setup/prompt": {"post": {"operationId": "lenz_api_setup_setup_prompt", "summary": "Setup Prompt", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Re-render the setup prompt for a changed chip selection.\n\nServer-side rather than in Alpine because the prompt bodies are files on\ndisk \u2014 shipping twelve of them to the browser to pick one would put the\ntext in two places and let them drift.\n\nThis is also where the test key is MINTED (2026-09-04). ``pick()`` calls\nthis on every chip click, partial selections included, so the gate has to\nbe here and not on the client: ``needs_key`` is true only once the\nselection is complete AND the route it resolves to pastes a key. Claude\nand ChatGPT never reach it, and neither does a half-picked coding-agent\nroute. Everything the mint stamps off-screen \u2014 ``api_credentials``, the\nHubSpot contact sync, ``key_waiting`` eligibility \u2014 now follows a choice\nsomeone actually made.\n\nA user who already holds a test key is not re-minted; ``resolve_test_key``\nreads and decrypts it without taking the provisioning lock. And a keyless\nroute answers ``api_key: ''`` even for a user who holds one, so the row\nthe client hides cannot be re-populated behind it. The gate itself lives\nin ``test_key_for_selection`` beside the helper, shared with the first\npaint and with ``/setup/use-case``.", "tags": ["setup"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SetupPromptIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/setup/use-case": {"post": {"operationId": "lenz_api_setup_submit_use_case", "summary": "Submit Use Case", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Store what the user says they'll use Lenz for.\n\nEvery CHANGED answer appends a row, and the newest row is the current\nanswer. A submit identical to the newest row (answer and chips both) is\nacknowledged and skipped \u2014 no row, no HubSpot enqueue, no GA4 event \u2014\nwhich is what lets the client's four save triggers (idle, blur, copy,\npagehide flush) overlap freely without inflating the fill-rate metrics.\nA per-user store cap backstops scripted distinct-text loops.\n\nIt carries no credits (onboarding PR C, 2026-09-27). The answer reward it\nused to grant, once ever through ``one_setup_reward_per_user``, is\nreplaced by the +200 on the first work action (``lenz.rewards``); those\nrewarded rows stay as history, and ``lenz.rewards`` reads them so nobody\nis paid twice.", "tags": ["setup"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UseCaseIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/setup/send": {"post": {"operationId": "lenz_api_setup_send_setup_instructions", "summary": "Send Setup Instructions", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Email the setup instructions to the signed-in user.\n\nThe mobile primary action: a 40-line prompt is not usefully copied on a\nphone, but it is trivially pasted from a laptop later.\n\nThe email carries the literal ``{{API_KEY}}`` placeholder, NEVER the key\nitself \u2014 the key lives on the API credentials page, and mail is forwarded, archived and\nsearched. ``test_setup_instructions_email_omits_the_raw_key`` is the\nenforcement against a future \"helpful\" substitution.", "tags": ["setup"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SetupPromptIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/setup/first-call": {"get": {"operationId": "lenz_api_setup_first_call", "summary": "First Call", "parameters": [{"in": "query", "name": "since", "schema": {"default": "", "title": "Since", "type": "string"}, "required": false}], "responses": {"200": {"description": "OK"}}, "description": "Has this user connected \u2014 ever, and since ``since``?\n\n``first_call_at`` is one PK-row read of ``UserProfile.first_api_call_at``,\nwhich is set-once and covers BOTH credential paths \u2014 an API-key call and an\nMCP OAuth call both land there via ``record_first_api_call``. It answers\n\"has this account ever connected\", which is what the exit link keys off.\n\n``since`` answers a different question the set-once stamp cannot: **did a\ncall land during THIS visit.** The screen needs both, because a returning\nuser has a stamp from months ago and must neither be told \"first call\nreceived\" as though it just happened, nor be unable to confirm the second\ntool they came back to connect.\n\nThe older docstring here refused to touch ``APICallLog`` at all. That was\nabout finding a user's first-ever call, which means reading their whole\nhistory; a bounded ``created_at > since`` is a short range scan off the\nexisting ``(user, -created_at)`` index and reads only rows newer than a\npage load. Different query, different cost.\n\nScoped to ``user``, never ``api_key``: an OAuth caller authenticates with\nan ``OAuthPrincipal`` and logs ``api_key=NULL``, so keying on the\ncredential would make every OAuth connection invisible \u2014 the cohort the\none-click route sends first.\n\nSide-effect free by construction. This exists as its own endpoint precisely\nso nobody bolts the poll onto ``GET /api/onboarding``, which consumes\n``is_new_signup``.", "tags": ["setup"], "security": [{"SessionAuth": []}]}}, "/api/welcome/choice": {"post": {"operationId": "lenz_api_welcome_welcome_choice", "summary": "Welcome Choice", "parameters": [], "responses": {"200": {"description": "OK"}}, "tags": ["setup"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WelcomeChoiceIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/bridge": {"get": {"operationId": "lenz_api_bridge_bridge_state", "summary": "Bridge State", "parameters": [], "responses": {"200": {"description": "OK"}}, "tags": ["setup"], "security": [{"SessionAuth": []}]}}, "/api/bridge/retire": {"post": {"operationId": "lenz_api_bridge_bridge_retire", "summary": "Bridge Retire", "parameters": [], "responses": {"200": {"description": "OK"}}, "tags": ["setup"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BridgeRetireIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/connected-apps": {"get": {"operationId": "lenz_api_connected_apps_list_connected_apps", "summary": "List Connected Apps", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ConnectedAppsListOut"}}}}}, "description": "Every authorization the current user has given: MCP connections and API grants.", "tags": ["connected-apps"], "security": [{"SessionAuth": []}]}}, "/api/connected-apps/grants/{grant_id}/revoke": {"post": {"operationId": "lenz_api_connected_apps_revoke_grant", "summary": "Revoke Grant", "parameters": [{"in": "path", "name": "grant_id", "schema": {"title": "Grant Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Disconnect one Lenz API authorization (and the MCP connection it rides).", "tags": ["connected-apps"], "security": [{"SessionAuth": []}]}}, "/api/connected-apps/grants/{grant_id}/deny": {"post": {"operationId": "lenz_api_connected_apps_deny_grant", "summary": "Deny Grant", "parameters": [{"in": "path", "name": "grant_id", "schema": {"title": "Grant Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Refuse a pending MCP host: revoked here and at WorkOS.", "tags": ["connected-apps"], "security": [{"SessionAuth": []}]}}, "/api/connected-apps/grants/{grant_id}/approve": {"post": {"operationId": "lenz_api_connected_apps_approve_grant", "summary": "Approve Grant", "parameters": [{"in": "path", "name": "grant_id", "schema": {"title": "Grant Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Approve a pending MCP host: its next token exchange succeeds.", "tags": ["connected-apps"], "security": [{"SessionAuth": []}]}}, "/api/connected-apps/revoke": {"post": {"operationId": "lenz_api_connected_apps_revoke_connected_app", "summary": "Revoke Connected App", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Disconnect one of the current user's MCP connections (a WorkOS app).\n\nOwnership-checked: the application must be one the caller has authorized.\nThe Lenz API grants riding it are revoked FIRST, so the API access is off\neven when WorkOS then fails.", "tags": ["connected-apps"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/RevokeConnectedAppIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}/annotations": {"post": {"operationId": "lenz_api_annotations_create_annotation", "summary": "Create Annotation", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AnnotationOut"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "tags": ["annotations"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AnnotationIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}, "get": {"operationId": "lenz_api_annotations_list_annotations", "summary": "List Annotations", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/AnnotationOut"}, "title": "Response", "type": "array"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Return ONLY the caller's own notes on this claim.\n\nThis is the endpoint where a mistake leaks somebody else's private note, so\nthe user filter lives in the queryset rather than in a post-filter, and\n``lenz/tests/`` asserts two users never see each other's rows.", "tags": ["annotations"], "security": [{"SessionAuth": []}]}}, "/api/annotation/{annotation_id}": {"delete": {"operationId": "lenz_api_annotations_delete_annotation", "summary": "Delete Annotation", "parameters": [{"in": "path", "name": "annotation_id", "schema": {"title": "Annotation Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OkOut"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "tags": ["annotations"], "security": [{"SessionAuth": []}]}}, "/api/attribution/apply": {"post": {"operationId": "lenz_api_attribution_apply_attribution", "summary": "Apply Attribution", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Persist localStorage-derived first-touch fields onto UserAttribution.\n\nAuth required \u2014 this is called immediately post-signup. Idempotent: empty\npayload values never clobber existing fields, so re-fires from a stale\nlocalStorage state are safe. ``source`` is promoted from\n``migrated_from_profile`` to ``live`` if any payload field is non-empty.", "tags": ["attribution"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AttributionApplyIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/": {"get": {"operationId": "lenz_api_auth_root", "summary": "Root", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "API root - health check.", "tags": ["auth"]}}, "/api/usage": {"get": {"operationId": "lenz_api_auth_get_usage", "summary": "Get Usage", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UsageOut"}}}}}, "description": "Return usage info for anonymous and authenticated users.\n\nWeb-session endpoint, read by the claim form and the workbench sidebar.\nIt reports the credit pool AND the verify-unit projection: the consumer\nsurfaces speak in verifications, so shipping only credits would push the\nfloor division into three separate JS files.", "tags": ["auth"]}}, "/api/me": {"get": {"operationId": "lenz_api_auth_get_me", "summary": "Get Me", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserOut"}}}}}, "description": "Return the current authenticated user.", "tags": ["auth"], "security": [{"SessionAuth": []}]}, "delete": {"operationId": "lenz_api_auth_delete_account", "summary": "Delete Account", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Delete the current user's account.\n\nPublic claims are disassociated (user set to None) so they remain in the\nLibrary. Private claims are permanently deleted. The Stripe side \u2014 erase\nthe customer, cancel what it bills \u2014 is NOT here: it hangs off the user\n``pre_delete`` signal (``lenz/signals.py``), dispatched on commit, so the\nadmin and this endpoint erase the same way and Stripe is told only after\nthe local rows are gone (issue #352: telling it first made its webhook\ninsert a SubscriptionEvent mid-cascade and trip the FK).", "tags": ["auth"], "security": [{"SessionAuth": []}]}}, "/api/me/profile": {"patch": {"operationId": "lenz_api_auth_update_profile", "summary": "Update Profile", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Update profile settings for the current user.", "tags": ["auth"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProfileIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/me/account-settings": {"patch": {"operationId": "lenz_api_auth_update_account_settings", "summary": "Update Account Settings", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Set the Pro and Scale account settings (session + CSRF only).\n\nInternal, on the default ``django_auth`` router: an API key, an OAuth\ntoken and the email-preferences link cannot reach it.\n\nMoving a setting away from its default is refused below paid Pro and\nScale (``lenz.coverage.plan.account_settings_available``, D3); returning\none to its default is allowed on any plan. A downgrade clears nothing: the gate keeps\nhonouring an opted-out account's choice as ``['plan', 'account']``, and the\nsweep keeps honouring a window. The switch governs checks submitted from\nnow on. A new window takes effect 24 hours after it is saved, re-armed on\nevery change (D18); clearing it (``null``) takes effect at once.", "tags": ["auth"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AccountSettingsIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/email-preferences/{token}": {"get": {"operationId": "lenz_api_auth_get_email_preferences", "summary": "Get Email Preferences", "parameters": [{"in": "path", "name": "token", "schema": {"title": "Token", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Return email preferences for a user identified by a signed token (no login required).", "tags": ["auth"]}, "patch": {"operationId": "lenz_api_auth_update_email_preferences", "summary": "Update Email Preferences", "parameters": [{"in": "path", "name": "token", "schema": {"title": "Token", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Update email preferences for a user identified by a signed token (no login required).\n\nAccepts either the legacy prefs token (bare user_pk) or the new unsub\ntoken (user_pk + email_log_id). When the user toggles a preference from\nTrue \u2192 False AND the token carries an email_log_id, we stamp\nUserProfile.unsubscribed_email_log_id so the c2 dashboard can attribute\nthe unsub to the campaign that triggered it.", "tags": ["auth"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProfileIn"}}}, "required": true}}}, "/api/history": {"get": {"operationId": "lenz_api_auth_get_history", "summary": "Get History", "parameters": [{"in": "query", "name": "page", "schema": {"default": 1, "title": "Page", "type": "integer"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HistoryOut"}}}}}, "description": "Return the authenticated user's claim history (paginated).", "tags": ["auth"], "security": [{"SessionAuth": []}]}, "delete": {"operationId": "lenz_api_auth_delete_all_claims", "summary": "Delete All Claims", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Wipe the authenticated user's history.\n\nDeletes every claim the user owns (cascades evidence/sources/etc) AND\nevery ClaimCheckLog row tied to the user \u2014 including rows pointing at\nclaims owned by someone else (cache-hit / similar-pick verifications).\nWithout the second step the page never truly empties for users who\nhave non-owned verification rows.", "tags": ["auth"], "security": [{"SessionAuth": []}]}}, "/api/onboarding": {"get": {"operationId": "lenz_api_auth_get_onboarding", "summary": "Get Onboarding", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OnboardingOut"}}}}}, "description": "Consume the new-signup flag for the client's post-signup gtag calls.", "tags": ["auth"], "security": [{"SessionAuth": []}]}}, "/api/auth/social-attempt": {"post": {"operationId": "lenz_api_auth_social_attempt", "summary": "Social Attempt", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Name the signup attempt a social button is about to leave with.\n\nThe provider's callback runs the signup signal with no header of ours, so\nthe tab records its attempt id in the session first (the Google/GitHub\nbuttons in ``_auth_flow.html``). Only an id that names a live attempt in\nTHIS session is kept; anything else is ignored. Always 204.", "tags": ["auth"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/auth/post-login-kickoff": {"post": {"operationId": "lenz_api_auth_post_login_kickoff", "summary": "Post Login Kickoff", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Server-side kickoff for the universal post-auth flow.\n\nFired by the auth-success path in auth.html / auth_callback.html /\nverify_email.html (signup OR login, all four flows: email signup, social\nsignup, email login, social login).\n\nReads ``session['signup_claim']`` (set by /api/lenz on every anon\nrejection \u2014 see lenz/api/claims.py:_paywall_403). If present, pops the\nkeys immediately to block double-submit + stale replay, calls the shared\n``kickoff_claim_for_user`` helper, and returns ``{\"redirect\": <url>}``\npointing at the in-progress claim's progress page.\n\nFalls back to ``signup_next`` (for non-paywall trigger flows) or '/'\nwhen no pending claim is in session.\n\nReturns 401 when the user is not authenticated. Helper exceptions\ndegrade gracefully to the legacy ``/?claim=<text>`` redirect so the\nhome page's auto-submit JS still picks up the work.\n\nAlso sets the ``lenz_last_user`` cookie so future visits to ``/auth``\ncan skip the email step entirely when the allauth MFA trust cookie is\nstill valid. See ``lenz/views/auth.py:_try_trust_redirect``.", "tags": ["auth"], "security": [{"SessionAuth": []}]}}, "/api/claim/stash-prefill": {"post": {"operationId": "lenz_api_auth_stash_prefill", "summary": "Stash Prefill", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Stash a claim text into session for the home-page prefill flow.\n\nCalled by progress.html's \"back to home\" / \"Try again\" buttons before\nnavigating to /, so the home view can prefill the textarea with the\nfull text (up to INPUT_TEXT_LIMIT) without going through the URL.\nReplaces the legacy `/?text=<text>` URL hop which silently truncated\nabove 500 chars.\n\nOpen to authed AND anon (an anon user can have an in-progress task and\nclick \"Try again\"). Only writes to the caller's own session; capped at\n2000 chars; no abuse vector beyond filling the caller's own session.", "tags": ["auth"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz": {"post": {"operationId": "lenz_api_claims_submit_dispatch_lenz", "summary": "Dispatch Lenz", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TaskOut"}}}}}, "description": "Submit a claim for checking. Authentication required.\n\nAnonymous visitors get 403 with `code=signup_required`. The text is\nstashed in session and replayed as an authenticated submission post-signup\n(see `_anon_submit_paywall` and `lenz/api/auth.py:compute_post_login_redirect_url`).", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/LenzIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/{task_id}/cancel": {"post": {"operationId": "lenz_api_claims_submit_cancel_lenz", "summary": "Cancel Lenz", "parameters": [{"in": "path", "name": "task_id", "schema": {"title": "Task Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Cancel a running pipeline task, or dismiss a picker.", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/{task_id}/continue": {"post": {"operationId": "lenz_api_claims_submit_continue_lenz", "summary": "Continue Lenz", "parameters": [{"in": "path", "name": "task_id", "schema": {"title": "Task Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TaskOut"}}}}}, "description": "Resume the pipeline after an intermediate state (multiple, rephrase, retry).\n\nAuthentication required \u2014 only authed users can submit/continue claims.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContinueIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/lenz/assess": {"post": {"operationId": "lenz_api_verify_page_web_assess", "summary": "Web Assess", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "One statement, one panel, one credit \u2014 the row-level action on the list.\n\nBilling mirrors the public handler step for step: afford \u2192 debit \u2192 panel \u2192\nrefund when the row delivered nothing. There is no idempotency key on an\ninternal route, so the double-charge guard is the lease above plus the\nclient disabling the row's button while the request is in flight.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebAssessIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review": {"post": {"operationId": "lenz_api_verify_page_web_review", "summary": "Web Review", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Start the tab's review. 202 ``{review_id, status}``; poll the GET.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebReviewIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/docx": {"post": {"operationId": "lenz_api_verify_page_web_review_docx_upload", "summary": "Web Review Docx Upload", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "The Workbench's Upload .docx: read the document, start its review, keep\nthe file in the private drafts store for the download. 202\n``{review_id, status, kept}``, as a paste.", "tags": ["claims"], "requestBody": {"content": {"multipart/form-data": {"schema": {"properties": {"file": {"format": "binary", "title": "File", "type": "string"}, "depth": {"default": "standard", "enum": ["standard", "low"], "title": "Depth", "type": "string"}, "idempotency_key": {"default": "", "maxLength": 64, "title": "Idempotency Key", "type": "string"}}, "required": ["file"], "title": "MultiPartBodyParams", "type": "object"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/gdoc": {"post": {"operationId": "lenz_api_verify_page_web_review_gdoc", "summary": "Web Review Gdoc", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "A link-shared Google Doc: fetched once through Google's export address\n(``lenz.docx.gdoc``, the named exception to \"never fetch pages\nourselves\"), then reviewed and kept exactly as an upload.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebGdocIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/{review_id}/back": {"get": {"operationId": "lenz_api_verify_page_web_review_back", "summary": "Web Review Back", "parameters": [{"in": "path", "name": "review_id", "schema": {"title": "Review Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Where \"\u2190 Back to your draft\" goes from a verification a review's row\nstarted (/progress and /c/ carry ``?review=``; Kosta, 2026-09-29).\n\nThe owner gets the draft's page: the Workbench's list, or /verify, which\nrestores the tab's own draft. Anyone else, anonymous, and a deleted or\nunknown review get the same ``{'url': None}``, so this never says whether\na review exists; nothing is written to the session. The pages render the\nlink from this read, so /c/ keeps its CDN cache (docs/caching.md).", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/{task_id}/input": {"get": {"operationId": "lenz_api_verify_page_deep_check_input", "summary": "Deep Check Input", "parameters": [{"in": "path", "name": "task_id", "schema": {"title": "Task Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "The text a deep check was asked on, for /verify's box when the reader\nchose \"Check the draft instead\" at the picker (?from=<task_id>; Kosta,\n2026-09-29). The owner's own only: anyone else, anonymous, and an unknown\nor text-less task get ``''``. The text never travels in a URL.", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/{review_id}": {"get": {"operationId": "lenz_api_verify_page_web_review_get", "summary": "Web Review Get", "parameters": [{"in": "path", "name": "review_id", "schema": {"title": "Review Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "The tab's review as it stands, with claim-page links. ``no-store``.\n\nBoth kinds: the draft's own review and a citations check (\"Check the\nother N\", the Workbench's Check), which polls here too.", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}, "delete": {"operationId": "lenz_api_verify_page_web_review_delete", "summary": "Web Review Delete", "parameters": [{"in": "path", "name": "review_id", "schema": {"title": "Review Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Delete the visitor's own web review (the Workbench's Delete).\n\nIts text, claims, quick checks (automatic and hand-run), citation checks\nand row links are removed through retention's purge (``blank_reviews``),\nand its zero-retention cache entries with them; every later read is 404.\nThe verifications its rows started stay: each is its own claim. Nothing is\nrefunded. Idempotent: a second delete answers 200 and does nothing.", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/{review_id}/docx": {"get": {"operationId": "lenz_api_verify_page_web_review_docx", "summary": "Web Review Docx", "parameters": [{"in": "path", "name": "review_id", "schema": {"title": "Review Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "The Workbench's Download: the review's draft as a .docx with Lenz's\nfindings as comments and its suggested edits as tracked changes\n(plan google-docs-review, D9). Built per request from what the list shows,\nnever stored. A pasted draft becomes a new document (its formatting was\nnever sent). ``no-store``, the owner's own web review only.", "tags": ["claims"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/review/{review_id}/verify": {"post": {"operationId": "lenz_api_verify_page_web_review_verify", "summary": "Web Review Verify", "parameters": [{"in": "path", "name": "review_id", "schema": {"title": "Review Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Verify rows of the visitor's own web review in full, each once.\n\nA row that already started answers its verification again (``replay``),\nfree; the others start under a task id reserved on the review first\n(``lenz.review.row_verify``), so a retry never dispatches twice. Every row\nit would start is paid for up front or none is: the quota preflight counts\nthem before the first kickoff. 200 ``{results, row_verifications}``.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ReviewVerifyIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/citations/check": {"post": {"operationId": "lenz_api_verify_page_web_citecheck", "summary": "Web Citecheck", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "\"Check the other N\": a citations-kind web review of those occurrences.\n\n202 ``{review_id, status}``; the page polls it through ``GET\n/api/lenz/review/{id}`` like the draft's own. No fair-use unit: it reads\nno page of the visitor's, and each checked citation's credit is the bound.", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CitationCheckIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/lenz/{task_id}": {"get": {"operationId": "lenz_api_claims_status_get_lenz_status", "summary": "Get Lenz Status", "parameters": [{"in": "path", "name": "task_id", "schema": {"title": "Task Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LenzStatusOut"}}}}}, "description": "Poll the status of a checking task.", "tags": ["claims"]}}, "/api/claim/{verification_id}/visibility": {"patch": {"operationId": "lenz_api_claims_admin_set_visibility", "summary": "Set Visibility", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Set the visibility of a claim (owner or staff).", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/VisibilityIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}/publish": {"post": {"operationId": "lenz_api_claims_admin_set_publish", "summary": "Set Publish", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Set a claim's editorial status (staff only).", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}/backlinks": {"put": {"operationId": "lenz_api_claims_admin_set_backlinks", "summary": "Set Backlinks", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Replace all backlink URLs for a claim (admin-only).", "tags": ["claims"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BacklinksIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}": {"delete": {"operationId": "lenz_api_claims_admin_delete_claim", "summary": "Delete Claim", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Permanently delete a claim (owner or staff).", "tags": ["claims"], "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}/history": {"delete": {"operationId": "lenz_api_claims_admin_delete_history_for_claim", "summary": "Delete History For Claim", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Remove this user's verification audit rows for a claim.\n\nHard-deletes all of ``request.user``'s ``ClaimCheckLog`` rows pointing\nat the claim. The claim itself is untouched \u2014 used by the \"Remove\nfrom history\" affordance for non-owners (and by owners as an\nequivalent of clearing their own audit trail without deleting the\nclaim). Idempotent: returns deleted=0 when there's nothing to drop.", "tags": ["claims"], "security": [{"SessionAuth": []}]}}, "/api/promo/redeem": {"post": {"operationId": "lenz_api_promos_redeem_promo", "summary": "Redeem Promo", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Redeem a one-time promotional code for bonus credits.", "tags": ["promos"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PromoRedeemIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/referral": {"get": {"operationId": "lenz_api_referrals_get_referral", "summary": "Get Referral", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ReferralOut"}}}}}, "description": "Return the authenticated user's referral code and stats.", "tags": ["referrals"], "security": [{"SessionAuth": []}]}}, "/api/referral/apply": {"post": {"operationId": "lenz_api_referrals_apply_referral", "summary": "Apply Referral", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Store a referral code to be credited after email verification.\n\nFor social (OAuth) users whose email is already verified, the reward\nis applied immediately.  For email signups, the code is stored and\napplied when the user confirms their address.\n\nReturns {\"success\": true} or {\"success\": false, \"error\": \"<reason>\"}.", "tags": ["referrals"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ReferralApplyIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/referral/invite": {"post": {"operationId": "lenz_api_referrals_send_colleague_invite", "summary": "Send Colleague Invite", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Send one invitation email (lenz/invites.py). 200 {sent, email} or\n400 {code, message}; the message is the one sentence the form shows.", "tags": ["referrals"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ColleagueInviteIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/subscription": {"get": {"operationId": "lenz_api_subscriptions_get_subscription", "summary": "Get Subscription", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubscriptionOut"}}}}}, "description": "Return the authenticated user's subscription status.", "tags": ["subscriptions"], "security": [{"SessionAuth": []}]}}, "/api/pricing": {"get": {"operationId": "lenz_api_subscriptions_get_pricing", "summary": "Get Pricing", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Which currency this visitor is shown and charged: ``{'currency': 'usd' | 'eur'}``.\n\nAlso MINTS the ``lenz_currency`` cookie, in the CDN cache key, so the\nnext page this visitor loads is rendered in their currency by the server\n(``billing.display_currency``) with no swap. It is minted here rather than\nin middleware because Cloud CDN refuses to store a response carrying\n``Set-Cookie`` and every page that prints a price is cacheable; this\nendpoint is ``no-store`` (``ApiNoStoreMiddleware``), so it may set one \u2014\nthe same out-of-band shape as ``lenz_aid`` (POST /anon-id) and\n``csrftoken`` (GET /auth/csrf).\n\nOnly the FIRST page of a visit still prints dollars and swaps client-side\n(``window.__lenzPricing`` in templates/_scripts_common.html), which is why\nthat path remains. Both checkouts charge ``billing.checkout_currency``, the\nsame decision answered here, so the price shown is the price Stripe charges.", "tags": ["subscriptions"]}}, "/api/subscribe": {"post": {"operationId": "lenz_api_subscriptions_create_subscription", "summary": "Create Subscription", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Create a Stripe Checkout Session for a Plus, Pro, or Scale subscription.\n\nCheckout is free\u2192paid ONLY. Existing active subscribers get a 409 and must\nuse the portal plan-change flow \u2014 a fresh checkout would create a second\nStripe subscription the webhook never cancels (double billing).", "tags": ["subscriptions"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubscribeIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/subscription/topup": {"post": {"operationId": "lenz_api_subscriptions_create_topup", "summary": "Create Topup", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Start a one-time top-up checkout (docs/designs/topup-credits.md).\n\nOffered to every signed-in account, comp holders included. A ``pending``\n``Purchase`` row is written once Stripe has the session: it is the test's\nexposure count, the lock ``lenz.topups.fulfil_topup`` takes, and where a\nrefund that lands before fulfilment is recorded. The offer is frozen into\nthe session metadata so fulfilment never reads today's ``TOPUP_SKUS``.\nCard only (no delayed-payment events to handle) and ``invoice_creation`` on,\nsince payment-mode Checkout creates no invoice otherwise.", "tags": ["subscriptions"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/TopupIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/subscription/portal": {"post": {"operationId": "lenz_api_subscriptions_subscription_portal", "summary": "Subscription Portal", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Create a Stripe Customer Portal session.\n\nDefault: billing management (payment method, invoices, cancel) on the\ndefault portal configuration, which has plan switching DISABLED.\n\n``flow='plan_change'``: the /plans tier-change flow \u2014 uses the dedicated\nportal configuration that enables ``subscription_update`` and deep-links\nstraight into Stripe's plan-change screen (native proration, same\nsubscription \u2014 never a second checkout).\n\n``flow='cancel'``: /plans \"Switch to Free\" \u2014 deep-links the DEFAULT\nconfiguration (the one with cancellation enabled; the plan-change one has\nit off) to its cancellation screen. Active rows only: ``is_active`` counts\na canceling subscriber, who has nothing left to cancel.", "tags": ["subscriptions"], "security": [{"SessionAuth": []}]}}, "/api/billing": {"get": {"operationId": "lenz_api_subscriptions_get_billing", "summary": "Get Billing", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BillingOut"}}}}}, "description": "Return combined billing info for the authenticated user.", "tags": ["subscriptions"], "security": [{"SessionAuth": []}]}}, "/api/claim/{verification_id}/vote": {"post": {"operationId": "lenz_api_voting_vote_claim", "summary": "Vote Claim", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VoteOut"}}}}}, "description": "Cast, change, or remove a vote on a claim.\n\nAuthenticated users: vote is tied to their account (persistent).\nAnonymous users: vote is tied to their Django session.", "tags": ["voting"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/VoteIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/claim/{verification_id}/feedback": {"post": {"operationId": "lenz_api_feedback_submit_feedback", "summary": "Submit Feedback", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeedbackOut"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Submit, change, or remove analysis feedback on a claim.\n\nIdempotent: posting the same value again removes the feedback row;\nposting a different value updates in place.", "tags": ["feedback"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeedbackIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}, "get": {"operationId": "lenz_api_feedback_get_feedback", "summary": "Get Feedback", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeedbackOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Read-only fetch \u2014 counts + the requester's current feedback (if any).", "tags": ["feedback"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/claim/{verification_id}/favorite": {"post": {"operationId": "lenz_api_favorites_toggle_favorite", "summary": "Toggle Favorite", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FavoriteOut"}}}}}, "description": "Toggle a claim as a favorite for the authenticated user.", "tags": ["favorites"], "security": [{"SessionAuth": []}]}}, "/api/favorites": {"get": {"operationId": "lenz_api_favorites_get_favorites", "summary": "Get Favorites", "parameters": [{"in": "query", "name": "page", "schema": {"default": 1, "title": "Page", "type": "integer"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FavoritesOut"}}}}}, "description": "Return the authenticated user's favorited claims (paginated).", "tags": ["favorites"], "security": [{"SessionAuth": []}]}}, "/api/library/suggest": {"get": {"operationId": "lenz_api_library_get_library_suggest", "summary": "Get Library Suggest", "parameters": [{"in": "query", "name": "q", "schema": {"default": "", "title": "Q", "type": "string"}, "required": false}, {"in": "query", "name": "domain", "schema": {"default": "", "title": "Domain", "type": "string"}, "required": false}, {"in": "query", "name": "entity", "schema": {"default": "", "title": "Entity", "type": "string"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuggestOut"}}}}}, "description": "Return autocomplete suggestions for library search.\n\nIncludes the caller's own private claims when authenticated, so the\nhome-page nav search surfaces personal verifications alongside the\npublic corpus. Entity / domain suggestions stay public-corpus-only \u2014\nthey're only shown when at least 2 public claims back them, and the\nentity-pill click-through path passes ``include_mine=true`` to\n``/api/library`` which already merges private claims into results.", "tags": ["library"]}}, "/api/library": {"get": {"operationId": "lenz_api_library_get_library", "summary": "Get Library", "parameters": [{"in": "query", "name": "domain", "schema": {"default": "", "title": "Domain", "type": "string"}, "required": false}, {"in": "query", "name": "entity", "schema": {"default": "", "title": "Entity", "type": "string"}, "required": false}, {"in": "query", "name": "search", "schema": {"default": "", "title": "Search", "type": "string"}, "required": false}, {"in": "query", "name": "sort", "schema": {"default": "recent", "title": "Sort", "type": "string"}, "required": false}, {"in": "query", "name": "page", "schema": {"default": 1, "title": "Page", "type": "integer"}, "required": false}, {"in": "query", "name": "include_mine", "schema": {"default": false, "title": "Include Mine", "type": "boolean"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LibraryOut"}}}}}, "description": "Return a paginated, publicly browsable list of claims in the Library.\n\n``include_mine`` (auth-only): when true and the user is signed in,\nsurface the caller's own claims alongside the public results \u2014 even\nif they're private. Used by the workbench browse so a click on an\nentity / domain pill shows the user their own verifications for\nthat filter, not just the public library.", "tags": ["library"]}}, "/api/library/domains": {"get": {"operationId": "lenz_api_library_get_library_domains", "summary": "Get Library Domains", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DomainsOut"}}}}}, "description": "Return the distinct domains available in the Library.", "tags": ["library"]}}, "/api/chat/{verification_id}": {"get": {"operationId": "lenz_api_chat_get_chat", "summary": "Get Chat", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChatOut"}}}}}, "description": "Load chat history and quota info for a claim.", "tags": ["chat"], "security": [{"SessionAuthAnonAllowed": []}]}, "delete": {"operationId": "lenz_api_chat_reset_chat", "summary": "Reset Chat", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK"}}, "description": "Delete all chat messages on a claim for the current user.\n\nQuota counters are NOT reset \u2014 this is a fresh-conversation affordance,\nnot a balance-recovery one, and the credits are already spent from the\npool.", "tags": ["chat"], "security": [{"SessionAuthAnonAllowed": []}]}, "post": {"operationId": "lenz_api_chat_send_chat", "summary": "Send Chat", "parameters": [{"in": "path", "name": "verification_id", "schema": {"title": "Verification Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChatOut"}}}}}, "description": "Send a chat message to Lenz and get a response.", "tags": ["chat"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChatIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/contact": {"post": {"operationId": "lenz_api_contact_submit_contact", "summary": "Submit Contact", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Email support and open a HubSpot ticket. Stores nothing locally.\n\n``lenz.ContactMessage`` used to be written here first and was the model the\nadmin's \"Support tickets\" screen listed. It was retired 2026-09-10: the\nticket lives in HubSpot, where support is actually worked, and the mail\nlands in the inbox that answers it. A third copy in Postgres that only\n``/admin/`` could read was one more place to keep in sync, and the one\nnobody used.", "tags": ["contact"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactIn"}}}, "required": true}, "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/support-chat/identity": {"get": {"operationId": "lenz_api_support_chat_support_chat_identity", "summary": "Support Chat Identity", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "``{email, token}`` for the chat widget, or 204: chat unidentified.", "tags": ["support-chat"], "security": [{"SessionAuthAnonAllowed": []}]}}, "/api/wa-cta": {"post": {"operationId": "lenz_api_wa_cta_create_wa_cta", "summary": "Create Wa Cta", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WaCtaOut"}}}}}, "description": "Record the CTA click + return the bare ``wa.me`` URL.\n\nNever authenticated \u2014 the CTA is public-facing and the flow works for\nanonymous visitors whose account isn't created yet.", "tags": ["whatsapp"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WaCtaIn"}}}, "required": true}}}, "/api/whatsapp/link": {"post": {"operationId": "lenz_api_whatsapp_link_link_whatsapp", "summary": "Link Whatsapp", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Link a WhatsApp number to the authenticated user's account.", "tags": ["whatsapp"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WhatsAppLinkIn"}}}, "required": true}, "security": [{"SessionAuth": []}]}}, "/api/whatsapp/unlink": {"post": {"operationId": "lenz_api_whatsapp_link_unlink_whatsapp", "summary": "Unlink Whatsapp", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Detach the current user from their linked WhatsApp number.\n\nIdempotent: returns ``ok=True`` whether or not anything was linked.\nThe WhatsApp row itself survives \u2014 only the ``user`` FK is cleared \u2014\nso the WhatsApp user retains their audit trail and can later /link\nagain (to this or a different web account).", "tags": ["whatsapp"], "security": [{"SessionAuth": []}]}}, "/api/workbench/search": {"get": {"operationId": "lenz_api_workbench_serializers_workbench_search", "summary": "Workbench Search", "parameters": [{"in": "query", "name": "q", "schema": {"default": "", "title": "Q", "type": "string"}, "required": false}, {"in": "query", "name": "scope", "schema": {"default": "mine", "title": "Scope", "type": "string"}, "required": false}, {"in": "query", "name": "tab", "schema": {"default": "", "title": "Tab", "type": "string"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WorkbenchSearchOut"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Sidebar autocomplete: claims, lists, entity pills and domain pills.\n\nTwo scopes (Kosta, 2026-09-28):\n\n- ``mine`` (the default): the reader's own checks, the Checks tab's rows\n  (``_claims_queryset``, ``_reviews_queryset``): verified claims, and reviews by their text or one\n  of their claims. Entity and domain pills are drawn from the claims the\n  reader checked, so a pill click lands on something non-empty.\n- ``published`` (``global`` is its old name): the published library, as\n  a library visitor sees it; nothing private, the reader's own included,\n  which is what Mine is for.\n\n``tab`` scopes Mine as \"See all results\" does, so both read the same\nrows: Reviews suggests reviews only, Verified and Bookmarked claims and\ntheir topics only. No tab (a page from before) suggests both.\n\nShort queries (< 2 chars) return empty buckets without hitting the DB.", "tags": ["workbench"], "security": [{"SessionAuth": []}]}}, "/api/workbench/library": {"get": {"operationId": "lenz_api_workbench_serializers_workbench_library", "summary": "Workbench Library", "parameters": [{"in": "query", "name": "tab", "schema": {"default": "reviews", "title": "Tab", "type": "string"}, "required": false}, {"in": "query", "name": "page", "schema": {"default": 1, "title": "Page", "type": "integer"}, "required": false}, {"in": "query", "name": "q", "schema": {"default": "", "title": "Q", "type": "string"}, "required": false}, {"in": "query", "name": "scope", "schema": {"default": "", "title": "Scope", "type": "string"}, "required": false}, {"in": "query", "name": "domain", "schema": {"default": "", "title": "Domain", "type": "string"}, "required": false}, {"in": "query", "name": "entity", "schema": {"default": "", "title": "Entity", "type": "string"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WorkbenchLibraryOut"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Paginated list for the workbench main panel.\n\n- **Tab mode** (default): the sidebar's \"See all\". Each tab pages exactly\n  the rows it lists (``_reviews_queryset`` / ``_claims_queryset``); a\n  topic chip filters the verifications by ``domain`` or ``entity``;\n  Bookmarked pages the bookmarks.\n- **Search mode** (``q`` set): ``scope=mine`` (the default) searches the\n  reader's checks, lists included, in the same shape; ``scope=published``\n  searches the published library.", "tags": ["workbench"], "security": [{"SessionAuth": []}]}}, "/api/workbench/state": {"get": {"operationId": "lenz_api_workbench_state_workbench_state", "summary": "Workbench State", "parameters": [{"in": "query", "name": "active", "schema": {"default": "", "title": "Active", "type": "string"}, "required": false}, {"in": "query", "name": "tab", "schema": {"default": "reviews", "title": "Tab", "type": "string"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WorkbenchStateOut"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}}, "description": "Return the live workbench state for the authed user.\n\nParameters:\n    active: ``task_id`` or ``verification_id`` of the claim that should\n        populate the document area. If absent, the server picks the\n        user's most-recent completed claim, or returns\n        ``active.kind='empty'`` if their history is empty.\n    tab: ``'reviews'`` | ``'verified'`` | ``'favorites'``: which panel\n        tab's entries to populate. A stored ``checks``, ``verifications``\n        or ``viewed`` from before reads as Verified.", "tags": ["workbench"], "security": [{"SessionAuth": []}]}}}, "components": {"schemas": {"APIKeyOut": {"description": "Returned only at creation time \u2014 includes the full key.", "properties": {"id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "prefix": {"title": "Prefix", "type": "string"}, "is_active": {"title": "Is Active", "type": "boolean"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}, "last_used_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Last Used At"}, "webhook_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Webhook Url"}, "has_webhook_secret": {"default": false, "title": "Has Webhook Secret", "type": "boolean"}, "is_test_key": {"default": false, "title": "Is Test Key", "type": "boolean"}, "key": {"title": "Key", "type": "string"}}, "required": ["id", "name", "prefix", "is_active", "created_at", "key"], "title": "APIKeyOut", "type": "object"}, "APIKeyCreateIn": {"properties": {"name": {"title": "Name", "type": "string"}}, "required": ["name"], "title": "APIKeyCreateIn", "type": "object"}, "APIKeyItemOut": {"properties": {"id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "prefix": {"title": "Prefix", "type": "string"}, "is_active": {"title": "Is Active", "type": "boolean"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}, "last_used_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Last Used At"}, "webhook_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Webhook Url"}, "has_webhook_secret": {"default": false, "title": "Has Webhook Secret", "type": "boolean"}, "is_test_key": {"default": false, "title": "Is Test Key", "type": "boolean"}}, "required": ["id", "name", "prefix", "is_active", "created_at"], "title": "APIKeyItemOut", "type": "object"}, "APIKeyListOut": {"properties": {"keys": {"items": {"$ref": "#/components/schemas/APIKeyItemOut"}, "title": "Keys", "type": "array"}}, "required": ["keys"], "title": "APIKeyListOut", "type": "object"}, "APIKeyUpdateIn": {"properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active"}}, "title": "APIKeyUpdateIn", "type": "object"}, "APIKeyWebhookURLIn": {"properties": {"webhook_url": {"default": "", "title": "Webhook Url", "type": "string"}}, "title": "APIKeyWebhookURLIn", "type": "object"}, "APIKeyRotateSecretOut": {"description": "One-time response with the freshly minted HMAC secret. NEVER stored\non the client side by this server in any cookie/cache \u2014 the customer\nmust save it themselves.", "properties": {"id": {"title": "Id", "type": "integer"}, "hmac_secret": {"title": "Hmac Secret", "type": "string"}}, "required": ["id", "hmac_secret"], "title": "APIKeyRotateSecretOut", "type": "object"}, "APIKeyWebhookTestOut": {"description": "Outcome of one synchronous test delivery.\n\n``ok`` is the only field a caller needs; the rest exists so the API credentials page can\nsay WHY without a second round trip. A refused connection or a 500 from the\ncustomer's endpoint is a successful test that reports a failure \u2014 the\nendpoint returns 200 with ``ok: false``, not an error status, because \"your\nendpoint rejected it\" is the answer they asked for.", "properties": {"ok": {"title": "Ok", "type": "boolean"}, "event": {"default": "", "title": "Event", "type": "string"}, "task_id": {"default": "", "title": "Task Id", "type": "string"}, "url": {"default": "", "title": "Url", "type": "string"}, "status_code": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Status Code"}, "elapsed_ms": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Elapsed Ms"}, "error": {"default": "", "title": "Error", "type": "string"}, "signature_header": {"default": "", "title": "Signature Header", "type": "string"}}, "required": ["ok"], "title": "APIKeyWebhookTestOut", "type": "object"}, "SetupPromptIn": {"properties": {"environment": {"default": "", "title": "Environment", "type": "string"}, "connector": {"default": "", "title": "Connector", "type": "string"}, "language": {"default": "", "title": "Language", "type": "string"}}, "title": "SetupPromptIn", "type": "object"}, "UseCaseIn": {"properties": {"use_case": {"default": "", "title": "Use Case", "type": "string"}, "surface": {"default": "", "title": "Surface", "type": "string"}, "environment": {"default": "", "title": "Environment", "type": "string"}, "connector": {"default": "", "title": "Connector", "type": "string"}, "language": {"default": "", "title": "Language", "type": "string"}, "from_example": {"default": false, "title": "From Example", "type": "boolean"}}, "title": "UseCaseIn", "type": "object"}, "WelcomeChoiceIn": {"properties": {"choice": {"default": "", "title": "Choice", "type": "string"}}, "title": "WelcomeChoiceIn", "type": "object"}, "BridgeRetireIn": {"properties": {"action": {"default": "", "title": "Action", "type": "string"}}, "title": "BridgeRetireIn", "type": "object"}, "ConnectedAppOut": {"description": "One OAuth app the user has authorized (a WorkOS authorized application).\n\nUnlike sessions, the authorization record carries the real app name, so the\nUI labels each entry directly instead of guessing from a user agent.", "properties": {"application_id": {"title": "Application Id", "type": "string"}, "name": {"title": "Name", "type": "string"}, "scopes": {"default": [], "items": {"type": "string"}, "title": "Scopes", "type": "array"}, "grant_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Grant Id"}, "api_scopes": {"default": [], "items": {"$ref": "#/components/schemas/GrantScopeOut"}, "title": "Api Scopes", "type": "array"}, "recognised": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Recognised"}}, "required": ["application_id", "name"], "title": "ConnectedAppOut", "type": "object"}, "ConnectedAppsListOut": {"properties": {"apps": {"items": {"$ref": "#/components/schemas/ConnectedAppOut"}, "title": "Apps", "type": "array"}, "apps_error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Apps Error"}, "pending": {"default": [], "items": {"$ref": "#/components/schemas/GrantOut"}, "title": "Pending", "type": "array"}, "grants": {"default": [], "items": {"$ref": "#/components/schemas/GrantOut"}, "title": "Grants", "type": "array"}}, "required": ["apps"], "title": "ConnectedAppsListOut", "type": "object"}, "GrantOut": {"description": "One Lenz API authorization (``OAuthGrant``).\n\n``claimed_name`` is what the application calls itself: anyone can register\na client under any name, so the page presents it as a claim, beside\n``client_id``, never as a fact (ruling B).", "properties": {"grant_id": {"title": "Grant Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "claimed_name": {"default": "", "title": "Claimed Name", "type": "string"}, "client_id": {"default": "", "title": "Client Id", "type": "string"}, "scopes": {"default": [], "items": {"$ref": "#/components/schemas/GrantScopeOut"}, "title": "Scopes", "type": "array"}, "created_at": {"title": "Created At", "type": "string"}, "last_used_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Last Used At"}, "recognised": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Recognised"}}, "required": ["grant_id", "name", "created_at"], "title": "GrantOut", "type": "object"}, "GrantScopeOut": {"properties": {"name": {"title": "Name", "type": "string"}, "label": {"title": "Label", "type": "string"}}, "required": ["name", "label"], "title": "GrantScopeOut", "type": "object"}, "RevokeConnectedAppIn": {"properties": {"application_id": {"title": "Application Id", "type": "string"}}, "required": ["application_id"], "title": "RevokeConnectedAppIn", "type": "object"}, "AnnotationOut": {"description": "Single annotation response.\n\n``author_name`` is gone with display names \u2014 you only ever see your own\nnotes, so there is no author to name. ``is_private`` and ``status`` stay:\nthey are always ``True`` / ``approved`` now, but a client that reads them\nkeeps working, and dropping them would be a needless response-shape break.", "properties": {"id": {"title": "Id", "type": "integer"}, "section": {"title": "Section", "type": "string"}, "anchor_text": {"title": "Anchor Text", "type": "string"}, "anchor_prefix": {"default": "", "title": "Anchor Prefix", "type": "string"}, "anchor_suffix": {"default": "", "title": "Anchor Suffix", "type": "string"}, "body": {"title": "Body", "type": "string"}, "is_private": {"default": true, "title": "Is Private", "type": "boolean"}, "status": {"default": "approved", "title": "Status", "type": "string"}, "created_at": {"title": "Created At", "type": "string"}}, "required": ["id", "section", "anchor_text", "body", "created_at"], "title": "AnnotationOut", "type": "object"}, "ErrorOut": {"description": "Generic error envelope for 401/403/404/422 with no extra fields.\n\nEndpoints that return *structured* error fields (e.g. multi-submit's\nquota / kickoff error shapes) should declare their own per-endpoint\nschema instead \u2014 Pydantic strict mode will drop or reject the extra\nkeys against this generic shape.", "properties": {"detail": {"title": "Detail", "type": "string"}, "code": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Code"}}, "required": ["detail"], "title": "ErrorOut", "type": "object"}, "AnnotationIn": {"description": "Input for creating a private note on a claim.\n\n``is_private`` is deliberately absent. Annotations are private notes since\n2026-08-15 and the server sets the flag; accepting it here would let a\nclient ask for a publicly visible note, which is the one request this\nsurface must not honour.", "properties": {"section": {"title": "Section", "type": "string"}, "anchor_text": {"title": "Anchor Text", "type": "string"}, "anchor_prefix": {"default": "", "title": "Anchor Prefix", "type": "string"}, "anchor_suffix": {"default": "", "title": "Anchor Suffix", "type": "string"}, "body": {"title": "Body", "type": "string"}}, "required": ["section", "anchor_text", "body"], "title": "AnnotationIn", "type": "object"}, "OkOut": {"description": "Generic mutation-success envelope for endpoints that return\n``{ok: True}`` (optionally with a ``status`` echo). Pair with\n``exclude_none=True`` so the ``status`` key only appears when set.", "properties": {"ok": {"default": true, "title": "Ok", "type": "boolean"}, "status": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Status"}}, "title": "OkOut", "type": "object"}, "AttributionApplyIn": {"properties": {"utm_source": {"default": "", "title": "Utm Source", "type": "string"}, "utm_medium": {"default": "", "title": "Utm Medium", "type": "string"}, "utm_campaign": {"default": "", "title": "Utm Campaign", "type": "string"}, "gclid": {"default": "", "title": "Gclid", "type": "string"}, "gbraid": {"default": "", "title": "Gbraid", "type": "string"}, "wbraid": {"default": "", "title": "Wbraid", "type": "string"}, "gad_source": {"default": "", "title": "Gad Source", "type": "string"}, "li_fat_id": {"default": "", "title": "Li Fat Id", "type": "string"}, "utm_content": {"default": "", "title": "Utm Content", "type": "string"}, "oppref": {"default": "", "title": "Oppref", "type": "string"}, "landing_path": {"default": "", "title": "Landing Path", "type": "string"}, "referrer": {"default": "", "title": "Referrer", "type": "string"}, "ga_client_id": {"default": "", "title": "Ga Client Id", "type": "string"}, "captured_at": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Captured At"}}, "title": "AttributionApplyIn", "type": "object"}, "UsageOut": {"description": "Usage info for both anonymous and authenticated users.\n\nWeb-session endpoint (``/api/usage``), not the public API. ``credits``\nhere is the whole pool balance, not a top-up bucket.", "properties": {"authenticated": {"title": "Authenticated", "type": "boolean"}, "subscribed": {"default": false, "title": "Subscribed", "type": "boolean"}, "tier": {"default": "free", "title": "Tier", "type": "string"}, "has_capacity": {"default": true, "title": "Has Capacity", "type": "boolean"}, "credits_total": {"default": 0, "title": "Credits Total", "type": "integer"}, "credits_used": {"default": 0, "title": "Credits Used", "type": "integer"}, "credits_remaining": {"default": 0, "title": "Credits Remaining", "type": "integer"}, "extra_credits": {"default": 0, "title": "Extra Credits", "type": "integer"}, "quota_used": {"default": 0, "title": "Quota Used", "type": "integer"}, "quota_total": {"default": 0, "title": "Quota Total", "type": "integer"}, "anon_checks_used": {"default": 0, "title": "Anon Checks Used", "type": "integer"}, "anon_checks_limit": {"default": 0, "title": "Anon Checks Limit", "type": "integer"}}, "required": ["authenticated"], "title": "UsageOut", "type": "object"}, "UserOut": {"description": "Current user info.", "properties": {"email": {"title": "Email", "type": "string"}, "first_name": {"title": "First Name", "type": "string"}, "last_name": {"title": "Last Name", "type": "string"}, "email_verified": {"default": false, "title": "Email Verified", "type": "boolean"}, "is_staff": {"default": false, "title": "Is Staff", "type": "boolean"}, "show_engagement_stats": {"default": false, "title": "Show Engagement Stats", "type": "boolean"}, "notify_claim_related": {"default": true, "title": "Notify Claim Related", "type": "boolean"}, "marketing_email": {"default": true, "title": "Marketing Email", "type": "boolean"}, "country": {"default": "", "title": "Country", "type": "string"}, "city": {"default": "", "title": "City", "type": "string"}, "ga_user_id": {"default": "", "title": "Ga User Id", "type": "string"}}, "required": ["email", "first_name", "last_name"], "title": "UserOut", "type": "object"}, "ProfileIn": {"description": "Input for updating profile settings.", "properties": {"first_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "First Name"}, "last_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Last Name"}, "notify_claim_related": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Notify Claim Related"}, "marketing_email": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Marketing Email"}}, "title": "ProfileIn", "type": "object"}, "AccountSettingsIn": {"additionalProperties": false, "description": "``PATCH /api/me/account-settings`` \u2014 the Pro and Scale account settings.\n\nIts own schema, never ``ProfileIn``: that one also backs the token-only\nemail-preferences PATCH, whose link rides every marketing email, and a\nsetting that changes what Lenz warrants or keeps must not be reachable from\nit. ``extra='forbid'`` so an unknown field is a 422, not a silent no-op\nthe caller reads as saved. An omitted field is left alone. For the switch\n``null`` is refused, never read as \"off\"; for the window ``null`` clears\nit (unlimited, at once).", "properties": {"coverage_opted_out": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Coverage Opted Out"}, "retention_days": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Retention Days"}}, "title": "AccountSettingsIn", "type": "object"}, "HistoryItemOut": {"description": "Single history entry.", "properties": {"verification_id": {"title": "Verification Id", "type": "string"}, "slug": {"default": "", "title": "Slug", "type": "string"}, "text": {"title": "Text", "type": "string"}, "atomic_claim": {"default": "", "title": "Atomic Claim", "type": "string"}, "conclusion_label": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Conclusion Label"}, "lenz_score": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Lenz Score"}, "executive_summary": {"default": "", "title": "Executive Summary", "type": "string"}, "domain": {"default": "", "title": "Domain", "type": "string"}, "entities": {"default": [], "items": {"type": "string"}, "title": "Entities", "type": "array"}, "visibility": {"default": "private", "title": "Visibility", "type": "string"}, "votes": {"allOf": [{"$ref": "#/components/schemas/VotesDict"}], "default": {"true": 0, "mostly_true": 0, "mixed": 0, "mostly_false": 0, "false": 0}}, "user_vote": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "User Vote"}, "published_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Published At"}, "created_at": {"title": "Created At", "type": "string"}, "is_favorited": {"default": false, "title": "Is Favorited", "type": "boolean"}, "og_image_url": {"default": "", "title": "Og Image Url", "type": "string"}, "og_image_webp_url": {"default": "", "title": "Og Image Webp Url", "type": "string"}, "key_finding": {"default": "", "title": "Key Finding", "type": "string"}}, "required": ["verification_id", "text", "created_at"], "title": "HistoryItemOut", "type": "object"}, "HistoryOut": {"description": "Paginated history response.", "properties": {"items": {"items": {"$ref": "#/components/schemas/HistoryItemOut"}, "title": "Items", "type": "array"}, "total": {"title": "Total", "type": "integer"}, "page": {"title": "Page", "type": "integer"}, "page_size": {"title": "Page Size", "type": "integer"}}, "required": ["items", "total", "page", "page_size"], "title": "HistoryOut", "type": "object"}, "VotesDict": {"description": "Vote counts by category.", "properties": {"true": {"default": 0, "title": "True", "type": "integer"}, "mostly_true": {"default": 0, "title": "Mostly True", "type": "integer"}, "mixed": {"default": 0, "title": "Mixed", "type": "integer"}, "mostly_false": {"default": 0, "title": "Mostly False", "type": "integer"}, "false": {"default": 0, "title": "False", "type": "integer"}}, "title": "VotesDict", "type": "object"}, "OnboardingOut": {"description": "Onboarding status after signup/login.", "properties": {"is_new_signup": {"default": false, "title": "Is New Signup", "type": "boolean"}, "sha256_email": {"default": "", "title": "Sha256 Email", "type": "string"}, "ga_user_id": {"default": "", "title": "Ga User Id", "type": "string"}}, "title": "OnboardingOut", "type": "object"}, "TaskOut": {"description": "Response when a task is dispatched.\n\n``redirect`` carries the URL the client should navigate to after the\nsubmission. Standard flow: ``/progress/<task_id>``. Workbench:\n``/workbench?activate=<task_id>``. The server computes it from the\nviewport, ``settings.DESKTOP_CLAIM_SURFACE``, and the originating\nsurface (an ``X-Lenz-Surface: workbench`` request header keeps a\nworkbench submit in the workbench) \u2014 see\n``lenz/api/claims_helpers.py:compute_redirect``. Clients that don't\ncare about the field can ignore it; clients that do navigate to it\ninstead of hard-coding a destination.", "properties": {"task_id": {"title": "Task Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "chain_id": {"default": "", "title": "Chain Id", "type": "string"}, "redirect": {"default": "", "title": "Redirect", "type": "string"}}, "required": ["task_id", "status"], "title": "TaskOut", "type": "object"}, "LenzIn": {"description": "Input for the checking pipeline.\n\nNo GA4 attribution rides a submission (the claim_complete event it fed was\nretired 2026-09-27). Old in-flight clients sending those fields are\ntolerated \u2014 Ninja's default Pydantic config ignores unknown\nfields, so no 422 during rolling deploys.", "properties": {"text": {"default": "", "title": "Text", "type": "string"}, "source_url": {"default": "", "title": "Source Url", "type": "string"}, "referrer": {"default": "", "title": "Referrer", "type": "string"}, "language": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Language"}, "depth": {"anyOf": [{"enum": ["standard", "low"], "type": "string"}, {"type": "null"}], "title": "Depth"}, "review_id": {"default": "", "title": "Review Id", "type": "string"}}, "title": "LenzIn", "type": "object"}, "ContinueIn": {"description": "Input for continuing a paused pipeline (multi-claim, not-a-claim rephrase, retry).\n\nMirrors the request body that progress.html POSTs to /api/lenz/<task_id>/continue.\n`/continue` is auth-only post-cleanup (migration 0214) \u2014 anon callers can't\nreach it. The `text` cap is the largest any surface may submit, so authed\ncallers can't post arbitrarily large payloads through this endpoint. It is\na REJECTION, so it has to be the ceiling of the most permissive surface;\nthe handler then truncates to the ceiling of the surface the resumed run\nactually came from (`lenz.api.helpers.input_text_limit`).", "properties": {"text": {"anyOf": [{"maxLength": 50000, "type": "string"}, {"type": "null"}], "title": "Text"}, "source": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Source"}, "depth": {"anyOf": [{"enum": ["standard", "low"], "type": "string"}, {"type": "null"}], "title": "Depth"}}, "title": "ContinueIn", "type": "object"}, "WebAssessIn": {"description": "One statement the extractor emitted on this page.", "properties": {"claim": {"default": "", "title": "Claim", "type": "string"}, "language": {"default": "", "title": "Language", "type": "string"}, "review_id": {"default": "", "title": "Review Id", "type": "string"}}, "title": "WebAssessIn", "type": "object"}, "WebReviewIn": {"additionalProperties": false, "description": "A draft pasted into the tab or the Workbench, or one public link.\n\nThe Workbench alone sends ``auto_check`` (how many of the draft's claims\nand citations its review checks: its top bar's \"Auto-check\") and\n``depth`` (the verification a one-claim draft turns into). Both pages\nsend ``idempotency_key``, their per-submit id: a resend is the same\nreview, never a second charge. The draft tab's review is fixed\nserver-side.", "properties": {"text": {"default": "", "title": "Text", "type": "string"}, "language": {"default": "", "title": "Language", "type": "string"}, "surface": {"default": "draft", "enum": ["draft", "workbench"], "title": "Surface", "type": "string"}, "auto_check": {"anyOf": [{"enum": [0, 5, 10, 20], "type": "integer"}, {"type": "null"}], "title": "Auto Check"}, "depth": {"anyOf": [{"enum": ["standard", "low"], "type": "string"}, {"type": "null"}], "title": "Depth"}, "idempotency_key": {"default": "", "maxLength": 64, "title": "Idempotency Key", "type": "string"}}, "title": "WebReviewIn", "type": "object"}, "WebGdocIn": {"additionalProperties": false, "description": "A Google Docs link pasted into the Workbench or the /verify box (plan\ngoogle-docs-review, PR (iii); plan verify-one-path, 9). Only its document\nid is used. ``surface`` is the page it came from: /verify's review keeps\n/verify's policy (``WEB_REVIEW_POLICIES['draft']``) and has no low\ndepth.", "properties": {"url": {"maxLength": 2048, "title": "Url", "type": "string"}, "surface": {"default": "workbench", "enum": ["draft", "workbench"], "title": "Surface", "type": "string"}, "depth": {"default": "standard", "enum": ["standard", "low"], "title": "Depth", "type": "string"}, "idempotency_key": {"default": "", "maxLength": 64, "title": "Idempotency Key", "type": "string"}}, "required": ["url"], "title": "WebGdocIn", "type": "object"}, "ReviewVerifyIn": {"additionalProperties": false, "description": "Rows of a web review to verify in full: one row's Verify, or\n\"Verify N selected\". The statements are the review's, never sent.", "properties": {"indexes": {"items": {"type": "integer"}, "title": "Indexes", "type": "array"}, "depth": {"default": "standard", "enum": ["standard", "low"], "title": "Depth", "type": "string"}, "surface": {"default": "draft", "enum": ["draft", "workbench"], "title": "Surface", "type": "string"}}, "required": ["indexes"], "title": "ReviewVerifyIn", "type": "object"}, "CitationCheckIn": {"additionalProperties": false, "description": "Citations of a draft to check. With ``review_id`` (the draft's own web\nreview) the draft is the review's, never the page's, the check is kept on\nit, and a citation the review or an earlier check already holds is not\nchecked again. ``idempotency_key``: a resend of one click is one check.", "properties": {"text": {"default": "", "title": "Text", "type": "string"}, "indexes": {"default": [], "items": {"type": "integer"}, "title": "Indexes", "type": "array"}, "language": {"default": "", "title": "Language", "type": "string"}, "review_id": {"default": "", "title": "Review Id", "type": "string"}, "idempotency_key": {"default": "", "maxLength": 64, "title": "Idempotency Key", "type": "string"}}, "title": "CitationCheckIn", "type": "object"}, "LenzStatusOut": {"description": "Status polling response.", "properties": {"task_id": {"title": "Task Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "result": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Result"}, "progress": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Progress"}, "verification_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Verification Id"}, "slug": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Slug"}, "domain": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Domain"}, "visibility": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Visibility"}, "votes": {"anyOf": [{"$ref": "#/components/schemas/VotesDict"}, {"type": "null"}]}, "user_vote": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "User Vote"}, "cached_from": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Cached From"}, "chain_id": {"default": "", "title": "Chain Id", "type": "string"}, "detail": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Detail"}, "failure": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Failure"}, "stage": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Stage"}, "outcome": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Outcome"}, "current_stage_partial": {"default": "", "title": "Current Stage Partial", "type": "string"}, "sections": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Sections"}, "interrupt": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Interrupt"}}, "required": ["task_id", "status"], "title": "LenzStatusOut", "type": "object"}, "VisibilityIn": {"description": "Input for setting claim visibility.", "properties": {"visibility": {"title": "Visibility", "type": "string"}}, "required": ["visibility"], "title": "VisibilityIn", "type": "object"}, "PublishIn": {"description": "Input for setting a claim's editorial status.", "properties": {"status": {"enum": ["pending", "published", "hidden"], "title": "Status", "type": "string"}}, "required": ["status"], "title": "PublishIn", "type": "object"}, "BacklinksIn": {"description": "Input for setting backlink URLs on a claim.", "properties": {"backlinks": {"title": "Backlinks", "type": "string"}}, "required": ["backlinks"], "title": "BacklinksIn", "type": "object"}, "PromoRedeemIn": {"properties": {"code": {"title": "Code", "type": "string"}}, "required": ["code"], "title": "PromoRedeemIn", "type": "object"}, "ReferralOut": {"properties": {"code": {"title": "Code", "type": "string"}, "url": {"title": "Url", "type": "string"}, "total_referred": {"title": "Total Referred", "type": "integer"}, "credits_earned": {"title": "Credits Earned", "type": "integer"}}, "required": ["code", "url", "total_referred", "credits_earned"], "title": "ReferralOut", "type": "object"}, "ReferralApplyIn": {"properties": {"code": {"title": "Code", "type": "string"}}, "required": ["code"], "title": "ReferralApplyIn", "type": "object"}, "ColleagueInviteIn": {"properties": {"email": {"default": "", "title": "Email", "type": "string"}, "purpose": {"default": "", "title": "Purpose", "type": "string"}}, "title": "ColleagueInviteIn", "type": "object"}, "SubscriptionOut": {"properties": {"status": {"title": "Status", "type": "string"}, "current_period_end": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Current Period End"}, "subscribed": {"title": "Subscribed", "type": "boolean"}}, "required": ["status", "subscribed"], "title": "SubscriptionOut", "type": "object"}, "SubscribeIn": {"properties": {"plan": {"default": "plus", "title": "Plan", "type": "string"}, "interval": {"default": "month", "title": "Interval", "type": "string"}, "wall_id": {"default": "", "title": "Wall Id", "type": "string"}, "currency": {"default": "", "title": "Currency", "type": "string"}}, "title": "SubscribeIn", "type": "object"}, "TopupIn": {"properties": {"sku": {"default": "", "title": "Sku", "type": "string"}, "wall_id": {"default": "", "title": "Wall Id", "type": "string"}, "currency": {"default": "", "title": "Currency", "type": "string"}}, "title": "TopupIn", "type": "object"}, "BillingCreditsOut": {"description": "The account's credit pool: monthly allowance plus non-expiring extra credits.", "properties": {"total": {"default": 0, "title": "Total", "type": "integer"}, "used": {"default": 0, "title": "Used", "type": "integer"}, "remaining": {"default": 0, "title": "Remaining", "type": "integer"}, "extra": {"default": 0, "title": "Extra", "type": "integer"}}, "title": "BillingCreditsOut", "type": "object"}, "BillingOut": {"description": "Web-session payload for /billing and the /plans tier badge.\n\nNOT the public API \u2014 it is session-authed and its only two consumers ship\nin the same deploy, so the shape changes in lockstep with them rather than\ncarrying a compatibility window. The six per-capability counters it used\nto expose collapsed into ``credits``, and the two parallel log/history\nlists into one each, because there is now one pool to report.", "properties": {"credits": {"$ref": "#/components/schemas/BillingCreditsOut"}, "subscribed": {"title": "Subscribed", "type": "boolean"}, "comp": {"default": false, "title": "Comp", "type": "boolean"}, "tier": {"title": "Tier", "type": "string"}, "billing_interval": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Billing Interval"}, "subscription_status": {"title": "Subscription Status", "type": "string"}, "current_period_end": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Current Period End"}, "credit_logs": {"items": {"additionalProperties": true, "type": "object"}, "title": "Credit Logs", "type": "array"}, "spend_history": {"items": {"additionalProperties": true, "type": "object"}, "title": "Spend History", "type": "array"}}, "required": ["credits", "subscribed", "tier", "subscription_status", "credit_logs", "spend_history"], "title": "BillingOut", "type": "object"}, "VoteOut": {"description": "Response after voting on a claim.", "properties": {"votes": {"$ref": "#/components/schemas/VotesDict"}, "user_vote": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "User Vote"}}, "required": ["votes"], "title": "VoteOut", "type": "object"}, "VoteIn": {"description": "Input for casting a vote on a claim.", "properties": {"value": {"enum": ["true", "mostly_true", "mixed", "mostly_false", "false", "misleading"], "title": "Value", "type": "string"}}, "required": ["value"], "title": "VoteIn", "type": "object"}, "FeedbackCounts": {"description": "Aggregate counts per ClaimFeedback value.", "properties": {"yes": {"default": 0, "title": "Yes", "type": "integer"}, "no": {"default": 0, "title": "No", "type": "integer"}, "somewhat": {"default": 0, "title": "Somewhat", "type": "integer"}}, "title": "FeedbackCounts", "type": "object"}, "FeedbackOut": {"description": "Response after submitting feedback.", "properties": {"counts": {"$ref": "#/components/schemas/FeedbackCounts"}, "user_feedback": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "User Feedback"}}, "required": ["counts"], "title": "FeedbackOut", "type": "object"}, "FeedbackIn": {"description": "Input for casting feedback on a claim's analysis.", "properties": {"value": {"enum": ["yes", "no", "somewhat"], "title": "Value", "type": "string"}}, "required": ["value"], "title": "FeedbackIn", "type": "object"}, "FavoriteOut": {"description": "Response after toggling a favorite on a claim.", "properties": {"is_favorited": {"title": "Is Favorited", "type": "boolean"}}, "required": ["is_favorited"], "title": "FavoriteOut", "type": "object"}, "FavoritesOut": {"description": "Paginated favorites response.", "properties": {"items": {"items": {"$ref": "#/components/schemas/LibraryItemOut"}, "title": "Items", "type": "array"}, "total": {"title": "Total", "type": "integer"}, "page": {"title": "Page", "type": "integer"}, "page_size": {"title": "Page Size", "type": "integer"}}, "required": ["items", "total", "page", "page_size"], "title": "FavoritesOut", "type": "object"}, "LibraryItemOut": {"description": "Single item in the Library listing.", "properties": {"verification_id": {"title": "Verification Id", "type": "string"}, "slug": {"default": "", "title": "Slug", "type": "string"}, "url": {"default": "", "title": "Url", "type": "string"}, "atomic_claim": {"title": "Atomic Claim", "type": "string"}, "domain": {"title": "Domain", "type": "string"}, "domain_color": {"default": "", "title": "Domain Color", "type": "string"}, "entities": {"items": {"type": "string"}, "title": "Entities", "type": "array"}, "conclusion_label": {"title": "Conclusion Label", "type": "string"}, "lenz_score": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Lenz Score"}, "executive_summary": {"title": "Executive Summary", "type": "string"}, "votes": {"allOf": [{"$ref": "#/components/schemas/VotesDict"}], "default": {"true": 0, "mostly_true": 0, "mixed": 0, "mostly_false": 0, "false": 0}}, "user_vote": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "User Vote"}, "published_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Published At"}, "publisher_name": {"default": "", "title": "Publisher Name", "type": "string"}, "is_favorited": {"default": false, "title": "Is Favorited", "type": "boolean"}, "key_finding": {"default": "", "title": "Key Finding", "type": "string"}, "og_image_url": {"default": "", "title": "Og Image Url", "type": "string"}, "og_image_webp_url": {"default": "", "title": "Og Image Webp Url", "type": "string"}}, "required": ["verification_id", "atomic_claim", "domain", "entities", "conclusion_label", "executive_summary"], "title": "LibraryItemOut", "type": "object"}, "SuggestItemOut": {"properties": {"text": {"title": "Text", "type": "string"}, "type": {"title": "Type", "type": "string"}, "url": {"default": "", "title": "Url", "type": "string"}, "verification_id": {"default": "", "title": "Verification Id", "type": "string"}, "score": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Score"}, "count": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Count"}}, "required": ["text", "type"], "title": "SuggestItemOut", "type": "object"}, "SuggestOut": {"properties": {"suggestions": {"items": {"$ref": "#/components/schemas/SuggestItemOut"}, "title": "Suggestions", "type": "array"}}, "required": ["suggestions"], "title": "SuggestOut", "type": "object"}, "LibraryOut": {"description": "Paginated Library response.", "properties": {"items": {"items": {"$ref": "#/components/schemas/LibraryItemOut"}, "title": "Items", "type": "array"}, "total": {"title": "Total", "type": "integer"}, "page": {"title": "Page", "type": "integer"}, "page_size": {"title": "Page Size", "type": "integer"}, "related_entities": {"default": [], "items": {"$ref": "#/components/schemas/RelatedEntityOut"}, "title": "Related Entities", "type": "array"}}, "required": ["items", "total", "page", "page_size"], "title": "LibraryOut", "type": "object"}, "RelatedEntityOut": {"description": "Single entity in the 'related entities' strip on the entity-filtered library page.", "properties": {"name": {"title": "Name", "type": "string"}, "entity_type": {"default": "", "title": "Entity Type", "type": "string"}, "count": {"title": "Count", "type": "integer"}}, "required": ["name", "count"], "title": "RelatedEntityOut", "type": "object"}, "DomainOut": {"description": "Domain metadata for filters and UI pills.", "properties": {"name": {"title": "Name", "type": "string"}, "slug": {"title": "Slug", "type": "string"}, "color": {"title": "Color", "type": "string"}}, "required": ["name", "slug", "color"], "title": "DomainOut", "type": "object"}, "DomainsOut": {"description": "Available domains for navigation and filtering.", "properties": {"domains": {"items": {"$ref": "#/components/schemas/DomainOut"}, "title": "Domains", "type": "array"}}, "required": ["domains"], "title": "DomainsOut", "type": "object"}, "ChatMessageOut": {"description": "A single chat message.", "properties": {"role": {"title": "Role", "type": "string"}, "content": {"title": "Content", "type": "string"}, "created_at": {"title": "Created At", "type": "string"}, "is_system_seeded": {"default": false, "title": "Is System Seeded", "type": "boolean"}}, "required": ["role", "content", "created_at"], "title": "ChatMessageOut", "type": "object"}, "ChatOut": {"description": "Chat history + the caller's remaining follow-up capacity.\n\n``exchanges_used`` / ``exchange_limit`` are in follow-up questions,\nprojected from the caller's credit pool. ``extra_credits`` is the\nnon-expiring bucket, in the same unit (it was ``chat_credits`` when chat\nhad its own pool).", "properties": {"messages": {"items": {"$ref": "#/components/schemas/ChatMessageOut"}, "title": "Messages", "type": "array"}, "exchanges_used": {"title": "Exchanges Used", "type": "integer"}, "exchange_limit": {"title": "Exchange Limit", "type": "integer"}, "can_send": {"title": "Can Send", "type": "boolean"}, "extra_credits": {"default": 0, "title": "Extra Credits", "type": "integer"}, "limit_code": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Limit Code"}, "chat_starters": {"default": [], "items": {"type": "string"}, "title": "Chat Starters", "type": "array"}}, "required": ["messages", "exchanges_used", "exchange_limit", "can_send"], "title": "ChatOut", "type": "object"}, "ChatIn": {"description": "Input for sending a chat message.", "properties": {"message": {"title": "Message", "type": "string"}}, "required": ["message"], "title": "ChatIn", "type": "object"}, "ContactIn": {"description": "Input for the contact form.", "properties": {"email": {"title": "Email", "type": "string"}, "subject": {"title": "Subject", "type": "string"}, "description": {"title": "Description", "type": "string"}, "claim_verification_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Claim Verification Id"}, "attribution": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "title": "Attribution"}}, "required": ["email", "subject", "description"], "title": "ContactIn", "type": "object"}, "WaCtaOut": {"properties": {"url": {"title": "Url", "type": "string"}}, "required": ["url"], "title": "WaCtaOut", "type": "object"}, "WaCtaIn": {"properties": {"ga_client_id": {"default": "", "title": "Ga Client Id", "type": "string"}, "referrer": {"default": "", "title": "Referrer", "type": "string"}, "utm_source": {"default": "", "title": "Utm Source", "type": "string"}, "utm_medium": {"default": "", "title": "Utm Medium", "type": "string"}, "utm_campaign": {"default": "", "title": "Utm Campaign", "type": "string"}, "gclid": {"default": "", "title": "Gclid", "type": "string"}}, "title": "WaCtaIn", "type": "object"}, "WhatsAppLinkIn": {"properties": {"token": {"title": "Token", "type": "string"}}, "required": ["token"], "title": "WhatsAppLinkIn", "type": "object"}, "WorkbenchSearchOut": {"description": "Response for ``GET /api/workbench/search``.\n\nThe ``claims`` / ``entities`` / ``domains`` buckets carry SuggestItem-\nshaped dicts (``{text, type, url, verification_id?}``) \u2014 typed loosely as\n``dict`` so future SuggestItem field additions don't require a schema\nbump.", "properties": {"q": {"title": "Q", "type": "string"}, "scope": {"title": "Scope", "type": "string"}, "claims": {"default": [], "items": {"additionalProperties": true, "type": "object"}, "title": "Claims", "type": "array"}, "lists": {"default": [], "items": {"additionalProperties": true, "type": "object"}, "title": "Lists", "type": "array"}, "entities": {"default": [], "items": {"additionalProperties": true, "type": "object"}, "title": "Entities", "type": "array"}, "domains": {"default": [], "items": {"additionalProperties": true, "type": "object"}, "title": "Domains", "type": "array"}}, "required": ["q", "scope"], "title": "WorkbenchSearchOut", "type": "object"}, "WorkbenchLibraryOut": {"description": "Response for ``GET /api/workbench/library`` (tab + search modes).\n\nTab mode populates ``tab`` and leaves ``q`` / ``scope`` empty.\nSearch mode populates ``q`` / ``scope`` and leaves ``tab`` empty.\nBoth modes return entries in the same row shape, so the browse view\nrenders them with the same template.", "properties": {"items": {"items": {"additionalProperties": true, "type": "object"}, "title": "Items", "type": "array"}, "total": {"title": "Total", "type": "integer"}, "page": {"title": "Page", "type": "integer"}, "page_size": {"title": "Page Size", "type": "integer"}, "tab": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Tab"}, "q": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Q"}, "scope": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Scope"}}, "required": ["items", "total", "page", "page_size"], "title": "WorkbenchLibraryOut", "type": "object"}, "WorkbenchStateOut": {"description": "Response for ``GET /api/workbench/state``.\n\n``active`` carries the verdict-active payload (or interrupt / empty /\nloading shape) \u2014 see ``docs/workbench-verdict-contract.md`` for the\nfull key inventory. Typed as ``dict`` so the Tier 0 snapshot test\nremains the byte-shape source of truth.", "properties": {"active": {"additionalProperties": true, "title": "Active", "type": "object"}, "library": {"items": {"additionalProperties": true, "type": "object"}, "title": "Library", "type": "array"}, "library_counts": {"additionalProperties": true, "title": "Library Counts", "type": "object"}, "tab": {"title": "Tab", "type": "string"}, "areas_of_interest": {"items": {"additionalProperties": true, "type": "object"}, "title": "Areas Of Interest", "type": "array"}, "quotas": {"additionalProperties": true, "title": "Quotas", "type": "object"}}, "required": ["active", "library", "library_counts", "tab", "areas_of_interest", "quotas"], "title": "WorkbenchStateOut", "type": "object"}}, "securitySchemes": {"SessionAuth": {"type": "apiKey", "in": "cookie", "name": "__session"}, "SessionAuthAnonAllowed": {"type": "apiKey", "in": "cookie", "name": "__session"}}}, "servers": []}