{"openapi":"3.1.0","info":{"title":"VNPulse","version":"0.1.0"},"paths":{"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/ready":{"get":{"summary":"Health Ready","description":"Is this process ready to take the box's traffic? For the blue-green deploy\n(scripts/bluegreen.sh), which starts a new release on its own port and moves nginx\nover only when this says `ready`: the commit it runs, the startup warm finished (so\nthe first users do not pay for cold caches), the database answering, and the stock\nuniverse in memory. Loopback only, like /health/turns.","operationId":"health_ready_health_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/stream":{"get":{"summary":"Health Stream","description":"A long-lived text/event-stream that sends a `tick` every second for `seconds`\n(at most 300). It stands in for a copilot answer when proving a deploy cuts no\nstream (scripts/deploy_probe.py): free, no account, no model. Capped at STREAM_MAX\nat once so it cannot be used to pin the box's connections.","operationId":"health_stream_health_stream_get","parameters":[{"name":"seconds","in":"query","required":false,"schema":{"type":"integer","default":60,"title":"Seconds"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health/turns":{"get":{"summary":"Health Turns","description":"How much running work a restart would kill right now. For the deploy's drain\n(scripts/restart_app.sh), which waits for `busy` to reach 0 before restarting.\n\nLoopback only, and only when no proxy forwarded the request: nginx on the box also\nconnects from 127.0.0.1 but always adds X-Real-IP, CloudFront adds X-Forwarded-For.\nIt carries counts and nothing else, so even a leak would say nothing about anyone.","operationId":"health_turns_health_turns_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/logos":{"get":{"summary":"Logo Manifest","operationId":"logo_manifest_api_logos_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/signin":{"get":{"summary":"Signin Page","operationId":"signin_page_signin_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Signin Post","operationId":"signin_post_signin_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_signin_post_signin_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signup":{"get":{"summary":"Signup Page","operationId":"signup_page_signup_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Signup Post","operationId":"signup_post_signup_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_signup_post_signup_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify-pending":{"get":{"summary":"Verify Pending","operationId":"verify_pending_verify_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/verify-code":{"post":{"summary":"Verify Code Post","description":"The website's half of the 6-digit code: verified -> signed in -> the copilot.","operationId":"verify_code_post_verify_code_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_verify_code_post_verify_code_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify-email":{"get":{"summary":"Verify Email","operationId":"verify_email_verify_email_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/resend-verification":{"get":{"summary":"Resend Verification","operationId":"resend_verification_resend_verification_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/account/change-password":{"post":{"summary":"Api Change Password","description":"Change the caller's password; every OTHER session and app token is signed out.\n\nThe credential that made the change stays signed in: the browser session is\nre-stamped (and its remember-me cookie re-issued), and an app caller gets a\nfresh bearer `token` in the response to replace the one that just stopped\nworking. Five attempts an hour, failures included; a refusal says when to retry.","operationId":"api_change_password_api_account_change_password_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/account/delete":{"post":{"summary":"Api Delete Account","description":"Close the caller's account, after they re-enter their password.\n\nBody: {\"password\": \"...\"}. Deletes or anonymises everything tied to the account\n(services/account_deletion.py says exactly what, and why payments are kept but\nunlinked), which also kills every session and app token. Irreversible.","operationId":"api_delete_account_api_account_delete_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/account/delete":{"get":{"summary":"Account Delete Page","description":"Public: how to delete a VNPulse account and what happens to the data.\n\nGoogle Play asks for a web page that explains this without installing the app.","operationId":"account_delete_page_account_delete_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/forgot-password":{"get":{"summary":"Forgot Password Page","operationId":"forgot_password_page_forgot_password_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Forgot Password Post","description":"Four outcomes, three answers — and \"we emailed you\" is only one of them.\n\nThis used to answer \"check your inbox\" unconditionally: when the send raised\n(observed live as `MessageRejected — Email address is not verified` in the log\nwhile the page said sent), and when the IP was rate limited, where it did not\neven attempt a send. Someone whose reset mail never arrives because we never\nsent it has no reason to retry and no idea anything went wrong.\n\nThe neutral answer for an address we have no account for is CORRECT and stays:\nsaying \"no such account\" would turn this page into an account-existence oracle.\nThat is a different thing from claiming a send that failed. The residual leak —\nonly a registered address can produce \"the send failed\" — needs the provider to\nbe broken to be observable at all, and staying silent about a real failure is\nthe worse trade.","operationId":"forgot_password_post_forgot_password_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_forgot_password_post_forgot_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reset-password":{"get":{"summary":"Reset Password Page","operationId":"reset_password_page_reset_password_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/reset-password":{"post":{"summary":"Api Reset Password","operationId":"api_reset_password_api_reset_password_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/user/prefs":{"get":{"summary":"Get Prefs","operationId":"get_prefs_api_user_prefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"patch":{"summary":"Update Prefs","operationId":"update_prefs_api_user_prefs_patch","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/signout":{"get":{"summary":"Signout","operationId":"signout_signout_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/token":{"post":{"summary":"Api Auth Token","description":"Email + password (JSON) -> a bearer token for the native app.\n\nSame checks as the web sign-in: the per-IP and per-email login rate limits, and an\nunverified address is refused. Errors are JSON codes the app translates itself.","operationId":"api_auth_token_api_auth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/me":{"get":{"summary":"Api Auth Me","description":"Who the caller is (session cookie or bearer token), or 401.\n\nCarries the month's allowance so the app and the website can show what is\nleft without a second round trip — the number a subscriber is entitled to see\nat any moment, not only when they run out.","operationId":"api_auth_me_api_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/allowance":{"get":{"summary":"Api Ai Allowance","description":"Just the allowance, for a client polling it after each answer.","operationId":"api_ai_allowance_api_ai_allowance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/signup":{"post":{"summary":"Api Auth Signup","description":"Create an account. Answers `mailed` for every address we posted something to.\n\nMirrors POST /signup: a taken-but-unverified address gets its verification link\nagain, a finished account gets a reset link, and both answer exactly as a brand\nnew address does. A caller cannot tell the three apart.","operationId":"api_auth_signup_api_auth_signup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/verify-code":{"post":{"summary":"Api Auth Verify Code","description":"{email, code} -> verified and signed in: {token, user}, like /api/auth/token.\n\nErrors: 400 code_invalid (+attempts_left), code_expired, code_locked (ask for a new\ncode); 429 rate_limited. An unknown or already-verified address answers\ncode_invalid, the same as a wrong code, so this endpoint reveals nothing.","operationId":"api_auth_verify_code_api_auth_verify_code_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/resend-code":{"post":{"summary":"Api Auth Resend Code","description":"{email} -> a new code (and link) for a never-verified account.\n\nAlways answers {\"status\": \"sent\", \"cooldown\": 60} for a well-formed address, whether\nor not an unverified account exists or a send was skipped for the cooldown - the\napp counts the 60 s down itself. A failed provider send is reported (502), because\nsilence leaves someone waiting for mail that is never coming.","operationId":"api_auth_resend_code_api_auth_resend_code_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/forgot-password":{"post":{"summary":"Api Auth Forgot Password","description":"Send a reset link. `sent` covers both a real send and an unknown address.\n\nMirrors POST /forgot-password, including the one deliberate vagueness: an address\nwe have no account for is answered the same way as a successful send. A send that\nactually failed is reported, because silence there leaves someone waiting for mail\nthat is never coming.","operationId":"api_auth_forgot_password_api_auth_forgot_password_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/":{"get":{"summary":"Home","operationId":"home__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ai-chat":{"get":{"summary":"Ai Chat","operationId":"ai_chat_ai_chat_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/copilot":{"get":{"summary":"Copilot","operationId":"copilot_copilot_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/stream":{"post":{"summary":"Chat Stream","operationId":"chat_stream_api_chat_stream_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/copilot/chart":{"get":{"summary":"Copilot Chart","description":"The series for a chart spec saved in an answer (services/copilot/agentic_ui.py),\nfor a reopened thread: the same stored rows the chart was drawn from.","operationId":"copilot_chart_api_copilot_chart_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbol"}},{"name":"start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"type":"string","default":"","title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deep/steer":{"post":{"summary":"Deep Steer","description":"Add a steering note to the caller's running deep analysis (plan item H,\nservices/copilot/steer.py): picked up at the next round boundary. Same cohort as\nthe other agentic features (COPILOT_AGENTIC_UI).","operationId":"deep_steer_api_deep_steer_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/deep/cancel":{"post":{"summary":"Deep Cancel","description":"Cancel the caller's running deep analysis. The loop checks the flag between\nrounds and inside the model stream, so a cancel lands within seconds; the run\nstill writes down what it had (with a \"cancelled\" note) rather than nothing.","operationId":"deep_cancel_api_deep_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/model":{"get":{"summary":"Get Chat Model","description":"The composer's model menu: the two choices and the account's current pick.\n{\"model\": \"opus\"|\"sonnet\", \"options\": [{key, name, desc, model}]}.","operationId":"get_chat_model_api_chat_model_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Set Chat Model","description":"Remember the reader's pick for the account. Body {\"model\": \"opus\"|\"sonnet\"};\nanything else is a 400 (allow-list).","operationId":"set_chat_model_api_chat_model_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/sessions":{"get":{"summary":"List Chat Sessions","operationId":"list_chat_sessions_api_chat_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Save Chat Session","operationId":"save_chat_session_api_chat_sessions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/turn/{session_id}":{"get":{"summary":"Chat Turn State","description":"Is a turn for this conversation still running, and what has it produced so far?\n\nThe client that loses its stream -- Android suspending the socket, a switch from\nwifi to mobile data -- needs to tell \"still working\" apart from \"gone\". Without this\nits only question was GET /api/chat/sessions/<id>, which 404s until the answer is\nSAVED, so a slow turn looked identical to a dead one. On 2026-09-26 that cost a user\na two-minute deep analysis: three 404s in 13s, an error on screen, and the finished\nanswer arriving by push notification a minute later.\n\n`running` carries the partial text, so the app can keep showing the answer as it\nbuilds rather than a spinner it cannot explain.","operationId":"chat_turn_state_api_chat_turn__session_id__get","parameters":[{"name":"session_id","in":"path","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"}}}}}}},"/api/chat/cancel":{"post":{"summary":"Chat Cancel","description":"Stop the caller's running turn on purpose, keeping what it had already said.\n\nThe sibling of /api/chat/turn/<id>: that one asks \"is it still coming?\", this one\nsays \"stop\". Both are keyed by the conversation rather than by a run id, because\nthe client that needs them most is the one that lost its stream and knows only\nwhich conversation it was in.\n\nDeliberately NOT a cancel of the HTTP request. A dropped connection is treated as\n\"finish it anyway, the answer is paid for\" (services/copilot/persist.py), which is\nright for a tunnel that died and wrong for a user who pressed stop: that user will\nnever read the answer, so the work should stop and the meter with it.\n\n404 when the conversation is not the caller's, never 403 — an id that exists is\nsomething worth knowing, and answering \"forbidden\" to a guess confirms it.","operationId":"chat_cancel_api_chat_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/sessions/{session_id}":{"get":{"summary":"Load Chat Session","operationId":"load_chat_session_api_chat_sessions__session_id__get","parameters":[{"name":"session_id","in":"path","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"}}}}}},"delete":{"summary":"Delete Chat Session","operationId":"delete_chat_session_api_chat_sessions__session_id__delete","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chat/turn/{session_id}/stream":{"get":{"summary":"Follow Chat Turn","description":"Re-attach to an answer that is still being written (owner, 23/10/2026).\n\nThe same SSE frames /api/chat/stream sent, from the first one, then the rest as they\ncome -- steps, text, the closing `done` -- so the client paints it with the code that\npaints any turn. It only reads: nothing is admitted or charged here, the turn was\ncharged once by the request that started it (services/copilot/persist.py::follow).\n409 `elsewhere` when the turn runs in the other app process (a blue-green deploy is\ndraining it): the client then waits for the saved answer. 404 when none is running.","operationId":"follow_chat_turn_api_chat_turn__session_id__stream_get","parameters":[{"name":"session_id","in":"path","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"}}}}}}},"/api/chat/sessions/{session_id}/rename":{"post":{"summary":"Rename Chat Session Ep","operationId":"rename_chat_session_ep_api_chat_sessions__session_id__rename_post","parameters":[{"name":"session_id","in":"path","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"}}}}}}},"/api/chat/sessions/{session_id}/{flag}":{"post":{"summary":"Toggle Chat Session Ep","operationId":"toggle_chat_session_ep_api_chat_sessions__session_id___flag__post","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}},{"name":"flag","in":"path","required":true,"schema":{"type":"string","title":"Flag"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/screener":{"get":{"summary":"Screener","description":"Bộ lọc cổ phiếu / Screener (26/10/2026): presets, custom ranges and the NL box over\nPOST /api/screener/run — the screen_board table, the same one the app's screen reads.\nPublic; the shell is static and loads its own JSON. Served on BOTH products.","operationId":"screener_screener_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/watchlist":{"get":{"summary":"Watchlist Page","operationId":"watchlist_page_watchlist_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin":{"get":{"summary":"Admin Panel","operationId":"admin_panel_admin_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/app-versions":{"get":{"summary":"Admin App Versions","description":"The app version/build/OTA update/runtime each user last ran, newest first.","operationId":"admin_app_versions_admin_api_app_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/send-digest":{"post":{"summary":"Admin Send Digest","operationId":"admin_send_digest_admin_api_send_digest_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/set-tier":{"post":{"summary":"Admin Set Tier","operationId":"admin_set_tier_admin_api_set_tier_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/payments":{"get":{"summary":"Admin Payments","description":"Every payment, newest first — the reconciliation view against PayOS.","operationId":"admin_payments_api_admin_payments_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/reports/{report_id}/resolve":{"post":{"summary":"Admin Resolve Report","description":"Mark a reported answer handled, so the queue shortens as it is worked.","operationId":"admin_resolve_report_admin_reports__report_id__resolve_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/settings":{"get":{"summary":"Settings Page","operationId":"settings_page_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/report/{share_uuid}":{"get":{"summary":"Shared Report","operationId":"shared_report_report__share_uuid__get","parameters":[{"name":"share_uuid","in":"path","required":true,"schema":{"type":"string","title":"Share Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/terms":{"get":{"summary":"Terms Page","description":"Static legal text. No data, so it renders even when every feed is down —\nwhich is when someone is most likely to be reading about liability.","operationId":"terms_page_terms_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy":{"get":{"summary":"Privacy Page","operationId":"privacy_page_privacy_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/about":{"get":{"summary":"About Page","description":"Who runs this, what it is, where the data comes from, how to reach us.\n\nStatic: renders with every feed down. Its absence is part of why a corporate web\nfilter (Trellix, 19/10) scored ai.vnpulse.online as phishing -- a new domain with a\nsign-in form and nothing saying who is behind it.","operationId":"about_page_about_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/security.txt":{"get":{"summary":"Security Txt","operationId":"security_txt_security_txt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/security.txt":{"get":{"summary":"Security Txt","operationId":"security_txt__well_known_security_txt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/download":{"get":{"summary":"App Download Page","description":"The address we give beta testers for the Android app.\n\nSigned out on purpose: the person installing it may not have an account yet. The\nversion shown comes from the published manifest, never from a constant here — a\npage that claims a version it is not serving is how someone installs the wrong\nbuild and reports a bug we already fixed.","operationId":"app_download_page_download_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/app":{"get":{"summary":"App Download Page","description":"The address we give beta testers for the Android app.\n\nSigned out on purpose: the person installing it may not have an account yet. The\nversion shown comes from the published manifest, never from a constant here — a\npage that claims a version it is not serving is how someone installs the wrong\nbuild and reports a bug we already fixed.","operationId":"app_download_page_app_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/app/download":{"get":{"summary":"App Download File","description":"Hand the phone somewhere to fetch the bytes, and carry none of them.\n\nThe box is a t4g.micro. Streaming a ~115MB APK through this process would hold a\nworker for the whole download and take memory from the market feed, so the file is\nserved by S3 (or a CDN when APK_PUBLIC_BASE is set) and this route only points at\nit. `?v=` fetches an earlier build so a tester can go back.","operationId":"app_download_file_app_download_get","parameters":[{"name":"v","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"V"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/app/latest":{"get":{"summary":"App Latest Release","description":"The published build, for an installed app deciding whether it is behind.\n\nThe app cannot ask Expo this: EAS Update only knows about JS updates, and the\nbuilds that matter here are the ones OTA CANNOT deliver — a new native module, a\nnew permission. So the app compares its own versionCode against what the download\npage is serving, which is this.\n\n`release: null` is a real answer (nothing published yet), not an error, and the app\nshows no banner for it rather than offering a link to nothing.","operationId":"app_latest_release_api_app_latest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/pricing":{"get":{"summary":"Pricing Page","description":"No plans any more (owner 19/10/2026): the app is free, only the AI is paid from\na prepaid balance, and that balance lives on /settings#ai-balance.","operationId":"pricing_page_pricing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing":{"get":{"summary":"Billing Page","description":"The old plan / usage page. Top-ups and receipts are on /settings#ai-balance.","operationId":"billing_page_billing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/status/{order_code}":{"get":{"summary":"Billing Status Page","description":"Where PayOS sends the customer back to, and what they watch while it settles.\n\nA bank transfer is not instant and the return URL fires the moment the customer\ncloses the payment app, so this page is often loaded BEFORE the money is confirmed.\nIt therefore polls rather than declaring an outcome, and the poll is the endpoint\nthat re-reads PayOS — so a customer sitting on this page gets their subscription\neven if the webhook never arrives.","operationId":"billing_status_page_billing_status__order_code__get","parameters":[{"name":"order_code","in":"path","required":true,"schema":{"type":"integer","title":"Order Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ticker-chart/{ticker}":{"get":{"summary":"Stock Chart Page","description":"The stock chart, on BOTH products: TradingView Lightweight Charts (Apache-2.0),\nthe same library and the same data endpoints as the app's stock screen\n(/api/stock/{t}/quote, /history, /intraday). Not under /stock/ or /chart because those\nprefixes are platform-only (services/flags.PLATFORM_ONLY_PAGES) and this page is not.\n\nThe TradingView Advanced Charts datafeed (/api/udf/*) stays in the code, unused,\nuntil TradingView answers the licence application. Never the free embed widget:\nit refuses every HOSE symbol.","operationId":"stock_chart_page_ticker_chart__ticker__get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/insight/{ticker}":{"get":{"summary":"Stock Insight Page","description":"\"Phân tích doanh nghiệp\" — the web twin of the app's company report\n(mobile/src/app/insight/[ticker].tsx). Served on BOTH products, like\n/ticker-chart/{t} (not under a services/flags.PLATFORM_ONLY_PAGES prefix).\n\nDATA ONLY: the page fetches GET /api/stock/{t}/insight, which the server already\nbuilds and caches; the only AI is the \"Hỏi AI\" hand-off to the copilot.","operationId":"stock_insight_page_insight__ticker__get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ticker-news/{ticker}":{"get":{"summary":"Ticker News Page","description":"Every news item and filing for one ticker, newest first, infinite scroll — the\n\"Xem thêm tin\" target of the chart page's news panel and the insight page. Served on\nBOTH products (not under a PLATFORM_ONLY_PAGES prefix). DATA ONLY: the page pages in\nGET /api/stock/{t}/news/all; every row carries a picture.","operationId":"ticker_news_page_ticker_news__ticker__get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/overview":{"get":{"summary":"Home Overview Page","description":"Tổng quan / Overview (20/10/2026): the market at a glance, then what changed for the\nreader (GET /api/home: fired alerts, watchlist movers, the paper account, the last\nchat — or a 3-step start card), then the brief. Public, on BOTH products; \"/\" stays\nthe AI chat. The shell is static and every block loads its own public JSON.","operationId":"home_overview_page_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/connect":{"get":{"summary":"Connect Page","description":"Use VNPulse inside the reader's own Claude / ChatGPT (26/10/2026): the steps to add\nthe hosted MCP server (routers/mcp.py) as a connector. Public, on BOTH products; the\nfull reference stays at /mcp.","operationId":"connect_page_connect_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/me":{"get":{"summary":"Glance Page","description":"Trang của tôi / My page (26/10/2026): the reader's own glance page. The layout, the\ncatalogue and every widget's figures come from /api/glance/* (routers/glance.py); a\nguest sees the market preset, read-only.","operationId":"glance_page_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/market":{"get":{"summary":"Market Overview Page","description":"The market overview (19/10/2026): index cards, the one-line read, the VN-Index\nsession line, breadth, drivers, top lists, foreign + prop-desk flows, world, news.\nPublic and served on BOTH products. The page shell is static; every section loads\nits own public JSON endpoint and updates in place (templates/market_overview.html).","operationId":"market_overview_page_market_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/notifications":{"get":{"summary":"Notifications Page","description":"THE inbox on the web (24/10/2026), opened by the header bell: the stored\nnotifications plus the alert fires and system messages (GET /api/notify/inbox\n?with=fires), newest first; opening it marks everything read. Signed in only.","operationId":"notifications_page_notifications_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/paper":{"get":{"summary":"Paper Alias","description":"/paper is the app's route name (/paper?ticker=HPG&side=sell) and what people type;\nthe web page lives at /paper-trading. 404'd in the launch crawl (18/10).","operationId":"paper_alias_paper_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/paper-trading":{"get":{"summary":"Paper Trading Page","operationId":"paper_trading_page_paper_trading_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/unsubscribe":{"get":{"summary":"Unsubscribe Digest","operationId":"unsubscribe_digest_unsubscribe_get","parameters":[{"name":"uid","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Uid"}},{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sitemap.xml":{"get":{"summary":"Sitemap","operationId":"sitemap_sitemap_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/robots.txt":{"get":{"summary":"Robots","operationId":"robots_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/search":{"get":{"summary":"Search","description":"Symbol or company-name search.\n\nReturns a bare LIST by default — five pages on the site read it that way. With\n`meta=1` it returns `{\"results\": [...], \"verdict\": ...}` instead, for a caller that\nhas to SAY something when there is no match: an empty list alone cannot tell a\nmisspelling from a real stock we do not carry. `verdict` is one of:\n\n    \"found\"    — results, nothing to explain\n    \"absent\"   — the universe is loaded and complete and does not have it, so we are\n                 entitled to say it is not a listed symbol\n    \"unknown\"  — no match, but the universe is short or the query is a name, so the\n                 honest answer is \"not found HERE\", never \"does not exist\"\n\nThe distinction is not pedantry: on 2026-09-06 a universe that was 85% loaded told\nthe owner FPT and VRE were not stocks. `ticker_is_known_absent` is the guard.","operationId":"search_api_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"meta","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Meta"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/symbols":{"get":{"summary":"Symbols","description":"Every listed symbol, for a client that has to tell a ticker from a word.\n\nThe app makes tickers in a copilot answer tappable (→ that stock's screen). \"HPG\"\nand \"ATC\" look alike to a regex; only the listing tells them apart, so the app\nfetches this once per launch. Symbols only (~1,600 × 3 chars ≈ 10 KB), indices\nseparately because they open the index screen, not a company one.\n\nNever triggers a cold universe fetch (get_cached_stocks): an empty list just means\nthe app falls back to its built-in VN30 + liquid-HOSE set for now.","operationId":"symbols_api_symbols_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/indices":{"get":{"summary":"Indices","operationId":"indices_api_indices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/signals":{"get":{"summary":"Market Signals","operationId":"market_signals_api_market_signals_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/highlights":{"get":{"summary":"Market Highlights","description":"Điểm nhấn hôm nay — gainers/losers/volume/foreign tabs with sparklines.","operationId":"market_highlights_api_market_highlights_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/volume-spikes":{"get":{"summary":"Market Volume Spikes","description":"Đột biến khối lượng — names on HOSE, HNX and UPCOM trading far above MA5 (5 sessions before\ntoday), pro-rated to the session elapsed while it is open. services/volume_spikes.","operationId":"market_volume_spikes_api_market_volume_spikes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/home":{"get":{"summary":"Home Api","description":"The Tổng quan tab's personal block in one read (services/home): the reader's\nwatchlist movers, paper account, today's fired alerts, the last conversation and the\nstart-card state; a signed-out guest gets the start chips and the session only.","operationId":"home_api_api_home_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/session":{"get":{"summary":"Market Session Api","description":"The session strip's phase (pre|ato|cont|lunch|atc|pt|closed|weekend|holiday), when it\nnext changes, the session whose figures are shown and the next open — the clock and\nthe trading calendar only, no data read (services/market_clock.session).","operationId":"market_session_api_api_market_session_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/panorama":{"get":{"summary":"Market Panorama Api","description":"The market page's heatmap, sectors table, breadth histogram, limit / value lists,\nliquidity vs the 20-session average (time-matched), active buy/sell and MA breadth —\none shared build from the board in memory and S3 caches (services/market_panorama).","operationId":"market_panorama_api_api_market_panorama_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/market/exchange":{"get":{"summary":"Market Exchange Api","description":"The Market tab's \"Sàn\" view for one exchange or basket (services/market_exchange):\nthe index, its breadth and value, and the active / up / down lists as stock rows.","operationId":"market_exchange_api_api_market_exchange_get","parameters":[{"name":"ex","in":"query","required":false,"schema":{"type":"string","default":"HOSE","title":"Ex"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/market/sparks":{"get":{"summary":"Market Sparks Api","description":"Index-card sparklines for the market page (web + app). Memory only — see\nmarket_sparks(); shared by every viewer, so no reader ever costs an SSI call.","operationId":"market_sparks_api_api_market_sparks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/starters":{"get":{"summary":"Chat Starters","description":"The suggestion chips on an empty chat screen — personal, rotating, model-free.\n\nSigned out it still answers, with the market-wide set: a stranger opening the app\nshould see today's questions, not a sign-in wall. Signed in it draws on the reader's\nwatchlist and recent conversations.\n\nDeliberately NOT cached per user at this layer: the expensive inputs (highlights,\nwatchlist) carry their own caches, and the part that varies run to run is the pick,\nwhich is free. Caching here would freeze the rotation that is the whole point.","operationId":"chat_starters_api_chat_starters_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"vi","title":"Lang"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":3,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/top-movers":{"get":{"summary":"Top Movers","operationId":"top_movers_api_top_movers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/board":{"get":{"summary":"Board","description":"Light board rows (curated sector universe) — live prices when market open.","operationId":"board_api_board_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/sector/{slug}":{"get":{"summary":"Sector Detail","description":"Per-sector detail for the dashboard pop-up: synthetic index series + today's breadth.","operationId":"sector_detail_api_sector__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sector/{slug}/live":{"get":{"summary":"Sector Live","description":"Fresh (uncached) breadth + top stocks — polled by the modal to tick live in-session.","operationId":"sector_live_api_sector__slug__live_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sector/{code}/history":{"get":{"summary":"Sector History","description":"An ICB sector's daily index (services/sector_index: cap-weighted, chain-linked, base 1000)\nfor the chart's \"RSI so sánh\". `code` = an ICB code (2-8 digits) or a ticker (its ICB\nlevel-2 sector). Public market data: no sign-in, no ownership; cached per code.","operationId":"sector_history_api_sector__code__history_get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":1000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/news":{"get":{"summary":"News","description":"The market headlines, newest first — a bare list, as every caller reads it.\n\n23/10/2026, the app's endless feed: `before=<epoch seconds>` returns the next page\nof older headlines (`limit`, default 20, max 50) from the archive services/news\nkeeps; an empty list is the end. `chips=1` adds `chips: [{t, pct}]` per item, the\nboard's % for its tickers. Without either the answer is exactly what it was.","operationId":"news_api_news_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/snapshot":{"get":{"summary":"Snapshot Meta","operationId":"snapshot_meta_api_snapshot_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/snapshot/stock/{ticker}":{"get":{"summary":"Snapshot Stock","operationId":"snapshot_stock_api_snapshot_stock__ticker__get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/prices":{"get":{"summary":"Prices","operationId":"prices_api_prices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/live-movers":{"get":{"summary":"Live Movers","operationId":"live_movers_api_live_movers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stock/{ticker}/info":{"get":{"summary":"Stock Info","operationId":"stock_info_api_stock__ticker__info_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/quote":{"get":{"summary":"Stock Quote","description":"Live price and the session's real trading band for one ticker.\n\nExists for the stock page. Everything else per-stock (company, financials,\nratios, news) was already served here; price was not, so the closest thing\nto a stock view -- the dashboard's insights panel -- showed company profile\nand no price at all.\n\n`ref`/`ceil`/`floor` are SSI's authoritative per-session limits when\navailable and null when not. A caller must render no band rather than\ninventing one from the exchange: Vietnam widens the band for a first trading\nday or a post-suspension resumption, so a fixed band is wrong exactly when it\nmatters most.","operationId":"stock_quote_api_stock__ticker__quote_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/panel":{"get":{"summary":"Stock Panel","description":"One payload for the copilot's in-chat ticker panel. See _panel_build().","operationId":"stock_panel_api_stock__ticker__panel_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream/status":{"get":{"summary":"Stream Status","description":"SSI stream connection health: per-channel connects / resets / failures since\nprocess start, last tick age, and whether the X-TRADE trial channel is on.\nSame numbers as `journalctl | grep '\\[ssi-stream\\]'`, without the log.","operationId":"stream_status_api_stream_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stock/{ticker}/tape":{"get":{"summary":"Stock Tape","description":"Today's time-and-sales for one stock + active buy/sell totals.\n\nSource: the SSI stream accumulated in-process by services/tape.py (SSI has no\ntick-history endpoint, so nothing exists before the process started or when\nSTREAM_ENABLED is off). `rows` newest first: {t, p, chg, v, side, src} — renders as\n`Khớp | Giá | +/- | KL | M/B`. `side_source` is \"vendor\" only when SSI itself sent\nthe side (X-TRADE); \"inferred\" means the quote/tick rule (src Q/T per row); null\nmeans no side at all (auction / put-through only). Render the label, not just the\ncolour — an inferred side must not be presented as a vendor fact.","operationId":"stock_tape_api_stock__ticker__tape_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/intraday":{"get":{"summary":"Stock Intraday","operationId":"stock_intraday_api_stock__ticker__intraday_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"timeframe","in":"query","required":false,"schema":{"type":"string","default":"data_5P","title":"Timeframe"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/depth":{"get":{"summary":"Stock Depth","description":"Live 3-level order book for the panel's Sổ lệnh tab. See _depth_build().\n\nIn-memory only -- no vendor call, no S3 read, no lock -- so the tab's poll costs\na dict lookup on a 1 GB box serving live traffic. The limit is sized for one\nreader polling one open panel every 5s (12/min) with room for a second panel and\na reload, not for a page that would poll several symbols at once.","operationId":"stock_depth_api_stock__ticker__depth_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/orderbook":{"get":{"summary":"Stock Orderbook","description":"A PAST session's order book at a picked minute (services/orderbook.py): the book as\nit stood at the end of `time` (HH:MM ICT) on `date` (YYYY-MM-DD). Stored from\n2026-09-22; a session is readable after 15:15 ICT. Today's live book is /depth.","operationId":"stock_orderbook_api_stock__ticker__orderbook_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","title":"Date"}},{"name":"time","in":"query","required":false,"schema":{"type":"string","default":"14:45","title":"Time"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/volume-profile":{"get":{"summary":"Stock Volume Profile","description":"Volume at price for the panel's \"Mức giá\" tab. See _volume_profile_build().","operationId":"stock_volume_profile_api_stock__ticker__volume_profile_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/panel/history":{"get":{"summary":"Stock Panel History","description":"Older daily sessions for the panel's chart, paged from the newest backwards.\n\n`offset` = sessions the chart already holds (counting back from the most recent),\n`limit` = page size (<= _PANEL_PAGE_MAX). Same compact [date,o,h,l,c,v] tuples and\nthe SAME adjusted basis as the /panel payload -- both come from one builder in\nservices.history -- so a page prepended to the chart cannot sit on a different\nbasis from the bars already drawn. `total_sessions` and `has_more` tell the chart\nwhen to stop asking. Not the stock page's /history (dict rows, its own paging).","operationId":"stock_panel_history_api_stock__ticker__panel_history_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/history":{"get":{"summary":"Stock History","operationId":"stock_history_api_stock__ticker__history_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/overlays":{"get":{"summary":"Stock Overlays","description":"Extra chart overlays (EMA34, BB, Ichimoku, ...) for /ticker-chart, from the SAME\nengine as the copilot's stock_indicators (services/ta.py): [{id, label, pts}].","operationId":"stock_overlays_api_stock__ticker__overlays_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"ind","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ind"}},{"name":"start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Start"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/events":{"get":{"summary":"Stock Events","operationId":"stock_events_api_stock__ticker__events_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/financials":{"get":{"summary":"Stock Financials","operationId":"stock_financials_api_stock__ticker__financials_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"year","title":"Period"}},{"name":"periods","in":"query","required":false,"schema":{"type":"integer","default":8,"title":"Periods"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/valuation-history":{"get":{"summary":"Stock Valuation History","description":"Daily P/E or P/B for the chart's valuation bands (services/valuation_series.py):\n[[date, value | null]] over the last `years` (1-10), the band statistics, and which\nmetric leads for this stock (P/B for banks and brokers). metric = pe | pb | auto.","operationId":"stock_valuation_history_api_stock__ticker__valuation_history_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"metric","in":"query","required":false,"schema":{"type":"string","default":"auto","title":"Metric"}},{"name":"years","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Years"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/activity":{"get":{"summary":"Stock Activity","operationId":"stock_activity_api_stock__ticker__activity_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/company":{"get":{"summary":"Stock Company","description":"Profile / shareholders / dividends for the insights modal tabs (cache-only).","operationId":"stock_company_api_stock__ticker__company_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/insight":{"get":{"summary":"Stock Insight","description":"Phân tích doanh nghiệp — the app's company report. Data only, no model call:\nservices/stock_insight.py assembles it from the caches the copilot tools read, and\ncaches the result per ticker (10 min in session, until the next morning after).","operationId":"stock_insight_api_stock__ticker__insight_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/news/all":{"get":{"summary":"Stock News All","description":"Every news item and filing for one ticker, newest first, paged — the app's\n\"Xem tất cả tin\" screen. Data only (RSS cache + Vietcap's list, 30 min); a page whose\nlinks are still arriving says `pending: true`.","operationId":"stock_news_all_api_stock__ticker__news_all_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/ratios":{"get":{"summary":"Stock Ratios","operationId":"stock_ratios_api_stock__ticker__ratios_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/trending":{"get":{"summary":"Trending","operationId":"trending_api_trending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stock/{ticker}/peers":{"get":{"summary":"Stock Peers","operationId":"stock_peers_api_stock__ticker__peers_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/sector-peers":{"get":{"summary":"Stock Sector Peers","operationId":"stock_sector_peers_api_stock__ticker__sector_peers_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/compare":{"get":{"summary":"Compare Stocks","operationId":"compare_stocks_api_compare_get","parameters":[{"name":"tickers","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tickers"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/screener/ai":{"post":{"summary":"Ai Screener","operationId":"ai_screener_api_screener_ai_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/screener/meta":{"get":{"summary":"Screener Meta","description":"Bộ lọc cổ phiếu: presets, filter fields, floors — for the web page and the app.","operationId":"screener_meta_api_screener_meta_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/screener/run":{"post":{"summary":"Screener Run","description":"Bộ lọc cổ phiếu (26/10/2026): one screen over the screen_board table, JSON for the\nweb page and the app. Body: {preset, filters {field: {min,max,gt,lt}}, exchange,\nsector (ICB code), min_avg_value_ty, sort, order, page, page_size}.","operationId":"screener_run_api_screener_run_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/screener":{"get":{"summary":"Screener","operationId":"screener_api_screener_get","parameters":[{"name":"change_min","in":"query","required":false,"schema":{"type":"number","default":-100,"title":"Change Min"}},{"name":"change_max","in":"query","required":false,"schema":{"type":"number","default":100,"title":"Change Max"}},{"name":"change30d_min","in":"query","required":false,"schema":{"type":"number","default":-100,"title":"Change30D Min"}},{"name":"change30d_max","in":"query","required":false,"schema":{"type":"number","default":100,"title":"Change30D Max"}},{"name":"vol_ratio_min","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Vol Ratio Min"}},{"name":"price_min","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Price Min"}},{"name":"price_max","in":"query","required":false,"schema":{"type":"number","default":9999999,"title":"Price Max"}},{"name":"from_high_min","in":"query","required":false,"schema":{"type":"number","default":-100,"title":"From High Min"}},{"name":"from_high_max","in":"query","required":false,"schema":{"type":"number","default":0,"title":"From High Max"}},{"name":"rsi_min","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Rsi Min"}},{"name":"rsi_max","in":"query","required":false,"schema":{"type":"number","default":100,"title":"Rsi Max"}},{"name":"pe_max","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Pe Max"}},{"name":"pb_max","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Pb Max"}},{"name":"roe_min","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Roe Min"}},{"name":"div_yield_min","in":"query","required":false,"schema":{"type":"number","default":0,"title":"Div Yield Min"}},{"name":"pattern","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Pattern"}},{"name":"exchange","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Exchange"}},{"name":"sector","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Sector"}},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","default":"change_desc","title":"Sort By"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/knowledge-graph":{"post":{"summary":"Copilot Knowledge Graph","description":"Build a news knowledge graph for the Copilot News agent.\n\nPremium-only (matches the Copilot page). Fetches recent news (ticker-specific\nwhen a ticker is given, else general market news), extracts an entity graph\nvia the AI, and returns {subject, ticker, graph, headlines}.","operationId":"copilot_knowledge_graph_api_copilot_knowledge_graph_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/feed":{"get":{"summary":"Get Feed Api","operationId":"get_feed_api_api_feed_get","parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Post To Feed","operationId":"post_to_feed_api_feed_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/feed/{post_id}":{"delete":{"summary":"Delete Feed Post","operationId":"delete_feed_post_api_feed__post_id__delete","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feed/{post_id}/like":{"post":{"summary":"Toggle Post Like","operationId":"toggle_post_like_api_feed__post_id__like_post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feed/{post_id}/comments":{"get":{"summary":"Get Post Comments","operationId":"get_post_comments_api_feed__post_id__comments_get","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Add Post Comment","operationId":"add_post_comment_api_feed__post_id__comments_post","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feed/{post_id}/comments/{comment_id}":{"delete":{"summary":"Delete Post Comment","operationId":"delete_post_comment_api_feed__post_id__comments__comment_id__delete","parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}},{"name":"comment_id","in":"path","required":true,"schema":{"type":"integer","title":"Comment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/news":{"get":{"summary":"Stock News","operationId":"stock_news_api_stock__ticker__news_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/discussion":{"get":{"summary":"Get Discussion","operationId":"get_discussion_api_stock__ticker__discussion_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Post Discussion","operationId":"post_discussion_api_stock__ticker__discussion_post","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/discussion/{post_id}":{"delete":{"summary":"Delete Discussion","operationId":"delete_discussion_api_stock__ticker__discussion__post_id__delete","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/discussion/{post_id}/like":{"post":{"summary":"Like Discussion","operationId":"like_discussion_api_stock__ticker__discussion__post_id__like_post","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"post_id","in":"path","required":true,"schema":{"type":"integer","title":"Post Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/foreign-flow":{"get":{"summary":"Stock Foreign Flow","description":"Per-ticker foreign flow, last `n` (<=1000) sessions oldest->newest — SSI only.\n\nSource of truth is services.foreign_stock.get_stock_foreign_flow (stored pipeline\nsessions + the crawler's live row; direct SSI only while the store is behind).\nKBS was retired here on 2026-09-12: its TREND feed stopped at 2026-08-26 for\nevery ticker checked, and it never carried a traded value (its FR was the room).\n\nReply: {ticker, source, flow:[{date, buy, sell, net (shares), buy_value,\nsell_value, net_value (₫), room, live}], as_of, stale, stale_reason,\nnet_value_total (₫, the \"GT NN mua ròng N phiên\" figure)}. `stale` is the\nfrozen-feed check — a UI must show as_of/stale rather than a dead window as\ncurrent. Index symbols get the market-wide value series (volumes null).","operationId":"stock_foreign_flow_api_stock__ticker__foreign_flow_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"n","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/report-card":{"post":{"summary":"Generate Report Card","operationId":"generate_report_card_api_stock__ticker__report_card_post","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/report/{share_uuid}":{"get":{"summary":"Get Shared Report","operationId":"get_shared_report_api_report__share_uuid__get","parameters":[{"name":"share_uuid","in":"path","required":true,"schema":{"type":"string","title":"Share Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/report-cards":{"get":{"summary":"List My Reports","operationId":"list_my_reports_api_stock__ticker__report_cards_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/world-markets":{"get":{"summary":"World Markets","operationId":"world_markets_api_world_markets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/breadth":{"get":{"summary":"Market Breadth","description":"% of HOSE stocks above their 50/200-day MAs (~6-month series). Cached; empty until\nthe background compute has warmed. Feeds the VN-Index page's breadth panel.","operationId":"market_breadth_api_breadth_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/index-intraday":{"get":{"summary":"Index Intraday","description":"Per-minute intraday series for an index's most recent session (VN-Index page).","operationId":"index_intraday_api_index_intraday_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"VNINDEX","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/heatmap":{"get":{"summary":"Market Heatmap","description":"Compact treemap tiles ({s,k,c,g}) — lean + cached so the dashboard heatmap loads\nfast and polls near-live. Much smaller than /board.","operationId":"market_heatmap_api_heatmap_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/macro/worldbank":{"get":{"summary":"Macro Indicators","description":"Vietnam macro-economy indicators (World Bank, annual series). Feeds /index/vnindex.\n(Was /api/macro until 24/10/2026; that path now serves the Vĩ mô cards.)","operationId":"macro_indicators_api_macro_worldbank_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/macro":{"get":{"summary":"Macro Cards","description":"The Vĩ mô tab's indicator cards: latest value, reference period, release date,\nchange vs the previous period coloured good/bad FOR STOCKS, a 24-period line.","operationId":"macro_cards_api_macro_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/macro/series/{sid}":{"get":{"summary":"Macro Series","description":"One macro series' full history (the card's detail chart).","operationId":"macro_series_api_macro_series__sid__get","parameters":[{"name":"sid","in":"path","required":true,"schema":{"type":"string","title":"Sid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/macro/calendar":{"get":{"summary":"Macro Calendar","description":"Macro releases, the last 7 days and the next 14 (actual / previous; no consensus).","operationId":"macro_calendar_api_macro_calendar_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/macro/sectors":{"get":{"summary":"Macro Sectors Map","description":"The hand-written sector-impact map (services/macro_sectors.py).","operationId":"macro_sectors_map_api_macro_sectors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/foreign/series":{"get":{"summary":"Foreign Series","description":"Whole-market foreign buy/sell/net over the last n sessions + per-stock leaders.\nFeeds the Foreign detail page (/index/foreign).","operationId":"foreign_series_api_foreign_series_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tudoanh/series":{"get":{"summary":"Tudoanh Series","description":"Proprietary-desk (Tự doanh) buy/sell/net over the last n sessions.\nFeeds the Prop-desk detail page (/index/prop-desk).","operationId":"tudoanh_series_api_tudoanh_series_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/market/periods":{"get":{"summary":"Market Periods Api","description":"Index cards' week / month / year (web /overview + /market, the app's Tổng quan):\nper index the reference session, its close and the daily closes since.","operationId":"market_periods_api_api_market_periods_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/index-history":{"get":{"summary":"Index History","description":"OHLC daily history for an index (dashboard chart).\n\nPaged mode (`limit` > 0): returns a newest-first page + `has_more`, so the chart\nlazy-loads older candles on scroll instead of pulling the whole multi-year series.\nLegacy mode (`limit` == 0): full `days`-deep history (kept for back-compat).","operationId":"index_history_api_index_history_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"VNINDEX","title":"Symbol"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":4000,"title":"Days"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sentiment":{"get":{"summary":"Market Sentiment","operationId":"market_sentiment_api_sentiment_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/price-board":{"get":{"summary":"Price Board","operationId":"price_board_api_price_board_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/udf/config":{"get":{"summary":"Udf Config","operationId":"udf_config_api_udf_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/udf/symbols":{"get":{"summary":"Udf Symbols","operationId":"udf_symbols_api_udf_symbols_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/udf/history":{"get":{"summary":"Udf History","operationId":"udf_history_api_udf_history_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbol"}},{"name":"resolution","in":"query","required":false,"schema":{"type":"string","default":"D","title":"Resolution"}},{"name":"from","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"To"}},{"name":"countback","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Countback"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/udf/search":{"get":{"summary":"Udf Search","operationId":"udf_search_api_udf_search_get","parameters":[{"name":"query","in":"query","required":false,"schema":{"type":"string","default":"","title":"Query"}},{"name":"type","in":"query","required":false,"schema":{"type":"string","default":"","title":"Type"}},{"name":"exchange","in":"query","required":false,"schema":{"type":"string","default":"","title":"Exchange"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/udf/time":{"get":{"summary":"Udf Time","operationId":"udf_time_api_udf_time_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/watchlist":{"get":{"summary":"List Watchlist","operationId":"list_watchlist_api_watchlist_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/watchlist/export.csv":{"get":{"summary":"Export Watchlist Csv","description":"The default list as CSV. Same builder as /api/watchlists/{gid}/export.csv: the\nview and period on screen, the table's columns and the table's row figures. (The\nold file had a snapshot \"30D %\" no view on the page shows any more.)","operationId":"export_watchlist_csv_api_watchlist_export_csv_get","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","default":"overview","title":"View"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"1M","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlist/{ticker}":{"post":{"summary":"Toggle Watchlist","operationId":"toggle_watchlist_api_watchlist__ticker__post","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlist/{ticker}/status":{"get":{"summary":"Watchlist Status","operationId":"watchlist_status_api_watchlist__ticker__status_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists":{"get":{"summary":"List Groups","operationId":"list_groups_api_watchlists_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Create Group","operationId":"create_group_api_watchlists_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/watchlists/membership":{"get":{"summary":"Ticker Membership","description":"Which of the user's own groups hold `ticker` (the chart's add-to-group popover).\nOnly the caller's groups; system groups are read-only and not listed.","operationId":"ticker_membership_api_watchlists_membership_get","parameters":[{"name":"ticker","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/hidden":{"put":{"summary":"Hide Group","description":"Hide (true) or show (false) a built-in group (VN30, VN100).","operationId":"hide_group_api_watchlists__gid__hidden_put","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/order":{"put":{"summary":"Reorder Groups","operationId":"reorder_groups_api_watchlists_order_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/watchlists/{gid}":{"patch":{"summary":"Rename Group","operationId":"rename_group_api_watchlists__gid__patch","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Group","operationId":"delete_group_api_watchlists__gid__delete","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/quotes":{"get":{"summary":"Group Quotes","description":"The group with a card per ticker: price, change, ref/ceiling/floor, exchange,\nshort name, and the session's intraday line (or an explicit `none`).","operationId":"group_quotes_api_watchlists__gid__quotes_get","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/table":{"get":{"summary":"Group Table","description":"The website's watchlist table for one group: every view's figures per row, one\nclose array per row with the period returns computed off that same array\n(services/watchlist_table). Cached sources only -- no vendor call per request.\n\n`closes` (the app passes 61, enough for its 3M window) trims each row's close\narray to the last N points; the returns are computed before the trim.","operationId":"group_table_api_watchlists__gid__table_get","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}},{"name":"closes","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Closes"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/export.csv":{"get":{"summary":"Group Export Csv","description":"The table as CSV: the view and period on screen, the same columns and the same\nrow figures (services.watchlist_table.csv_columns).","operationId":"group_export_csv_api_watchlists__gid__export_csv_get","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","default":"overview","title":"View"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"1M","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/fundamentals":{"get":{"summary":"Group Fundamentals","description":"The Fundamentals view, fetched only when that tab is opened.","operationId":"group_fundamentals_api_watchlists__gid__fundamentals_get","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/tickers":{"post":{"summary":"Add Ticker","operationId":"add_ticker_api_watchlists__gid__tickers_post","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Reorder Tickers","operationId":"reorder_tickers_api_watchlists__gid__tickers_put","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/watchlists/{gid}/tickers/{ticker}":{"delete":{"summary":"Remove Ticker","operationId":"remove_ticker_api_watchlists__gid__tickers__ticker__delete","parameters":[{"name":"gid","in":"path","required":true,"schema":{"type":"integer","title":"Gid"}},{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/memory":{"get":{"summary":"List Memory","operationId":"list_memory_api_copilot_memory_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Forget All","operationId":"forget_all_api_copilot_memory_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/copilot/memory/{item_id}":{"delete":{"summary":"Forget One","operationId":"forget_one_api_copilot_memory__item_id__delete","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"integer","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/suggestions":{"get":{"summary":"Copilot Suggestions","operationId":"copilot_suggestions_api_copilot_suggestions_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"vi","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/capabilities":{"get":{"summary":"Copilot Capabilities","operationId":"copilot_capabilities_api_copilot_capabilities_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/actions/{aid}":{"get":{"summary":"Get Action","operationId":"get_action_api_copilot_actions__aid__get","parameters":[{"name":"aid","in":"path","required":true,"schema":{"type":"string","title":"Aid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/actions/{aid}/confirm":{"post":{"summary":"Confirm Action","operationId":"confirm_action_api_copilot_actions__aid__confirm_post","parameters":[{"name":"aid","in":"path","required":true,"schema":{"type":"string","title":"Aid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/actions/{aid}/cancel":{"post":{"summary":"Cancel Action","operationId":"cancel_action_api_copilot_actions__aid__cancel_post","parameters":[{"name":"aid","in":"path","required":true,"schema":{"type":"string","title":"Aid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/portfolio":{"get":{"summary":"List Portfolio","operationId":"list_portfolio_api_portfolio_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Add Position","operationId":"add_position_api_portfolio_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Clear Portfolio","description":"Xóa toàn bộ danh mục. `?confirm=1` is required: a bare DELETE does nothing.","operationId":"clear_portfolio_api_portfolio_delete","parameters":[{"name":"confirm","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Confirm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/portfolio/bulk":{"post":{"summary":"Bulk Save","description":"The import review's \"Lưu\": every row the user confirmed. Rows that fail are\nreturned with their error; the others are saved.","operationId":"bulk_save_api_portfolio_bulk_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/portfolio/{holding_id}":{"patch":{"summary":"Edit Position","operationId":"edit_position_api_portfolio__holding_id__patch","parameters":[{"name":"holding_id","in":"path","required":true,"schema":{"type":"integer","title":"Holding Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Remove Position","operationId":"remove_position_api_portfolio__holding_id__delete","parameters":[{"name":"holding_id","in":"path","required":true,"schema":{"type":"integer","title":"Holding Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/portfolio/import-image":{"post":{"summary":"Import Image","description":"A broker-app screenshot → PROPOSED rows for the review screen. Nothing is saved,\nand the image is not stored (services/portfolio_import.py).","operationId":"import_image_api_portfolio_import_image_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_import_image_api_portfolio_import_image_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/portfolio/export.csv":{"get":{"summary":"Export Portfolio Csv","operationId":"export_portfolio_csv_api_portfolio_export_csv_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/portfolio/sectors":{"get":{"summary":"Portfolio Sectors","operationId":"portfolio_sectors_api_portfolio_sectors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts":{"get":{"summary":"List Alerts","operationId":"list_alerts_api_alerts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Add Alert","operationId":"add_alert_api_alerts_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/triggered-count":{"get":{"summary":"Alerts Triggered Count","operationId":"alerts_triggered_count_api_alerts_triggered_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/acknowledge-all":{"post":{"summary":"Alerts Acknowledge All","operationId":"alerts_acknowledge_all_api_alerts_acknowledge_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/{alert_id}":{"delete":{"summary":"Remove Alert","operationId":"remove_alert_api_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/rules":{"get":{"summary":"List Rules","operationId":"list_rules_api_alerts_rules_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Create Rule","operationId":"create_rule_api_alerts_rules_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/rules/{rule_id}":{"delete":{"summary":"Delete Rule","operationId":"delete_rule_api_alerts_rules__rule_id__delete","parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"integer","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Edit Rule","description":"Change a rule the reader already has: a threshold, the window, the cooldown, where\nit is delivered, when it expires. The body is a partial rule; what it leaves out stays\nas it was. An id that is not the caller's is 404, not 403 — otherwise the answer tells\na stranger the rule exists.","operationId":"edit_rule_api_alerts_rules__rule_id__patch","parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"integer","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/rules/{rule_id}/status":{"post":{"summary":"Rule Status","operationId":"rule_status_api_alerts_rules__rule_id__status_post","parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"integer","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/fires":{"get":{"summary":"List Fires","operationId":"list_fires_api_alerts_fires_get","parameters":[{"name":"unseen","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Unseen"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/fires/{fire_id}":{"delete":{"summary":"Delete Fire","operationId":"delete_fire_api_alerts_fires__fire_id__delete","parameters":[{"name":"fire_id","in":"path","required":true,"schema":{"type":"integer","title":"Fire Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/fires/seen":{"post":{"summary":"Fires Seen","operationId":"fires_seen_api_alerts_fires_seen_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/push/expo-token":{"post":{"summary":"Push Register","description":"The app registers its Expo push token after sign-in (any signed-in account).","operationId":"push_register_api_push_expo_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Push Unregister","description":"Sign-out on the phone: stop sending this account's alerts to it.","operationId":"push_unregister_api_push_expo_token_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/push/test":{"post":{"summary":"Push Test","description":"Send one test notification to the caller's phones — proves the whole path.","operationId":"push_test_api_push_test_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/telegram":{"get":{"summary":"Telegram Status","operationId":"telegram_status_api_alerts_telegram_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/telegram/link":{"post":{"summary":"Telegram Link","operationId":"telegram_link_api_alerts_telegram_link_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Telegram Unlink","operationId":"telegram_unlink_api_alerts_telegram_link_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/telegram/webhook/{token}":{"post":{"summary":"Telegram Webhook","description":"Telegram pushes each update here: `/start <code>` links the chat; a button press\nconfirms or cancels a described rule; any other text from a linked chat is an ORDER\nin plain language (services/broker/orders.py).","operationId":"telegram_webhook_api_telegram_webhook__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/confirmations":{"get":{"summary":"Confirmations","operationId":"confirmations_api_broker_confirmations_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lang"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/confirmations/count":{"get":{"summary":"Confirmations Count","operationId":"confirmations_count_api_broker_confirmations_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/confirmations/{pid}/confirm":{"post":{"summary":"Confirmation Confirm","operationId":"confirmation_confirm_api_broker_confirmations__pid__confirm_post","parameters":[{"name":"pid","in":"path","required":true,"schema":{"type":"string","title":"Pid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/confirmations/{pid}/reject":{"post":{"summary":"Confirmation Reject","operationId":"confirmation_reject_api_broker_confirmations__pid__reject_post","parameters":[{"name":"pid","in":"path","required":true,"schema":{"type":"string","title":"Pid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/ssi/status":{"get":{"summary":"Ssi Status","operationId":"ssi_status_api_broker_ssi_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/link":{"post":{"summary":"Ssi Link","operationId":"ssi_link_api_broker_ssi_link_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/account":{"post":{"summary":"Ssi Account","operationId":"ssi_account_api_broker_ssi_account_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/start-login":{"post":{"summary":"Ssi Start Login","operationId":"ssi_start_login_api_broker_ssi_start_login_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/verify-otp":{"post":{"summary":"Ssi Verify","operationId":"ssi_verify_api_broker_ssi_verify_otp_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/login-pin":{"post":{"summary":"Ssi Login Pin","description":"ONE token request with the PIN; stored only if SSI accepts it.","operationId":"ssi_login_pin_api_broker_ssi_login_pin_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/broker/ssi/portfolio":{"get":{"summary":"Ssi Portfolio","operationId":"ssi_portfolio_api_broker_ssi_portfolio_get","parameters":[{"name":"refresh","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Refresh"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/ssi/orders":{"get":{"summary":"Ssi Orders","operationId":"ssi_orders_api_broker_ssi_orders_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/broker/ssi":{"delete":{"summary":"Ssi Disconnect","operationId":"ssi_disconnect_api_broker_ssi_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stock/{ticker}/rail-snapshot":{"get":{"summary":"Rail Snapshot","operationId":"rail_snapshot_api_stock__ticker__rail_snapshot_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"parts","in":"query","required":false,"schema":{"type":"string","default":"","title":"Parts"}},{"name":"ref","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Ref"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":60,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/rail-day":{"get":{"summary":"Rail Day","description":"One PAST session for the Info panel when the reader clicks a candle\n(services/session_day.py): that day's daily row + reference, its minute bars and\nvolume at price from the S3 archive, its stored foreign row and prop-desk row. Never\na vendor call. A completed session does not change, so the browser may keep it 5 min.","operationId":"rail_day_api_stock__ticker__rail_day_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"date","in":"query","required":false,"schema":{"type":"string","default":"","title":"Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/rail-prop":{"get":{"summary":"Rail Prop","description":"The latest session's HOSE prop-desk row for the Statistics tab (the same source and shape\nas rail-day's `prop`). `date` is the chart's last session. CafeF publishes after the close:\ntoday with no row yet is {\"state\": \"pending\"}, never \"none\". Off HOSE: {\"prop\": null}.","operationId":"rail_prop_api_stock__ticker__rail_prop_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"date","in":"query","required":false,"schema":{"type":"string","default":"","title":"Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stock/{ticker}/rail-news":{"get":{"summary":"Rail News","operationId":"rail_news_api_stock__ticker__rail_news_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/news-card/{ticker}/{kind}/{date}/{source}.png":{"get":{"summary":"News Card","operationId":"news_card_news_card__ticker___kind___date___source__png_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"kind","in":"path","required":true,"schema":{"type":"string","title":"Kind"}},{"name":"date","in":"path","required":true,"schema":{"type":"string","title":"Date"}},{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/news-card/2/{ticker}/{kind}/{date}/{source}.png":{"get":{"summary":"News Card","operationId":"news_card_news_card_2__ticker___kind___date___source__png_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"kind","in":"path","required":true,"schema":{"type":"string","title":"Kind"}},{"name":"date","in":"path","required":true,"schema":{"type":"string","title":"Date"}},{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ticker-news/{ticker}/read":{"get":{"summary":"News Read","operationId":"news_read_ticker_news__ticker__read_get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"vid","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ticker-news/{ticker}/read/{vid}":{"get":{"summary":"News Read","operationId":"news_read_ticker_news__ticker__read__vid__get","parameters":[{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}},{"name":"vid","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks":{"get":{"summary":"List Tasks","operationId":"list_tasks_api_tasks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/tasks/{tid}/runs":{"get":{"summary":"Task Runs","operationId":"task_runs_api_tasks__tid__runs_get","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/{tid}/pause":{"post":{"summary":"Pause Task","operationId":"pause_task_api_tasks__tid__pause_post","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/{tid}/resume":{"post":{"summary":"Resume Task","operationId":"resume_task_api_tasks__tid__resume_post","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/{tid}":{"delete":{"summary":"Delete Task","operationId":"delete_task_api_tasks__tid__delete","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/{tid}/run":{"post":{"summary":"Run Task Now","operationId":"run_task_now_api_tasks__tid__run_post","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/email-off":{"get":{"summary":"Email Off Page","description":"Shows what the link turns off; the POST does it (mail scanners follow links).","operationId":"email_off_page_api_tasks_email_off_get","parameters":[{"name":"k","in":"query","required":false,"schema":{"type":"string","default":"","title":"K"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Email Off","operationId":"email_off_api_tasks_email_off_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/notify/settings":{"get":{"summary":"Get Settings","operationId":"get_settings_api_notify_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"summary":"Put Settings","operationId":"put_settings_api_notify_settings_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/notify/inbox":{"get":{"summary":"Get Inbox","operationId":"get_inbox_api_notify_inbox_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/notify/inbox/{iid}":{"get":{"summary":"Get Item","operationId":"get_item_api_notify_inbox__iid__get","parameters":[{"name":"iid","in":"path","required":true,"schema":{"type":"integer","title":"Iid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/notify/inbox/read":{"post":{"summary":"Mark Read","operationId":"mark_read_api_notify_inbox_read_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/notify/digest/preview":{"get":{"summary":"Preview","operationId":"preview_api_notify_digest_preview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/events/calendar":{"get":{"summary":"Calendar","operationId":"calendar_api_events_calendar_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/events/calendar.ics":{"get":{"summary":"Calendar Ics","operationId":"calendar_ics_api_events_calendar_ics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/events/feed":{"get":{"summary":"Feed","operationId":"feed_api_events_feed_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Revoke","operationId":"revoke_api_events_feed_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/cal/{token}.ics":{"get":{"summary":"Subscription","operationId":"subscription_api_cal__token__ics_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/levels":{"get":{"summary":"List Levels","operationId":"list_levels_api_levels_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"summary":"Save Levels","operationId":"save_levels_api_levels_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/levels/preview":{"post":{"summary":"Preview Levels","operationId":"preview_levels_api_levels_preview_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/levels/{book}/{ticker}/rearm":{"post":{"summary":"Rearm Level","operationId":"rearm_level_api_levels__book___ticker__rearm_post","parameters":[{"name":"book","in":"path","required":true,"schema":{"type":"string","title":"Book"}},{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/levels/{book}/{ticker}":{"delete":{"summary":"Clear Level","operationId":"clear_level_api_levels__book___ticker__delete","parameters":[{"name":"book","in":"path","required":true,"schema":{"type":"string","title":"Book"}},{"name":"ticker","in":"path","required":true,"schema":{"type":"string","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/glance/catalog":{"get":{"summary":"Glance Catalog","operationId":"glance_catalog_api_glance_catalog_get","parameters":[{"name":"client","in":"query","required":false,"schema":{"type":"string","default":"web","title":"Client"}},{"name":"registry","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Registry"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/glance/layout":{"get":{"summary":"Glance Layout","operationId":"glance_layout_api_glance_layout_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/glance/layout/ops":{"post":{"summary":"Glance Ops","operationId":"glance_ops_api_glance_layout_ops_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/glance/data":{"get":{"summary":"Glance Data","operationId":"glance_data_api_glance_data_get","parameters":[{"name":"ids","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ids"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"vi","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chart/drawings":{"get":{"summary":"List Drawings","operationId":"list_drawings_api_chart_drawings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Clear Drawings","operationId":"clear_drawings_api_chart_drawings_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chart/drawings/{drawing_id}":{"put":{"summary":"Save Drawing","operationId":"save_drawing_api_chart_drawings__drawing_id__put","parameters":[{"name":"drawing_id","in":"path","required":true,"schema":{"type":"string","title":"Drawing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Drawing","operationId":"delete_drawing_api_chart_drawings__drawing_id__delete","parameters":[{"name":"drawing_id","in":"path","required":true,"schema":{"type":"string","title":"Drawing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chart/indicators":{"get":{"summary":"Get Indicator Layout","operationId":"get_indicator_layout_api_chart_indicators_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"summary":"Save Indicator Layout","operationId":"save_indicator_layout_api_chart_indicators_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/attachments":{"post":{"summary":"Upload","operationId":"upload_api_chat_attachments_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_api_chat_attachments_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chat/attachments/{att_id}":{"get":{"summary":"Fetch","description":"Redirect to a short-lived signed URL, so a reloaded conversation can still show\nits thumbnails. A parsed table has no file to serve; its text is returned instead.","operationId":"fetch_api_chat_attachments__att_id__get","parameters":[{"name":"att_id","in":"path","required":true,"schema":{"type":"string","title":"Att Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Remove","operationId":"remove_api_chat_attachments__att_id__delete","parameters":[{"name":"att_id","in":"path","required":true,"schema":{"type":"string","title":"Att Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/copilot/files/{file_id}":{"get":{"summary":"Copilot File","operationId":"copilot_file_api_copilot_files__file_id__get","parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"json","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Json"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/feedback/answer":{"post":{"summary":"Report Answer","operationId":"report_answer_api_feedback_answer_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/chat/share/{session_id}":{"post":{"summary":"Create Share","operationId":"create_share_api_chat_share__session_id__post","parameters":[{"name":"session_id","in":"path","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"}}}}}},"delete":{"summary":"Stop Share","operationId":"stop_share_api_chat_share__session_id__delete","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chat/shares":{"get":{"summary":"List Shares","operationId":"list_shares_api_chat_shares_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/s/{token}":{"get":{"summary":"Shared Page","operationId":"shared_page_s__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"}}}}}}},"/admin/activity":{"get":{"summary":"Activity Page","operationId":"activity_page_admin_activity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/testers":{"get":{"summary":"Testers List","operationId":"testers_list_admin_api_testers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Testers Grant","operationId":"testers_grant_admin_api_testers_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/testers-pending/revoke":{"post":{"summary":"Testers Revoke Pending","description":"Cancel a grant still waiting for its account (body: {email}).","operationId":"testers_revoke_pending_admin_api_testers_pending_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/testers/{user_id}/revoke":{"post":{"summary":"Testers Revoke","operationId":"testers_revoke_admin_api_testers__user_id__revoke_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/activity/chats":{"get":{"summary":"Activity Chats","operationId":"activity_chats_admin_api_activity_chats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/mcp":{"get":{"summary":"Activity Mcp","operationId":"activity_mcp_admin_api_activity_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/costs":{"get":{"summary":"Activity Costs","operationId":"activity_costs_admin_api_activity_costs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/rescore/{turn_id}":{"post":{"summary":"Activity Rescore","operationId":"activity_rescore_admin_api_activity_rescore__turn_id__post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"integer","title":"Turn Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/activity/report":{"get":{"summary":"Activity Report Preview","description":"The morning email for a day (default yesterday), as it would be sent.","operationId":"activity_report_preview_admin_api_activity_report_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/send-report":{"post":{"summary":"Activity Send Report","operationId":"activity_send_report_admin_api_activity_send_report_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/morning":{"get":{"summary":"Activity Morning Preview","description":"The 07:00 Melbourne email for an ICT day (default yesterday), as it would be sent.","operationId":"activity_morning_preview_admin_api_activity_morning_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/activity/send-morning":{"post":{"summary":"Activity Send Morning","description":"Send the owner a PREVIEW now (subject marked; does not count as the day's send).","operationId":"activity_send_morning_admin_api_activity_send_morning_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/owner/failed-steps.json":{"get":{"summary":"Failed Steps Json","operationId":"failed_steps_json_owner_failed_steps_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/home":{"get":{"summary":"Home","operationId":"home_admin_api_home_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/reveal-email":{"post":{"summary":"Reveal Email","description":"The full address behind a masked one. Logged in admin_audit, every time.","operationId":"reveal_email_admin_api_reveal_email_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/growth":{"get":{"summary":"Growth","description":"Funnel (signup -> verified -> first use -> 3+ active days -> paid) and the weekly\ncohort retention grid W0-W4 (services/admin_growth.py).","operationId":"growth_admin_api_growth_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/users":{"get":{"summary":"Users Index","operationId":"users_index_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/users/{user_id}":{"get":{"summary":"User Page","operationId":"user_page_admin_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/review":{"get":{"summary":"Review Page","operationId":"review_page_admin_review_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/audit":{"get":{"summary":"Audit Page","operationId":"audit_page_admin_audit_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/users/{user_id}":{"get":{"summary":"User Json","operationId":"user_json_admin_api_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/plan":{"post":{"summary":"User Plan","operationId":"user_plan_admin_api_users__user_id__plan_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/credit-pack":{"post":{"summary":"User Credit Pack","description":"Grant a pay-as-you-go pack by hand (owner 09/10: PayOS is not live yet, so a\nbank transfer or a comp is granted here). Body {\"pack\": \"extra_150\", \"note\"?}.\nAudited (`grant_credit_pack`); valid 12 months like a bought pack.","operationId":"user_credit_pack_admin_api_users__user_id__credit_pack_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/reset-windows":{"post":{"summary":"User Reset Windows","description":"WINDOWS V3 \"Đặt lại lượng dùng\" (owner 10/10): close the account's open 5-hour\nand weekly windows. Body {\"note\"?}. Paid packs untouched. Audited (`reset_windows`).","operationId":"user_reset_windows_admin_api_users__user_id__reset_windows_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/reset-bank":{"post":{"summary":"User Reset Bank","description":"Reset the rollover allowance bank (\"Đặt lại lượng dùng\"). Body {\"mode\": \"open\"\n(7 days' worth) | \"full\" (30 days' worth), \"note\"?}. Paid packs untouched.\nAudited (`reset_bank`) with the old and new balance.","operationId":"user_reset_bank_admin_api_users__user_id__reset_bank_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/trial":{"post":{"summary":"User Trial","operationId":"user_trial_admin_api_users__user_id__trial_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/trial-requests/{request_id}/{action}":{"post":{"summary":"Trial Request Action","description":"One click on a \"Xin dùng thử\" request (services/ai_access.py): `grant` gives Pro\nfor 14 days (admin_users.extend_trial, audited) or `dismiss` closes it, audited.","operationId":"trial_request_action_admin_api_trial_requests__request_id___action__post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"integer","title":"Request Id"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","title":"Action"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/tester":{"post":{"summary":"User Tester","description":"Grant or revoke tester (Ψ2's services/testers.py), audited.","operationId":"user_tester_admin_api_users__user_id__tester_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/resend-verification":{"post":{"summary":"User Resend","operationId":"user_resend_admin_api_users__user_id__resend_verification_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/suspend":{"post":{"summary":"User Suspend","operationId":"user_suspend_admin_api_users__user_id__suspend_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/note":{"post":{"summary":"User Note","operationId":"user_note_admin_api_users__user_id__note_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/review":{"get":{"summary":"Review Json","operationId":"review_json_admin_api_review_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/review/{turn_id}":{"post":{"summary":"Review Mark","operationId":"review_mark_admin_api_review__turn_id__post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"integer","title":"Turn Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/audit":{"get":{"summary":"Audit Json","operationId":"audit_json_admin_api_audit_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/export/{what}.csv":{"get":{"summary":"Export Csv","description":"users | costs | audit. Full emails (an export is the owner's own file); every\nexport is itself audited.","operationId":"export_csv_admin_export__what__csv_get","parameters":[{"name":"what","in":"path","required":true,"schema":{"type":"string","title":"What"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ai/access":{"get":{"summary":"Access","operationId":"access_api_ai_access_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/trial-request":{"post":{"summary":"Trial Request","operationId":"trial_request_api_ai_trial_request_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/owner/trial-grant":{"get":{"summary":"Trial Grant Page","operationId":"trial_grant_page_owner_trial_grant_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Trial Grant","operationId":"trial_grant_owner_trial_grant_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/payments/checkout":{"post":{"summary":"Checkout","description":"Start a purchase. Body: {\"plan\": \"premium_month\"}. Returns a QR / payment link.","operationId":"checkout_api_payments_checkout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/payments/status/{order_code}":{"get":{"summary":"Status","description":"Where a payment stands — and the fallback path when no webhook arrived.","operationId":"status_api_payments_status__order_code__get","parameters":[{"name":"order_code","in":"path","required":true,"schema":{"type":"integer","title":"Order Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/payments/history":{"get":{"summary":"History","description":"The customer's own receipts.","operationId":"history_api_payments_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/payments/webhook":{"post":{"summary":"Webhook","description":"PayOS telling us money arrived. Proves itself, then grants exactly once.\n\nAlways 200 on a verified callback, whatever we decided to do with it — including\n\"nothing, we already applied this\". PayOS retries anything else, and their retries\nare how a confirmation storm turns into a support problem.","operationId":"webhook_api_payments_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/payments/packs":{"get":{"summary":"Packs","description":"The extra-credit packs on sale, and whether checkout can take money yet.\n\nPublic: the price list is not a secret, and the app shows it on the refusal\nsheet before it knows whether the reader will buy.","operationId":"packs_api_payments_packs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/plans":{"get":{"summary":"Plan Terms","description":"The paid tiers' terms — price and the limits (WINDOWS V3, owner 10/10).\n\n`limits` is [\"5h\", \"week\"]: Anthropic-style rolling windows per user, a 5-hour one\nand a weekly one, each opened by the first message; no monthly or daily limit.\nEach row carries `window_5h` / `window_week` (internal credits), their US$ worth,\nand `limit_text` (the plan card's line, vi/en). `launch` is {\"active\": false}:\nthe launch label is retired.\n\nPublic, and read from services/plans.py like the pricing page, so the app's\nupgrade card states the same allowance the meter enforces instead of copy\nthat went stale when the plans changed.","operationId":"plan_terms_api_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/extra":{"get":{"summary":"Extra Get","description":"This account's extra-credit balance, nearest expiry and auto-use setting.","operationId":"extra_get_api_ai_extra_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/extra/auto":{"post":{"summary":"Extra Auto","description":"Body {\"enabled\": bool}: \"always use extra credits when my plan runs out\".\n\nDefault OFF. With it on, a turn past a plan cap is paid from extra credits\nwithout asking each time; with it off every such turn asks first.","operationId":"extra_auto_api_ai_extra_auto_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/balance":{"get":{"summary":"Ai Balance","description":"FREE APP, PAID AI: balance, packs, usage per day / chat, top-ups, spend limit.","operationId":"ai_balance_api_ai_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/payments/quote":{"get":{"summary":"Payments Quote","description":"What a custom top-up of ?usd= (or, Vietnamese, ?vnd=) costs (public; $2-$200 =\n50.000₫-5.000.000₫).","operationId":"payments_quote_api_payments_quote_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ai/spend-limit":{"post":{"summary":"Ai Spend Limit","description":"Body {\"usd\": 20} sets the monthly AI spend limit ($1-$1000); {\"usd\": null} clears it.","operationId":"ai_spend_limit_api_ai_spend_limit_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/billing/checkout/{plan}":{"post":{"summary":"Create Checkout","operationId":"create_checkout_api_billing_checkout__plan__post","parameters":[{"name":"plan","in":"path","required":true,"schema":{"type":"string","title":"Plan"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/portal":{"post":{"summary":"Customer Portal","operationId":"customer_portal_api_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/billing/webhook":{"post":{"summary":"Stripe Webhook","operationId":"stripe_webhook_api_billing_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/account":{"get":{"summary":"Paper Account","operationId":"paper_account_api_paper_account_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/vs-index":{"get":{"summary":"Paper Vs Index","description":"The account's return since it started against VN-Index over the same span.","operationId":"paper_vs_index_api_paper_vs_index_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/holdings":{"get":{"summary":"Paper Holdings","operationId":"paper_holdings_api_paper_holdings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/history":{"get":{"summary":"Paper History","operationId":"paper_history_api_paper_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/leaderboard":{"get":{"summary":"Paper Leaderboard","operationId":"paper_leaderboard_api_paper_leaderboard_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/trade":{"post":{"summary":"Paper Trade","operationId":"paper_trade_api_paper_trade_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/reset":{"post":{"summary":"Paper Reset","operationId":"paper_reset_api_paper_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/history/export.csv":{"get":{"summary":"Paper History Csv","operationId":"paper_history_csv_api_paper_history_export_csv_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/overview":{"get":{"summary":"Paper Overview","operationId":"paper_overview_api_paper_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/positions":{"get":{"summary":"Paper Positions","operationId":"paper_positions_api_paper_positions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/orders":{"get":{"summary":"Paper Orders","operationId":"paper_orders_api_paper_orders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/nav-series":{"get":{"summary":"Paper Nav Series","operationId":"paper_nav_series_api_paper_nav_series_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/ticket":{"get":{"summary":"Paper Ticket","operationId":"paper_ticket_api_paper_ticket_get","parameters":[{"name":"ticker","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ticker"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/paper/preview":{"post":{"summary":"Paper Preview","operationId":"paper_preview_api_paper_preview_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/order":{"post":{"summary":"Paper Order","description":"Place a VIRTUAL order: a marketable limit fills now at the market price, never\nworse than the limit (a market order at the last price), else rejected with the\nreason. Never a real order.","operationId":"paper_order_api_paper_order_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/cancel":{"post":{"summary":"Paper Cancel","description":"Cancel a waiting VIRTUAL order: {order_id} one order, or {ticker} every one on it.\nGives back the reserved cash / shares.","operationId":"paper_cancel_api_paper_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/paper/open-orders":{"get":{"summary":"Paper Open Orders","operationId":"paper_open_orders_api_paper_open_orders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/app/errors":{"post":{"summary":"App Errors","operationId":"app_errors_api_app_errors_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/voice/session":{"post":{"summary":"Voice Session","operationId":"voice_session_api_voice_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/voice/normalise":{"post":{"summary":"Voice Normalise","operationId":"voice_normalise_api_voice_normalise_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webpush/config":{"get":{"summary":"Config","operationId":"config_api_webpush_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webpush/subscribe":{"post":{"summary":"Subscribe","operationId":"subscribe_api_webpush_subscribe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webpush/unsubscribe":{"post":{"summary":"Unsubscribe","operationId":"unsubscribe_api_webpush_unsubscribe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webpush/devices/{sub_id}":{"post":{"summary":"Toggle","operationId":"toggle_api_webpush_devices__sub_id__post","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"integer","title":"Sub Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Remove","operationId":"remove_api_webpush_devices__sub_id__delete","parameters":[{"name":"sub_id","in":"path","required":true,"schema":{"type":"integer","title":"Sub Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/webpush/test":{"post":{"summary":"Test","description":"\"Gửi thử\": one banner to this browser (or every enabled one), proving the path.","operationId":"test_api_webpush_test_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/tips":{"get":{"summary":"Get Tips","operationId":"get_tips_api_tips_get","parameters":[{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"web","title":"Platform"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tips/event":{"post":{"summary":"Tip Event","operationId":"tip_event_api_tips_event_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/mcp/keys":{"get":{"summary":"Api List Keys","operationId":"api_list_keys_api_mcp_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Api Create Key","operationId":"api_create_key_api_mcp_keys_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/mcp/keys/{key_id}":{"delete":{"summary":"Api Revoke Key","operationId":"api_revoke_key_api_mcp_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_forgot_password_post_forgot_password_post":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"Body_forgot_password_post_forgot_password_post"},"Body_import_image_api_portfolio_import_image_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_import_image_api_portfolio_import_image_post"},"Body_signin_post_signin_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"remember_me":{"type":"string","title":"Remember Me","default":""}},"type":"object","required":["email","password"],"title":"Body_signin_post_signin_post"},"Body_signup_post_signup_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"confirm":{"type":"string","title":"Confirm"}},"type":"object","required":["email","password","confirm"],"title":"Body_signup_post_signup_post"},"Body_upload_api_chat_attachments_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_api_chat_attachments_post"},"Body_verify_code_post_verify_code_post":{"properties":{"email":{"type":"string","title":"Email"},"code":{"type":"string","title":"Code"}},"type":"object","required":["email","code"],"title":"Body_verify_code_post_verify_code_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}