{"openapi":"3.1.0","info":{"title":"PlanningLedger Report Web","description":"Web application for UK planning constraint reports.","version":"0.1.0"},"paths":{"/":{"get":{"tags":["pages"],"summary":"Landing page","description":"The homepage: ask the Reading Room a question (or join its waitlist),\nwith an example memo, dated proof figures and the free address check.\n\n``?q=`` pre-fills the question box (an example's \"Start from this question\"\nwithout JS) and ``?ex=`` names that example.","operationId":"landing_page__get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"ex","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ex"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/address/search-form":{"post":{"tags":["pages"],"summary":"HTML address search","description":"HTML endpoint for the landing page form POST.\n\nThe landing page form submits here (not to the JSON API endpoint at\n``POST /address/search``).  This endpoint calls the GeocodingService,\nthen renders address_confirm.html with the candidate list. Candidates\noutside Greater London are dropped — PlanningLedger currently covers the\n33 London boroughs only.\n\nIf the query is empty, re-renders the landing page with an error message.\nIf the geocoding service is unavailable, raises HTTP 503.\n\nArgs:\n    request: FastAPI request — used to read form data and access app.state.\n\nReturns:\n    HTML response: address_confirm.html on success, index.html on empty query.\n\nRaises:\n    HTTPException 503: When the geocoding service is not configured.","operationId":"address_search_page_address_search_form_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/snapshot/create-form":{"post":{"tags":["pages"],"summary":"HTML snapshot creation from address confirm form","description":"HTML form handler for creating a snapshot from the address confirm page.\n\nReads form-encoded data (application/x-www-form-urlencoded) posted by the\naddress_confirm.html template.  Calls SnapshotService, persists the result\nto the database, and redirects to the snapshot view page (POST-Redirect-GET).\n\nThis is the HTML counterpart of ``POST /snapshot`` (JSON API).  Both endpoints\nproduce the same result — this one accepts form data from browser forms, while\nthe JSON endpoint accepts application/json from programmatic clients.\n\nArgs:\n    request: FastAPI request — used to read form data and access app.state.\n\nReturns:\n    303 redirect to ``GET /snapshot/{snapshot_id}``.\n\nRaises:\n    HTTPException 400: When required address fields are missing or coordinates\n        cannot be parsed.\n    HTTPException 503: When snapshot service or database is not available.","operationId":"snapshot_create_form_snapshot_create_form_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/faq":{"get":{"tags":["pages"],"summary":"FAQ / Help page","description":"Render the FAQ / Help page.\n\nCovers common questions about the service, linked from every page\nvia the base.html footer.\n\nArgs:\n    request: FastAPI request — used to access app.state.templates.\n\nReturns:\n    HTML response with the FAQ page (faq.html).","operationId":"faq_page_faq_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy":{"get":{"tags":["pages"],"summary":"Privacy policy","description":"Render the privacy policy page.\n\nPlain-English summary of visitor/customer data handling plus the\nplanning-application data-minimisation policy (PRIVACY.md is the\ninternal compliance counterpart). Linked from the base.html footer.\n\nArgs:\n    request: FastAPI request — used to access app.state.templates.\n\nReturns:\n    HTML response with the privacy policy page (privacy.html).","operationId":"privacy_page_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy/professional-contacts":{"get":{"tags":["pages"],"summary":"Professional-contacts privacy notice","description":"Render the Art-14 privacy notice for the professional prospect database.\n\nThe transparency page for planning professionals whose public register\nactivity appears in the N5 prospect database — linked from every first-touch\noutreach message and from the main privacy policy (PRIVACY.md v2.6.0\n§Professional Prospect Database is the internal counterpart; the LIA lives\nin document_vault/n5/).\n\nArgs:\n    request: FastAPI request — used to access app.state.templates.\n\nReturns:\n    HTML response with the notice page (professional_contacts.html).","operationId":"professional_contacts_page_privacy_professional_contacts_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/terms":{"get":{"tags":["pages"],"summary":"Terms of Service","description":"Render the Terms of Service page.\n\nStatic legal page linked from the base.html footer. Draft terms compiled\nfrom the product and the site's existing disclaimers — see the DRAFT note\nin terms.html; not yet solicitor-reviewed.\n\nArgs:\n    request: FastAPI request — used to access app.state.templates.\n\nReturns:\n    HTML response with the Terms of Service page (terms.html).","operationId":"terms_page_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/report/{report_id}/action-gate":{"post":{"tags":["pages"],"summary":"Action gate email capture","description":"Handle the action gate form submission on shared report pages.\n\nCreates a user account (idempotent via AuthService.upsert_user), saves\nthe report to the user's saved_reports table (idempotent via PK constraint),\nstores marketing consent, and triggers a magic link email so the user can\naccess their portal.\n\nArgs:\n    report_id: UUID string of the shared report.\n    request: FastAPI request.\n\nReturns:\n    303 redirect to /report/{report_id}?saved=1.\n\nRaises:\n    HTTPException 400: Empty email.\n    HTTPException 503: DB or auth service unavailable.","operationId":"action_gate_submit_report__report_id__action_gate_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/portal":{"get":{"tags":["pages"],"summary":"My Reports portal","description":"Portal page — My Reports listing with search, download, and share.\n\nQueries all orders for the authenticated user (all statuses including\nin-progress), joins to reports table for status and asset keys.\n\nUnauthenticated visitors are redirected to ``/auth/login`` (rather than\ngetting a 401) so the header \"My Reports\" link leads to a sign-in flow.\n\nArgs:\n    request: FastAPI request (templates, session_maker, settings).\n\nReturns:\n    HTML response: portal.html (signed in) or a 303 redirect to the\n    sign-in page (signed out).","operationId":"portal_portal_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/boroughs":{"get":{"tags":["content"],"summary":"London borough index","operationId":"borough_index_boroughs_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/boroughs/{slug}":{"get":{"tags":["content"],"summary":"Borough planning hub","operationId":"borough_hub_boroughs__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/league-tables":{"get":{"tags":["content"],"summary":"London planning league tables index","operationId":"league_index_league_tables_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/league-tables/{metric}":{"get":{"tags":["content"],"summary":"London planning league table","operationId":"league_table_league_tables__metric__get","parameters":[{"name":"metric","in":"path","required":true,"schema":{"type":"string","title":"Metric"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/methodology":{"get":{"tags":["content"],"summary":"Methodology","operationId":"methodology_methodology_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/about":{"get":{"tags":["content"],"summary":"About PlanningLedger","operationId":"about_about_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/guides":{"get":{"tags":["content"],"summary":"Planning guides","operationId":"guide_index_guides_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/guides/{slug}":{"get":{"tags":["content"],"summary":"Planning guide","operationId":"guide_page_guides__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/updates/subscribe":{"post":{"tags":["content"],"summary":"Subscribe to updates (first-party email capture)","operationId":"subscribe_updates_subscribe_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/updates/unsubscribe":{"get":{"tags":["content"],"summary":"Unsubscribe confirmation page","description":"Render a confirm page — the actual unsubscribe happens on POST only,\nso mail-client link scanners that prefetch GETs cannot unsubscribe anyone.","operationId":"unsubscribe_confirm_updates_unsubscribe_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["content"],"summary":"Unsubscribe from updates","operationId":"unsubscribe_updates_unsubscribe_post","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health":{"get":{"tags":["health"],"summary":"Health check","description":"Return a simple health check response.\n\nReturns:\n    A dict with ``status`` (``\"ok\"``) and ``timestamp`` (ISO 8601 UTC).","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Health Get"}}}}}}},"/address/search":{"post":{"tags":["address"],"summary":"Search for UK addresses","description":"Resolve a free-text address, postcode, or UPRN to a list of candidates.\n\nReturns up to 10 geocoded candidate addresses.  Results are filtered to\nEngland and Wales only — Scottish and Northern Irish addresses return an\nempty candidates list.\n\nArgs:\n    body: JSON body with ``query`` field.\n    request: FastAPI request (used to access app.state.geocoding_service).\n\nReturns:\n    :class:`AddressSearchResponse` with ``candidates`` list.","operationId":"address_search_address_search_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/address/confirm":{"post":{"tags":["address"],"summary":"Confirm a selected address","description":"Confirm a selected address candidate and return full administrative context.\n\nValidates that the address is within England (country_code \"E\"); Wales is\nrefused until its data is covered (DE-0.2).  Calls the geocoding service to resolve LPA, ward, LSOA, and postcode.\n\nArgs:\n    body: JSON body containing the selected :class:`AddressConfirmRequest`.\n    request: FastAPI request (used to access app.state.geocoding_service).\n\nReturns:\n    :class:`AddressConfirmResponse` with merged address data and admin context.\n\nRaises:\n    HTTPException 400: If country_code is not \"E\".","operationId":"address_confirm_address_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressConfirmResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/snapshot":{"post":{"tags":["snapshot"],"summary":"Create Snapshot","description":"Create a planning snapshot for the given geocoded site.\n\nFans out MCP calls in parallel, classifies 12 constraint pills, persists\nthe result to the database, and returns the shareable URL.\n\nArgs:\n    body: Geocoded site data (address, coordinates, admin context).\n    request: FastAPI request — used to extract and hash the client IP.\n\nReturns:\n    A :class:`SnapshotCreateResponse` with snapshot_id and redirect_url.\n\nRaises:\n    HTTPException 503: When database is not available.","operationId":"create_snapshot_snapshot_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotCreateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/snapshot/{snapshot_id}":{"get":{"tags":["snapshot"],"summary":"Get Snapshot","description":"Return the stored SnapshotResult for the given snapshot UUID.\n\nFor browser requests (Accept: text/html), renders the snapshot.html template.\nFor API clients (Accept: application/json), returns JSON.\nNo authentication required — snapshots are public by design.\n\nArgs:\n    snapshot_id: UUID of the snapshot to retrieve (FastAPI validates format).\n    request: FastAPI request — used to access app.state and check Accept header.\n\nReturns:\n    HTML response (browser) or JSON dict (API clients).\n\nRaises:\n    HTTPException 404: When snapshot_id is not found in the database.\n    HTTPException 503: When the database is not available.","operationId":"get_snapshot_snapshot__snapshot_id__get","parameters":[{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Snapshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/snapshot/{snapshot_id}/email":{"post":{"tags":["snapshot"],"summary":"Capture Email","description":"Capture the user's email address for the 'Email me this' action gate.\n\nSaves the email to report_web.snapshots.user_email (nullable, opt-in only).\nOn success, redirects back to the snapshot page.\n\nNote: Per GDPR/PRIVACY.md, the email address is opt-in data for delivering\nthe snapshot link.  It must not be used for marketing without explicit\nconsent (enforced at application level, not this endpoint).\n\nArgs:\n    snapshot_id: UUID string of the snapshot.\n    request: FastAPI request — used to read form data and access session_maker.\n\nReturns:\n    Redirect to GET /snapshot/{snapshot_id} on success.\n\nRaises:\n    HTTPException 503: When the database is not available.\n    HTTPException 400: When no email address is provided.","operationId":"capture_email_snapshot__snapshot_id__email_post","parameters":[{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string","title":"Snapshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"get":{"tags":["auth"],"summary":"Sign-in page (request a magic link)","description":"Render the passwordless sign-in page.\n\nShows an email-entry form that POSTs to ``/auth/request``, which emails a\nmagic link. This is the entry point returning users reach from the header\n\"Sign in\" link and the ``/portal`` logged-out redirect.\n\nArgs:\n    request: FastAPI request (used to access ``app.state.templates``).\n\nReturns:\n    HTML response rendering ``auth/login.html``.","operationId":"login_page_auth_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Login Page Auth Login Get"}}}}}}},"/auth/request":{"post":{"tags":["auth"],"summary":"Request Magic Link","description":"Request a magic link sign-in email.\n\nUpserts the user (creates account on first request), generates a one-time\ntoken, and emails a magic link to the submitted address.\n\nArgs:\n    request: FastAPI request (used to access app state and rate limiter).\n    email:   Email address submitted via the HTML form.\n\nReturns:\n    HTML template confirming the email was sent.\n\nRaises:\n    HTTPException(422): If the email address format is invalid.\n    HTTPException(503): If the database or email service is unavailable.","operationId":"request_magic_link_auth_request_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_request_magic_link_auth_request_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Request Magic Link Auth Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/verify":{"get":{"tags":["auth"],"summary":"Verify Magic Link","description":"Verify a magic link token and create an authenticated session.\n\nAtomically marks the token as used (via UPDATE...WHERE...RETURNING) and\nsets an HttpOnly, signed session cookie.  Redirects to /portal on success.\nRenders link_expired.html if the token is invalid, expired, or already used.\n\nArgs:\n    request: FastAPI request (used to access app state).\n    token:   Plaintext token from the magic link URL query parameter.\n\nReturns:\n    303 redirect to /portal on success, or link_expired.html page on failure.","operationId":"verify_magic_link_auth_verify_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Verify Magic Link Auth Verify Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Log out the current user by clearing the session cookie.\n\nArgs:\n    request: FastAPI request.\n\nReturns:\n    303 redirect to the landing page (/).","operationId":"logout_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Logout Auth Logout Post"}}}}}}},"/payment/checkout":{"post":{"tags":["payments"],"summary":"Create Stripe Checkout session","description":"Create a Stripe Checkout session and redirect the user to Stripe.\n\nWorks for anonymous buyers coming from a snapshot — the funnel — by taking\n``user_email`` from the form; an authenticated session is NOT required.  If\nno email is supplied, it is resolved from the session when present.\n\nWorkflow:\n1. Resolve user email from session (or form if provided).\n2. Upsert user by email.\n3. Create a pending order in the database (product_tier + amount_pence set\n   from ``tier``).\n4. Create a Stripe Checkout session (£45 Standard or £90 Pro).\n5. Store the stripe_session_id on the order.\n6. Redirect (303) to the Stripe Checkout URL.\n\nArgs:\n    request:              FastAPI request — used to access ``app.state``.\n    snapshot_id:          Snapshot the user is upgrading from.\n    user_email:           Buyer's email address (optional — derived from session).\n    site_address:         Human-readable site address.\n    site_latitude:        Optional latitude coordinate.\n    site_longitude:       Optional longitude coordinate.\n    site_uprn:            Optional UPRN identifier.\n    user_planning_notes:  Optional free-text notes from the user.\n    tier:                 \"standard\" (£45, default) or \"pro\" (£90). Any other\n                          value is rejected with a 422 by FastAPI's Form validation.\n    user_id:              Authenticated user ID from session cookie (injected by Depends).\n\nReturns:\n    303 redirect to the Stripe Checkout URL.\n\nRaises:\n    HTTPException 401: If no valid session cookie is present.\n    HTTPException 503: If payment service or database is not configured.","operationId":"create_checkout_payment_checkout_post","parameters":[{"name":"session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_create_checkout_payment_checkout_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment/bypass":{"post":{"tags":["payments"],"summary":"Dev/eval bypass — skip Stripe and enqueue report directly","description":"Dev/eval payment bypass — creates order (status=authorized), inserts reports row,\nenqueues ARQ jobs, and redirects to /report/{report_id}.\n\nGuard: returns 404 when REPORT_WEB_PAYMENT_BYPASS is not true.\n\nThis endpoint replicates the post-payment flow exactly (order insert,\nreports insert, ARQ enqueue) without any Stripe interaction.\nMUST NEVER be enabled in production.\n\nArgs:\n    request:             FastAPI request — used to access ``app.state``.\n    snapshot_id:         Snapshot the user is upgrading from.\n    site_address:        Human-readable site address.\n    user_email:          Optional email address; falls back to dev-bypass@planningledger.com.\n    site_latitude:       Optional latitude coordinate.\n    site_longitude:      Optional longitude coordinate.\n    site_uprn:           Optional UPRN identifier.\n    user_planning_notes: Optional free-text notes from the user.\n    tier:                \"standard\" (default) or \"pro\" — sets product_tier + amount_pence\n                          identically to the paid checkout path.\nReturns:\n    303 redirect to /report/{report_id}.\n\nRaises:\n    HTTPException 404: When payment_bypass_enabled is False (default).\n    HTTPException 503: When database is not available.","operationId":"bypass_payment_payment_bypass_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_bypass_payment_payment_bypass_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhook/stripe":{"post":{"tags":["payments"],"summary":"Handle Stripe webhook events","description":"Handle Stripe webhook events.\n\nCRITICAL: ``await request.body()`` must be called first — before any JSON\nparsing — because Stripe signature verification operates on the raw request\nbody bytes.  Parsing first breaks the HMAC computation.\n\nReturns 200 even for already-processed events (idempotent) to prevent Stripe\nfrom retrying deliveries.\n\nHandled events:\n- ``checkout.session.completed``: Fulfil order, link Stripe Customer, send email.\n\nAll other event types are acknowledged with ``{\"status\": \"ignored\"}``.\n\nArgs:\n    request: FastAPI request — raw body read via ``await request.body()``.\n\nReturns:\n    JSON response: ``{\"status\": \"ok\" | \"already_processed\" | \"ignored\"}``.\n\nRaises:\n    HTTPException 400: If the webhook signature is invalid.","operationId":"stripe_webhook_webhook_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/payment/success":{"get":{"tags":["payments"],"summary":"Payment success page","description":"Render the payment success page after Stripe Checkout redirect.\n\nLooks up the order by ``stripe_session_id`` to display site address and\nbuyer email.  Returns 404 if the order is not found.\n\nA LEFT JOIN to ``report_web.reports`` is included so that ``report_id``\nis available in the template when the reports row already exists (fast\nwebhook processing).  When the reports row has not yet been created\n(Stripe redirect arrives before the webhook — a race condition),\n``report_id`` is ``None`` and the template conditional omits the link.\n\nArgs:\n    request:    FastAPI request — used to access ``app.state.templates``.\n    session_id: Stripe Checkout session ID from the ``?session_id=`` query param.\n\nReturns:\n    HTML response: ``payment_success.html`` with order context including\n    ``report_id`` (str or None).\n\nRaises:\n    HTTPException 404: If no order matches the session ID.","operationId":"payment_success_payment_success_get","parameters":[{"name":"session_id","in":"query","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/pdf":{"get":{"tags":["report"],"summary":"Download report PDF","description":"Serve the report PDF to its owner or anyone who saved it.\n\nRequires authentication and verifies the requesting user either **owns** the\nreport (via ``orders``) or has **saved** it to their portal (via\n``saved_reports``, i.e. the \"Save & Get PDF\" action gate). This matches the\n\"My Reports\" listing, which shows owned *and* saved reports — so the\n\"Download PDF\" button there works for both. It still blocks a user with no\nrelationship to the report.\n\nServing depends on the configured asset store:\n- filesystem/NFS (default): streams the file via ``FileResponse``.\n- S3 (``REPORTS_S3_BUCKET`` set): 302-redirects to a short-lived presigned URL.\n\nArgs:\n    report_id: UUID string of the report.\n    request: FastAPI request.\n    user_id: Authenticated user ID.\n\nReturns:\n    ``FileResponse`` (filesystem backend) or 302 ``RedirectResponse`` (S3).\n\nRaises:\n    HTTPException 404: Report not found, unowned, or PDF asset missing.\n    HTTPException 503: Database or asset store not available.","operationId":"download_report_pdf_report__report_id__pdf_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/asset/{asset_name}":{"get":{"tags":["report"],"summary":"Serve a report image asset","description":"Serve a report image asset (map / chart / OG image) by report link.\n\nThese assets are embedded in the shareable report page (which is viewable by\nnon-owners) and the OG image must be reachable by unauthenticated social\ncrawlers, so this route is **not** ownership-gated — the unguessable report\nUUID is the capability, exactly as for ``GET /report/{id}`` itself. The PDF\n(a downloadable deliverable) is served separately and stays ownership-checked.\n\n``asset_name`` is strictly allow-listed to image assets (``og_image``,\n``map_*``, ``chart_*``); the PDF/HTML keys can never be reached here.\n\nArgs:\n    report_id: UUID string of the report.\n    asset_name: Logical asset name (e.g. ``chart_epc_rating``, ``og_image``).\n    request: FastAPI request.\n\nReturns:\n    ``FileResponse`` (filesystem backend) or 302 ``RedirectResponse`` (S3).\n\nRaises:\n    HTTPException 404: Invalid id/name, or the asset does not exist.\n    HTTPException 503: Asset store not available.","operationId":"get_report_asset_report__report_id__asset__asset_name__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"asset_name","in":"path","required":true,"schema":{"type":"string","title":"Asset Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/share-copied":{"post":{"tags":["report"],"summary":"Record share link copy","description":"Record that a user copied the share link for a report.\n\nCalled by the copyReportLink() JS function after clipboard write succeeds.\nRecords a link_view row with share_copied=TRUE for funnel tracking (FNLR-02).\n\nArgs:\n    report_id: UUID string of the report.\n    request: FastAPI request.\n\nReturns:\n    JSON {\"ok\": true}.","operationId":"record_share_copied_report__report_id__share_copied_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/status":{"get":{"tags":["report"],"summary":"Polling endpoint for report status","description":"Return order status for JS polling.\n\nUsed by the placeholder page to detect when the report is ready.\n\nRPRT-08: JOINs report_web.orders to detect sla_breached override.\nThe SLA watcher updates orders.status='sla_breached' but never reports.status,\nso a CASE expression is used to surface the breach to the polling JS.\n\nArgs:\n    report_id: UUID string of the report.\n    request:   FastAPI request — used to access ``app.state``.\n\nReturns:\n    JSON ``{\"status\": \"authorized\" | \"paid\" | \"sla_breached\" | \"delivered_free\" | ...}``\n    Returns ``\"sla_breached\"`` when orders.status='sla_breached' regardless of reports.status.\n\nRaises:\n    HTTPException 404: If report_id does not exist.\n    HTTPException 503: If database is not configured.","operationId":"get_report_status_report__report_id__status_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}":{"get":{"tags":["report"],"summary":"Report page (placeholder or full report)","description":"Render the placeholder or full report page.\n\n- In-progress orders (queued, researching, writing, sla_breached):\n  render ``report_placeholder.html`` with polling JS.\n- Complete orders (paid, delivered_free, authorized) with ``content_json`` set:\n  render ``report_view.html`` with full 12-section content, presigned S3 URLs,\n  OG meta tags, panzoom.js, and link_view tracking (fire-and-forget).\n\nArgs:\n    report_id: UUID string of the report.\n    request:   FastAPI request — used to access ``app.state``.\n\nReturns:\n    HTML response: ``report_view.html`` (complete) or ``report_placeholder.html`` (in-progress).\n\nRaises:\n    HTTPException 404: If report_id does not exist.\n    HTTPException 503: If database is not configured.","operationId":"get_report_report__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhook/letter-optout":{"post":{"tags":["refusal-letters"],"summary":"Postmark inbound: opt-outs emailed to stop@ (shared-secret auth)","operationId":"letter_optout_webhook_webhook_letter_optout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/letter":{"get":{"tags":["refusal-letters"],"summary":"Code Page","description":"The box, with the QR code's code filled in. Records nothing.","operationId":"code_page_letter_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["refusal-letters"],"summary":"Code Submit","operationId":"code_submit_letter_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/letter/report/{code}":{"get":{"tags":["refusal-letters"],"summary":"Report Page","operationId":"report_page_letter_report__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/letter/report/{code}/register":{"post":{"tags":["refusal-letters"],"summary":"Register","operationId":"register_letter_report__code__register_post","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stop":{"get":{"tags":["refusal-letters"],"summary":"Stop Page","operationId":"stop_page_stop_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["refusal-letters"],"summary":"Stop Submit","operationId":"stop_submit_stop_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/letter/stop":{"get":{"tags":["refusal-letters"],"summary":"Stop Page","operationId":"stop_page_letter_stop_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["refusal-letters"],"summary":"Stop Submit","operationId":"stop_submit_letter_stop_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/email/confirm/{token}":{"get":{"tags":["audience"],"summary":"Double-opt-in confirmation step (GET never mutates)","operationId":"confirm_page_email_confirm__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["audience"],"summary":"Activate a pending subscription","operationId":"confirm_subscription_email_confirm__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/email/unsubscribe/{token}":{"get":{"tags":["audience"],"summary":"Unsubscribe confirmation step (GET never mutates)","operationId":"unsubscribe_page_email_unsubscribe__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"list","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["audience"],"summary":"Unsubscribe (per-list or global), effective immediately","operationId":"unsubscribe_email_unsubscribe__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/email/one-click/{token}":{"post":{"tags":["audience"],"summary":"RFC 8058 List-Unsubscribe-Post target","description":"Acts immediately — no confirmation page, 200 plain (AEM-24). Tolerates\n(and ignores) the RFC 8058 ``List-Unsubscribe=One-Click`` form body. A\ntoken scoped to a list leaves that list; a recipient-wide token is a\nglobal unsubscribe.","operationId":"one_click_unsubscribe_email_one_click__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/email/preferences/{token}":{"get":{"tags":["audience"],"summary":"Preference centre — all list memberships, no login","operationId":"preferences_page_email_preferences__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["audience"],"summary":"Apply preference-centre toggles, effective immediately","operationId":"update_preferences_email_preferences__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/newsletter":{"get":{"tags":["audience"],"summary":"Newsletter archive — published issues index","operationId":"newsletter_index_newsletter_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/newsletter/{week_start}":{"get":{"tags":["audience"],"summary":"One published newsletter issue","operationId":"newsletter_issue_newsletter__week_start__get","parameters":[{"name":"week_start","in":"path","required":true,"schema":{"type":"string","title":"Week Start"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhook/email-events":{"post":{"tags":["audience"],"summary":"SMTP2GO delivery-event webhook (shared-secret auth)","description":"Ingest transport delivery events (delivered, bounce, complaint,\nunsubscribe). Authenticated by the AUDIENCE_WEBHOOK_SECRET shared secret —\nheader ``X-Audience-Webhook-Secret`` or ``?token=`` query — and rejected\noutright when no secret is configured. Events are handed to\n``audience_email.delivery_events.ingest_delivery_events(store, events)``\n(``app.state.audience_ingest_events`` overrides — the test seam).","operationId":"email_events_webhook_webhook_email_events_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/reading-room/":{"get":{"tags":["reading-room"],"summary":"Landing","operationId":"landing_reading_room__get","parameters":[{"name":"invite","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invite"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"ex","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ex"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/apply":{"post":{"tags":["reading-room"],"summary":"Apply","description":"The waitlist (door off or full) and the pre-open-door application. Only\nthe email address and consent are required (owner ruling 2026-09-30: no\n\"built for professionals\" gate, no required practice): name, organisation\nand what they would ask are optional, and the role question is gone (an\nold form that still sends one is kept). The owner still admits each\nwaitlisted reader by hand.","operationId":"apply_reading_room_apply_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_apply_reading_room_apply_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/decide/{token}":{"get":{"tags":["reading-room"],"summary":"Decide Page","operationId":"decide_page_reading_room_decide__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Decide","operationId":"decide_reading_room_decide__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_decide_reading_room_decide__token__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/auth/login":{"get":{"tags":["reading-room"],"summary":"Login Page","operationId":"login_page_reading_room_auth_login_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/reading-room/auth/request":{"post":{"tags":["reading-room"],"summary":"Request Link","operationId":"request_link_reading_room_auth_request_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_request_link_reading_room_auth_request_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/auth/verify":{"get":{"tags":["reading-room"],"summary":"Verify","operationId":"verify_reading_room_auth_verify_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Verify Post","operationId":"verify_post_reading_room_auth_verify_post","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_verify_post_reading_room_auth_verify_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/start":{"post":{"tags":["reading-room"],"summary":"Start","operationId":"start_reading_room_start_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/reading-room/resend":{"post":{"tags":["reading-room"],"summary":"Resend","description":"An unconfirmed reader's first-answer email, again: a fresh claim link,\nnever the typed question, once the first one has gone, at most once per\nRESEND_AFTER (the question's notified_at) and three times a day per address.\nA confirmed reader is pointed at sign-in by the page, which has its own\nroute; nothing is sent to them here. The same page whatever happened, and\nthe send runs after the response, so neither the page nor its timing says\nwhether an address is known.","operationId":"resend_reading_room_resend_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_resend_reading_room_resend_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/claim":{"get":{"tags":["reading-room"],"summary":"Claim Page","description":"The answer email's button lands here. A GET changes nothing (mail\nscanners fetch links); the page's own button POSTs. A browser already\nsigned in as the thread's owner goes straight to the answer.","operationId":"claim_page_reading_room_claim_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}},{"name":"t","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"T"}},{"name":"f","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"F"}},{"name":"r","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"R"}},{"name":"l","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"L"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Claim","operationId":"claim_reading_room_claim_post","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_claim_reading_room_claim_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/unsubscribe/{token}":{"get":{"tags":["reading-room"],"summary":"Unsubscribe Page","description":"A bare GET changes nothing (mail scanners fetch links): the page's\nbutton POSTs. An unknown token and a spent one show the same page.","operationId":"unsubscribe_page_reading_room_unsubscribe__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Unsubscribe","description":"Withdraw marketing consent: the page's button, and the RFC 8058\none-click POST (List-Unsubscribe-Post) alike. Immediate; the address is\nerased from the consent record. The reader's questions are untouched.","operationId":"unsubscribe_reading_room_unsubscribe__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/about":{"post":{"tags":["reading-room"],"summary":"About","description":"The optional questions (growth plan §4.4), asked while the first answer\nis being written (signed token from the started page, for the reader that\nrequest created) or later from home (session). Everything is optional; a\ntoken for anyone else, or a forged one, changes nothing and shows the same\npage.","operationId":"about_reading_room_about_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_about_reading_room_about_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/examples/{slug}":{"get":{"tags":["reading-room"],"summary":"Example Page","description":"A full example answer, read live from the API. Only for entries the\nowner has marked full_page=True after the name-removal read (growth plan\n§4.6); every other slug is a 404. Kept out of search (noindex).","operationId":"example_page_reading_room_examples__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/auth/logout":{"post":{"tags":["reading-room"],"summary":"Logout","operationId":"logout_reading_room_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/reading-room/home":{"get":{"tags":["reading-room"],"summary":"Home","operationId":"home_reading_room_home_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/reading-room/home/emails":{"post":{"tags":["reading-room"],"summary":"Home Emails","description":"Turn the emails about PlanningLedger on or off (PRIVACY.md v2.13.1).\nThe reader is signed in, so the address is already proven: \"on\" records a\nconsent and confirms it at once (the box's own wording and version);\n\"off\" withdraws it as the unsubscribe link does. Explicit values, not a\nflip, so a double press cannot undo itself. Nothing here sends an email.","operationId":"home_emails_reading_room_home_emails_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_home_emails_reading_room_home_emails_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/ask":{"post":{"tags":["reading-room"],"summary":"Ask","operationId":"ask_reading_room_ask_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_ask_reading_room_ask_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/t/{thread_id}":{"get":{"tags":["reading-room"],"summary":"Thread Page","operationId":"thread_page_reading_room_t__thread_id__get","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"string","title":"Thread Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/q/{question_id}/status":{"get":{"tags":["reading-room"],"summary":"Question Status","operationId":"question_status_reading_room_q__question_id__status_get","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","title":"Question Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/q/{question_id}/rate":{"post":{"tags":["reading-room"],"summary":"Rate","description":"'Did it answer your question?' Yes / Partly / No (instrument v0). A\npage still showing the earlier bad / ok / good buttons keeps working.","operationId":"rate_reading_room_q__question_id__rate_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","title":"Question Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_rate_reading_room_q__question_id__rate_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/r/{token}":{"get":{"tags":["reading-room"],"summary":"Ready Page","operationId":"ready_page_reading_room_r__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/r/{token}/claim":{"post":{"tags":["reading-room"],"summary":"Ready Claim","description":"Take an address for a ready answer and send it a confirmation link (see\n_send_ready_claim). The same page every time; the answer moves only when\nthe link is used, by the account it was sent to.","operationId":"ready_claim_reading_room_r__token__claim_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_ready_claim_reading_room_r__token__claim_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/q/{question_id}/more":{"post":{"tags":["reading-room"],"summary":"More","description":"'What will you do with this answer?' and 'Without this, what would you\nhave done?' (DE-1.7). Not there at all until v2.10.0 is in force.","operationId":"more_reading_room_q__question_id__more_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","title":"Question Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_more_reading_room_q__question_id__more_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/v/{token}":{"get":{"tags":["reading-room"],"summary":"Verdict Confirm","operationId":"verdict_confirm_reading_room_v__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Verdict Record","operationId":"verdict_record_reading_room_v__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/f/{token}":{"get":{"tags":["reading-room"],"summary":"Owner Flag Confirm","operationId":"owner_flag_confirm_reading_room_f__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Owner Flag Record","operationId":"owner_flag_record_reading_room_f__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/code/{token}":{"get":{"tags":["reading-room"],"summary":"Owner Code Form","operationId":"owner_code_form_reading_room_code__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Owner Code Save","operationId":"owner_code_save_reading_room_code__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/q/{question_id}/outcome":{"post":{"tags":["reading-room"],"summary":"Outcome Opt In","description":"'May we ask in a week how it went?' — one email, seven days on (privacy v2.10.0).","operationId":"outcome_opt_in_reading_room_q__question_id__outcome_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","title":"Question Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/q/{question_id}/outcome/withdraw":{"post":{"tags":["reading-room"],"summary":"Outcome Withdraw","description":"Take back the opt-in: as easy as giving it (PECR).","operationId":"outcome_withdraw_reading_room_q__question_id__outcome_withdraw_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","title":"Question Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/o/{token}":{"get":{"tags":["reading-room"],"summary":"Outcome Confirm","operationId":"outcome_confirm_reading_room_o__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["reading-room"],"summary":"Outcome Record","operationId":"outcome_record_reading_room_o__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/letter/{code}":{"get":{"tags":["reading-room"],"summary":"Letter Scenarios","operationId":"letter_scenarios_reading_room_letter__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/letter/{code}/call":{"post":{"tags":["reading-room"],"summary":"Letter Call","description":"Take up the invitation to talk: record_call 'booked' on the letter (the letter's\ncall_booked rides on it), and the owner is told who and when, to send the invite. Only for\na reader over the usage bar (book_call asks get_letter_call_offer again); \"the firm books;\nwe never ring\" (CAP plan): the reader picks the slot.","operationId":"letter_call_reading_room_letter__code__call_post","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_letter_call_reading_room_letter__code__call_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reading-room/letter/{code}/future":{"post":{"tags":["reading-room"],"summary":"Letter Future","description":"Turn \"send me analysis of my future refusals\" on or off. The reader is signed in,\nso the address is already proven: on counts at once; off withdraws it.","operationId":"letter_future_reading_room_letter__code__future_post","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_letter_future_reading_room_letter__code__future_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddressConfirmRequest":{"properties":{"address":{"type":"string","title":"Address","description":"Human-readable address string"},"lat":{"type":"number","title":"Lat","description":"Latitude (WGS84)"},"lon":{"type":"number","title":"Lon","description":"Longitude (WGS84)"},"uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uprn","description":"UPRN if available"},"postcode":{"type":"string","title":"Postcode","description":"UK postcode"},"ward_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ward Code","description":"ONS ward code"},"local_custodian_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Local Custodian Code","description":"OS local custodian code"},"local_custodian_code_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Local Custodian Code Description","description":"LPA name from OS Places"},"country_code":{"type":"string","title":"Country Code","description":"OS country code (E or W)"}},"type":"object","required":["address","lat","lon","postcode","country_code"],"title":"AddressConfirmRequest","description":"Request body for POST /address/confirm.\n\nThe client sends back one of the candidates returned by /address/search."},"AddressConfirmResponse":{"properties":{"address":{"type":"string","title":"Address"},"lat":{"type":"number","title":"Lat"},"lon":{"type":"number","title":"Lon"},"uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uprn"},"postcode":{"type":"string","title":"Postcode"},"lpa_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lpa Code"},"lpa_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lpa Name"},"ward":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ward"},"lsoa":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lsoa"}},"type":"object","required":["address","lat","lon","uprn","postcode","lpa_code","lpa_name","ward","lsoa"],"title":"AddressConfirmResponse","description":"Response body for POST /address/confirm."},"AddressSearchRequest":{"properties":{"query":{"type":"string","maxLength":500,"minLength":1,"title":"Query","description":"Address, postcode, or UPRN"}},"type":"object","required":["query"],"title":"AddressSearchRequest","description":"Request body for POST /address/search."},"AddressSearchResponse":{"properties":{"candidates":{"items":{"$ref":"#/components/schemas/GeocodedAddress"},"type":"array","title":"Candidates"}},"type":"object","required":["candidates"],"title":"AddressSearchResponse","description":"Response body for POST /address/search."},"Body_about_reading_room_about_post":{"properties":{"token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organisation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organisation"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"heard_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Heard From"},"call_ok":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Ok"}},"type":"object","title":"Body_about_reading_room_about_post"},"Body_apply_reading_room_apply_post":{"properties":{"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"practice":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Practice"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"consent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent"},"invite":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invite"}},"type":"object","required":["email"],"title":"Body_apply_reading_room_apply_post"},"Body_ask_reading_room_ask_post":{"properties":{"question":{"type":"string","title":"Question"},"thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Id"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site"},"letter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Letter"}},"type":"object","required":["question"],"title":"Body_ask_reading_room_ask_post"},"Body_bypass_payment_payment_bypass_post":{"properties":{"snapshot_id":{"type":"string","title":"Snapshot Id"},"site_address":{"type":"string","title":"Site Address"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email"},"site_latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Site Latitude"},"site_longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Site Longitude"},"site_uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Uprn"},"user_planning_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Planning Notes"},"tier":{"type":"string","enum":["standard","pro"],"title":"Tier","default":"standard"}},"type":"object","required":["snapshot_id","site_address"],"title":"Body_bypass_payment_payment_bypass_post"},"Body_claim_reading_room_claim_post":{"properties":{"token":{"type":"string","title":"Token"},"t":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"T"},"r":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"R"},"l":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"L"}},"type":"object","required":["token"],"title":"Body_claim_reading_room_claim_post"},"Body_create_checkout_payment_checkout_post":{"properties":{"snapshot_id":{"type":"string","title":"Snapshot Id"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email"},"site_address":{"type":"string","title":"Site Address"},"site_latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Site Latitude"},"site_longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Site Longitude"},"site_uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Uprn"},"user_planning_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Planning Notes"},"tier":{"type":"string","enum":["standard","pro"],"title":"Tier","default":"standard"}},"type":"object","required":["snapshot_id","site_address"],"title":"Body_create_checkout_payment_checkout_post"},"Body_decide_reading_room_decide__token__post":{"properties":{"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","title":"Body_decide_reading_room_decide__token__post"},"Body_home_emails_reading_room_home_emails_post":{"properties":{"want":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Want"}},"type":"object","title":"Body_home_emails_reading_room_home_emails_post"},"Body_letter_call_reading_room_letter__code__call_post":{"properties":{"slot":{"type":"string","title":"Slot","default":""}},"type":"object","title":"Body_letter_call_reading_room_letter__code__call_post"},"Body_letter_future_reading_room_letter__code__future_post":{"properties":{"want":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Want"}},"type":"object","title":"Body_letter_future_reading_room_letter__code__future_post"},"Body_more_reading_room_q__question_id__more_post":{"properties":{"intended_use":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Intended Use"},"counterfactual":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Counterfactual"}},"type":"object","title":"Body_more_reading_room_q__question_id__more_post"},"Body_rate_reading_room_q__question_id__rate_post":{"properties":{"verdict":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"},"rating":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rating"},"comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment"}},"type":"object","title":"Body_rate_reading_room_q__question_id__rate_post"},"Body_ready_claim_reading_room_r__token__claim_post":{"properties":{"email":{"type":"string","title":"Email","default":""},"consent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent"}},"type":"object","title":"Body_ready_claim_reading_room_r__token__claim_post"},"Body_request_link_reading_room_auth_request_post":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"Body_request_link_reading_room_auth_request_post"},"Body_request_magic_link_auth_request_post":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"Body_request_magic_link_auth_request_post"},"Body_resend_reading_room_resend_post":{"properties":{"email":{"type":"string","title":"Email","default":""}},"type":"object","title":"Body_resend_reading_room_resend_post"},"Body_verify_post_reading_room_auth_verify_post":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"Body_verify_post_reading_room_auth_verify_post"},"GeocodedAddress":{"properties":{"address":{"type":"string","title":"Address","description":"Human-readable address string"},"lat":{"type":"number","title":"Lat","description":"Latitude (WGS84)"},"lon":{"type":"number","title":"Lon","description":"Longitude (WGS84)"},"uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uprn","description":"Unique Property Reference Number"},"postcode":{"type":"string","title":"Postcode","description":"UK postcode"},"ward_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ward Code","description":"ONS ward code"},"ward_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ward Name","description":"Human-readable ward name (or raw code as fallback)"},"lpa_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lpa Code","description":"GSS code for the Local Planning Authority, resolved via IMD lookup (resolve_admin_context) — NOT the OS local_custodian_code, which is a different code space and does not key the policy corpus"},"local_custodian_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Local Custodian Code","description":"OS local custodian code (LPA identifier)"},"local_custodian_code_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Local Custodian Code Description","description":"Human-readable LPA name from OS Places"},"country_code":{"type":"string","title":"Country Code","description":"OS country code: E=England, W=Wales"},"lsoa":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lsoa","description":"LSOA code, resolved via IMD lookup"},"is_approximate":{"type":"boolean","title":"Is Approximate","description":"True when this is a postcode-area centroid with no matched building (Code-Point DB last resort), not a specific street address","default":false}},"type":"object","required":["address","lat","lon","postcode","country_code"],"title":"GeocodedAddress","description":"A geocoded UK address with administrative context."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"SnapshotCreateRequest":{"properties":{"address":{"type":"string","title":"Address"},"lat":{"type":"number","title":"Lat"},"lon":{"type":"number","title":"Lon"},"uprn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uprn"},"postcode":{"type":"string","title":"Postcode"},"lpa_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lpa Code"},"lpa_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lpa Name"},"ward":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ward"},"lsoa":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lsoa"}},"type":"object","required":["address","lat","lon","postcode"],"title":"SnapshotCreateRequest","description":"Geocoded site data required to generate a planning snapshot."},"SnapshotCreateResponse":{"properties":{"snapshot_id":{"type":"string","title":"Snapshot Id"},"redirect_url":{"type":"string","title":"Redirect Url"}},"type":"object","required":["snapshot_id","redirect_url"],"title":"SnapshotCreateResponse","description":"Response returned after creating a snapshot."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}