{"openapi":"3.1.0","info":{"title":"hyreflow engine (local)","version":"0.1.0"},"paths":{"/api/v2/mcp/oauth/authorize":{"get":{"summary":"Mcp Authorize Get","operationId":"mcp_authorize_get_api_v2_mcp_oauth_authorize_get","parameters":[{"name":"response_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Type"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"redirect_uri","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Redirect Uri"}},{"name":"code_challenge","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge Method"}},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"resource","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resource"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Mcp Authorize Post","description":"Consent approval. No CSRF token, and that rests on ONE property of the cookie rather than on an\nomission: identity here comes from `hf_session`, which is set `samesite=\"lax\"` (see app.py), so a\ncross-site POST arrives without it and falls through to the 401 below. Client registration is open\n(RFC 7591), so an attacker can register their own redirect_uri — if that cookie is ever widened to\n`samesite=none`, this endpoint needs a real one-time token BEFORE that ships.","operationId":"mcp_authorize_post_api_v2_mcp_oauth_authorize_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/mcp/oauth/register":{"post":{"summary":"Mcp Register","operationId":"mcp_register_api_v2_mcp_oauth_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/mcp/oauth/token":{"post":{"summary":"Mcp Token","operationId":"mcp_token_api_v2_mcp_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/mcp/oauth/revoke":{"post":{"summary":"Mcp Revoke","operationId":"mcp_revoke_api_v2_mcp_oauth_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/openai-apps-challenge":{"get":{"summary":"Openai Apps Challenge","description":"Domain proof for the OpenAI plugin directory: the portal hands the publisher a token and fetches\nit back from this exact path on the MCP server's host, as bare text — not JSON, no wrapping. The token\ncomes from the environment so rotating it is a config change, not a deploy; unset, the path is a 404\nlike any other, so nothing is claimable by accident.","operationId":"openai_apps_challenge__well_known_openai_apps_challenge_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource/api/v2/mcp":{"get":{"summary":"Protected Resource Metadata","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_api_v2_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"summary":"Authorization Server Metadata","operationId":"authorization_server_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/mcp":{"get":{"summary":"Mcp Get","description":"No server-initiated SSE streams: a GET without a valid bearer gets the discovery 401; with one,\n405 tells the client this server is single-response only.","operationId":"mcp_get_api_v2_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Mcp Post","description":"Streamable-HTTP transport, single-response mode: each JSON-RPC POST gets one application/json\nresponse (spec-compliant; SSE streams are optional and none of our responses benefit from them).\nUnauthenticated requests get the RFC 6750 401 that drives OAuth discovery in every compliant\nclient.\n\nThe body is read here and EVERYTHING else runs in a worker thread. The dispatch below calls the\nengine's own sync route functions, which do blocking provider HTTP — a waterfall runs for tens of\nseconds. Awaiting that inline on the event loop stalls every other request in the process (one\nuvicorn worker serves the whole engine), including the container health check. Same discipline as\n`stripe_webhook`, the only other async route here.","operationId":"mcp_post_api_v2_mcp_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"summary":"Mcp Delete","description":"DELETE terminates an MCP SESSION, which is identified by `Mcp-Session-Id`. We never issue one —\nthis server is stateless, every request stands alone — so there is nothing here to terminate, and the\nspec's answer for a server without session termination is 405.\n\nIt must NOT revoke the OAuth grant. Clients send DELETE as routine shutdown cleanup, and treating\nthat as 'destroy my credentials' would force a full re-authorization on every restart. Revocation is\na deliberate act with its own endpoint: POST /api/v2/mcp/oauth/revoke (RFC 7009).","operationId":"mcp_delete_api_v2_mcp_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/":{"get":{"summary":"Root","description":"recruit.hyreflow.ai is the single product origin (engine owns the bare host, the web app owns the\nUI path prefixes via Traefik). The root belongs to the engine, so send humans to the dashboard.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/cli/version":{"get":{"summary":"Cli Version","description":"Self-update target for the thin client (`hyreflow update` checks this).","operationId":"cli_version_cli_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/recipes":{"get":{"summary":"Recipes","description":"Rich onboarding recipe cards for the platform Quickstart page. Public (pre-auth onboarding).","operationId":"recipes_api_v2_recipes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/versions":{"get":{"summary":"Versions","description":"Per-component content hashes the CLI diffs against to update only what changed. Public.","operationId":"versions_api_v2_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/cli/releases":{"get":{"summary":"Cli Releases","description":"Immutable release index for version pinning + rollback (issue #74). Public.\n\nReturns {latest, min_cli_version, releases:[{version, sha256, download_url}]}. Every entry's\nsha256 is the manifest's RELEASE sha — the immutable Release asset's hash, stable across\nAPI redeploys. The `latest` entry falls back to the live baked-pyz hash only when the manifest\nrecorded no sha for it (pre-release seed). Older entries' sha256 are used to verify a pinned\ndownload; `latest` is the live/rolling artifact, so the CLI does not hard-verify it.\n\n`download_url` is ALWAYS this engine's version-addressable path — never the manifest's recorded\n`download_url`. That field points at the private source repo's Release asset: echoing it out of a\npublic endpoint handed every caller a link that 404s for them and leaked the repo slug. The engine\nproxies the same asset server-side with its own token (see cli_pyz_versioned).","operationId":"cli_releases_api_v2_cli_releases_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/cli/install":{"get":{"summary":"Cli Install","description":"Public installer served for `curl -fsSL …/api/v2/cli/install | bash`. No auth (bootstrap).","operationId":"cli_install_cli_install_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v2/cli/install":{"get":{"summary":"Cli Install","description":"Public installer served for `curl -fsSL …/api/v2/cli/install | bash`. No auth (bootstrap).","operationId":"cli_install_api_v2_cli_install_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/brand/{name}":{"get":{"summary":"Brand Asset","description":"Brand assets the emails reference.\n\nServed by the ENGINE, not the web app, even though both answer on this host. The template that\npoints at the file lives here, so shipping the file here means the two can never skew: a web-side\nasset 404s in every email sent between the api deploy and the web deploy, and the recipient just\nsees a broken image. `/brand` is deliberately absent from web_code_paths so Traefik falls through.\nAllowlisted rather than path-joined — `name` is caller-supplied.","operationId":"brand_asset_brand__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/SKILL.md":{"get":{"summary":"Setup Skill","description":"Public setup skill — the URL a user hands their agent (\"set up Hyreflow from <this>\"). No auth:\nit runs before any account exists. Served as markdown so an agent that fetches it gets text, not a\ndownload. Honors a conditional request like the reference docs do — an agent that re-fetches this\nevery run (a repair pass, a retried setup) revalidates instead of re-reading the whole document.","operationId":"setup_skill_SKILL_md_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v2/auth/cli/start":{"post":{"summary":"Auth Cli Start","description":"CLI kicks off a claim → gets a claim_id + the approval/poll URLs. No auth (bootstrap).\nRate-limited per IP (anti credit-farming) when HYREFLOW_START_RATE_LIMIT > 0.","operationId":"auth_cli_start_api_v2_auth_cli_start_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/cli/claim/{claim_id}":{"get":{"summary":"Auth Cli Claim Page","description":"Browser entry (the CLI opens this). Redirect to the sign-in page on the same origin\ncarrying the cli_token. Sign-in → magic-link → finalize approves THIS claim in-process — no\nweb<->engine secret. (POST below still approves directly for the legacy dashboard path.)","operationId":"auth_cli_claim_page_api_v2_auth_cli_claim__claim_id__get","parameters":[{"name":"claim_id","in":"path","required":true,"schema":{"type":"string","title":"Claim Id"}},{"name":"ref","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Auth Cli Claim Approve","description":"Legacy ANONYMOUS approve (kept for dev / back-compat). Identity binding happens exclusively via\nthe magic-link verify endpoint, which approves in-process after proving email control — so this\npublic endpoint ignores any identity in the body (no claim forgery surface, no shared secret).","operationId":"auth_cli_claim_approve_api_v2_auth_cli_claim__claim_id__post","parameters":[{"name":"claim_id","in":"path","required":true,"schema":{"type":"string","title":"Claim Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/cli/approve":{"post":{"summary":"Auth Cli Approve Signed In","description":"Instant approve for an ALREADY signed-in browser: the bearer (hf_session) is\nthe identity proof, so a waiting CLI claim binds to its user without a second magic-link email.\nAnonymous/legacy bearers (no bound user) are refused — they prove nothing about an email.\n\n`invite_code` (optional) binds this CLI claim to the invited TEAM workspace — an explicit \"join my\nwaiting CLI into a team\" action, so it deliberately differs from the magic-link CLI leg\n(`signin.complete`), which binds the CLI to the user's HOME workspace (personal, or their team if\nthey own none). Without an invite, approve_claim ensures that home workspace. (Threading an\ninvite through the magic-link EMAIL round-trip is deferred until the invite UX exists: invites are\nsingle-use, and verify approves two claims, so blind forwarding would consume the invite on one and\nsplit the keys across workspaces.)","operationId":"auth_cli_approve_signed_in_api_v2_auth_cli_approve_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/auth/cli/poll/{claim_id}":{"get":{"summary":"Auth Cli Poll","description":"CLI polls here until status=approved, then receives its bearer token. No auth (bootstrap).","operationId":"auth_cli_poll_api_v2_auth_cli_poll__claim_id__get","parameters":[{"name":"claim_id","in":"path","required":true,"schema":{"type":"string","title":"Claim Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/logout":{"post":{"summary":"Auth Logout","description":"Revoke the caller's bearer token server-side (dashboard / CLI). Revoked by token hash, NOT by the\nresolved workspace — under an `X-Workspace-Id` switch that isn't the key's own workspace.","operationId":"auth_logout_api_v2_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/auth/magic-link":{"post":{"summary":"Auth Magic Link","description":"Issue a single-use magic sign-in link and email it. No auth (bootstrap).\n\n`cli_token` (the waiting CLI claim_id) threads the link's callbackURL so the click finalizes that\nclaim; absent ⇒ `next` (allowlisted, see `_safe_next`) or a plain dashboard sign-in. The engine\nverifies the token itself at /verify — no web<->engine secret. Delivery failures (incl. a missing\nRESEND_API_KEY without the explicit HYREFLOW_DEV_MAIL_LOG=1 dev opt-in) surface as 502.","operationId":"auth_magic_link_api_v2_auth_magic_link_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/magic-link/verify":{"post":{"summary":"Auth Magic Link Verify","description":"Verify a magic token (single-use) and bind its email to a user. Then either approve the waiting\nCLI claim in-process (kind=cli) or mint a fresh dashboard session (kind=session). The token is the\nproof of email control — this is what replaces the old X-Hyreflow-Internal handshake.\n\nIf the existing user is an unverified password account (legacy takeover target), we instead return\nkind='set_password' so the real owner can set a fresh password before being signed in.\n\nOn failure the 400's `detail` is a dict carrying a machine-readable `reason`\n({\"expired\",\"used\",\"unknown\",\"missing\"}) alongside the human `message`, so a caller can tell a\ngenuinely aged-out link apart from a double-click replay or a garbage token — FastAPI renders it as\n`{\"detail\": {...}}`.","operationId":"auth_magic_link_verify_api_v2_auth_magic_link_verify_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/magic-link/verify-code":{"post":{"summary":"Auth Magic Link Verify Code","description":"Verify the 6-digit code from the sign-in email — the code-first path that keeps an OAuth resume\nin the tab that started it. Binds identity exactly like /verify (same _finish_magic tail), so a\ncode and the link on its row spend each other.\n\nOn failure the 400's `detail` carries a machine-readable `reason` ({\"wrong\",\"expired\",\"locked\",\n\"unknown\",\"invalid\"}) the sign-in form maps to copy. The per-email guess cap lives in\nmagic.consume_code (checked before the compare, summed across resends); this route adds a\nrequest cap keyed by the normalized email, since browser requests share the BFF's source IP.","operationId":"auth_magic_link_verify_code_api_v2_auth_magic_link_verify_code_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/oauth/google/start":{"get":{"summary":"Auth Oauth Google Start","description":"Kick off Google sign-in: mint a single-use state (bound to any waiting CLI claim, the post-auth\ncallback, and the invite the sign-in started from) and 302 to Google's consent screen. 404 when\nGOOGLE_CLIENT_ID/SECRET aren't configured (UI hides the button).","operationId":"auth_oauth_google_start_api_v2_auth_oauth_google_start_get","parameters":[{"name":"cli_token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cli Token"}},{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}},{"name":"invite_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invite Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/oauth/google/callback":{"get":{"summary":"Auth Oauth Google Callback","description":"Google redirects here. Verify our state, exchange the code for the user's email, bind identity,\napprove any waiting CLI claim, and set the dashboard session cookie — then 302 to the finalize page\n(CLI connect) or the dashboard. Any failure bounces to /sign-in?error=connect.","operationId":"auth_oauth_google_callback_api_v2_auth_oauth_google_callback_get","parameters":[{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/integrations/{provider}/oauth/credentials":{"put":{"summary":"Integration Oauth Save Credentials","description":"Save the workspace's OWN OAuth app credentials for `provider`, registered by the workspace directly\nwith the vendor. The OVERRIDE path: connecting normally runs on Hyreflow's managed app and needs none\nof this (see integration_oauth.py's module docstring), so this is for a workspace that wants its own\naudit trail / rate-limit bucket. Overwrites any prior connection outright, including a live\nrefresh_token — see `save_credentials`.","operationId":"integration_oauth_save_credentials_api_v2_integrations__provider__oauth_credentials_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/integrations/{provider}/oauth/start":{"get":{"summary":"Integration Oauth Start","description":"Kick off a workspace's CRM/ATS connect: mint a single-use state bound to this workspace + provider\nand return the vendor's consent-screen URL for the browser to navigate to (JSON, not a redirect — the\ncaller is the web BFF's server-side fetch, not the browser itself; see infra/dokploy/web.tf's\n`web_code_paths` note on why this can't just be a raw 302 from a plain `<a href>`). 404 when the\nprovider has no OAuth config; 400 only when there is no app to connect WITH at all — neither a\nHyreflow-owned managed app in env nor credentials the workspace pasted itself.\n\nThe state is ALSO mirrored into a short-lived httponly cookie, scoped to the callback path. The\nworkspace/provider binding on the state row alone proves who requested the connect, not which\nBROWSER is completing it — without the cookie, an attacker could mint a state for their own\nworkspace via their own bearer token, then hand the resulting consent URL to a victim; the victim's\nJobAdder login would land the victim's tokens in the attacker's workspace (a login-CSRF /\nstate-injection variant on the OAuth dance). The callback below requires the cookie to match the\nstate it's given, which only the browser that actually started THIS flow will have.","operationId":"integration_oauth_start_api_v2_integrations__provider__oauth_start_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/integrations/{provider}/oauth/callback":{"get":{"summary":"Integration Oauth Callback","description":"The vendor redirects here. Verify our state (row AND the matching cookie set by /start — see its\ndocstring), exchange the code for tokens against the SAME app /start sent them to (the workspace's own\npasted credentials if it has them, else Hyreflow's managed app — `app_credentials`), and store the\nresult as the workspace's connected credential for `provider`. Any failure bounces to the dashboard's\nintegrations page with an error flag -- this is a redirect surface, never a JSON error.","operationId":"integration_oauth_callback_api_v2_integrations__provider__oauth_callback_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/password/signup":{"post":{"summary":"Auth Password Signup","description":"Start password sign-up OR reset: create a pending verification row and email a set-password link.\nThe user chooses their password only after proving email control, so an existing account is not a\nrefusal — the same link resets it. That also means the response never reveals whether the address is\nregistered. Returns {ok, pending, email}.","operationId":"auth_password_signup_api_v2_auth_password_signup_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/password/signup/complete":{"post":{"summary":"Auth Password Signup Complete","description":"Complete password sign-up: consume the pending verification token, set the password, and sign in.\nReturns the same {ok, kind, cli_token?, session, next_url?} shape as magic verify, with the\nhf_session cookie set by the engine.","operationId":"auth_password_signup_complete_api_v2_auth_password_signup_complete_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/auth/password/login":{"post":{"summary":"Auth Password Login","description":"Sign in with email/password. 401 on any mismatch (no user / no password set / wrong password / email\nnot verified — not distinguished, to avoid account enumeration). The engine sets the hf_session cookie.","operationId":"auth_password_login_api_v2_auth_password_login_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cli/hyreflow.pyz":{"get":{"summary":"Cli Pyz","description":"The distributable CLI (built into the image at dist/hyreflow.pyz). Public — installer fetches it.","operationId":"cli_pyz_cli_hyreflow_pyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/cli/v{version}/hyreflow.pyz":{"get":{"summary":"Cli Pyz Versioned","description":"Version-addressable CLI artifact (issue #74). The `latest` version serves the baked pyz directly\n(always available, even if GitHub is down). Any older version is streamed from its immutable GitHub\nRelease asset — PROXIED through the engine (not a 302) because the repo is private, so the public\ndownload URL would 404 without auth; the engine holds the token. If no token is configured, fall\nback to a 302 to the public download URL (works for a public repo). Unknown version → 404. Public.","operationId":"cli_pyz_versioned_cli_v_version__hyreflow_pyz_get","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string","title":"Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/npm/hyreflow":{"get":{"summary":"Npm Packument","description":"npm registry packument for the `hyreflow` package.\n\nUpstream's document verbatim except for one rewrite: every `dist.tarball` points back at THIS host,\nbecause a client using this registry is one that cannot reach npmjs.com. `shasum` and `integrity`\nare passed through untouched, so the bytes served from here still verify against what npm published.\n`dist-tags` rides along unchanged, so `npm i -g hyreflow` and `hyreflow@next` both resolve the same\nway they would upstream. Public.","operationId":"npm_packument_api_v2_npm_hyreflow_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/npm/hyreflow/-/hyreflow-{version}.tgz":{"get":{"summary":"Npm Tarball","description":"Serve a published tarball, proxied from upstream.\n\nThe version shape is validated BEFORE any upstream call so a bogus path can't turn this endpoint\ninto a request amplifier against the registry. Public.","operationId":"npm_tarball_api_v2_npm_hyreflow___hyreflow__version__tgz_get","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string","title":"Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/skills/bundle":{"get":{"summary":"Skills Bundle","description":"gzip tarball of every skill package under skills/ (hyreflow-recruit + companions), preserving\n`<name>/...` arcnames. The CLI installs each package into ~/.agents/skills and symlinks\n~/.claude/skills.\n\nINTENTIONALLY PUBLIC (no auth). Reviewed decision (TODO \"Public skill bundle\"): the bootstrap\n`curl … | bash` installer runs `skills install` *before* `auth login`, so there is no bearer at\nfetch time. The packages carry NO secrets — every adapter is a thin vendor-API client that reads\nits key from the server-side engine env (managed) or the client's own BYOK key; provider keys\nnever ship in the bundle. The exposed asset is the playbook/recipe *source*, accepted as public.\nIf this is ever reclassified as trade-secret IP, gate it behind a real client bearer and move\n`auth login` ahead of `skills install` in `_install_script`.\n\nOnly real packages ship — a directory is one when it has a SKILL.md. Anything else that ends up\nalongside them (an editor/agent dropping a dotdir in there, a stray scratch folder) would otherwise be\ntarred up and served to every user on a public route, which is how internal notes leak.","operationId":"skills_bundle_api_v2_skills_bundle_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/skills/index.json":{"get":{"summary":"Skills Wellknown Index","description":"Skill-discovery index for any agent harness, not just our own CLI — same public rationale as\nskills_bundle (no secrets ship in these packages; there's no bearer yet at discovery time).\n\nEvery entry is consumable BOTH ways, because harnesses split on this and the index can't know which\none is reading it:\n  * `files` + the per-file route — a consumer that GETs each file as text (Hermes does exactly this:\n    it reads `files`, defaults to [\"SKILL.md\"], and fetches `<index dir>/<name>/<relpath>`). Note it\n    derives that base from the INDEX URL and ignores any `base_url` we publish, so the per-file route\n    has to live directly beside this one.\n  * `url` + `digest` — one byte-verifiable archive, for a consumer that would rather take the whole\n    package in a single request than make ~70 of them.\nDropping either half silently breaks whichever consumer needed it — an archive-only index leaves\nHermes 404ing on SKILL.md, and it still LISTS fine (search only reads name+description), so the\nfailure shows up at install time, not discovery.\n\n`$schema` is what selects the parse in a consumer that knows both shapes, so it can't be added as\ndecoration: declaring it routes the reference installer down its single-artifact path, where only\n`name`/`type`/`url`/`digest` are read and the archive is expected to unpack from its root. `version`,\n`base_url`, `files`, `entrypoint` and `documents` ride along as extra keys for the consumers that use\nthem (they're ignored by the artifact path), and every skills_sha-keyed extra stays consistent with the\ndigest because both come off the same content hash.","operationId":"skills_wellknown_index__well_known_skills_index_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/skills/archives/{name}.tar.gz":{"get":{"summary":"Skills Wellknown Archive","description":"Serve one package's deterministic gzip tarball — see _skill_archive for what keeps this\nbyte-identical to the digest index.json advertises. Unknown/non-skill name (incl. any traversal\nshape, which can't even reach here — no `/` fits {name}) -> 404, never a path outside SKILL_ROOT.","operationId":"skills_wellknown_archive__well_known_skills_archives__name__tar_gz_get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/skills/{name}/{rel_path}":{"get":{"summary":"Skills Wellknown File","description":"One file from one package, as text — the half of the catalog a harness uses when it installs by\nfetching each file rather than an archive (see skills_wellknown_index for who does which).\n\nEverything served here is inside a package the index already lists publicly, and `{rel_path:path}`\naccepts slashes, so this is the one catalog route where a traversal is expressible. Two independent\ngates: the name must be a real package, and the resolved path must stay inside that package — a\n`..` climb, an absolute path, or a symlink pointing out all fail the second one.","operationId":"skills_wellknown_file__well_known_skills__name___rel_path__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"rel_path","in":"path","required":true,"schema":{"type":"string","title":"Rel Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/reference/onet/{name}":{"get":{"summary":"Onet Reference","description":"O*NET title/occupation reference docs, moved out of hyreflow-recruit's shipped package (see above).\nPublic — same doc, just no longer bundled into the skill archive. Big + static, so a conditional\nrequest 304s instead of re-sending 2MB.","operationId":"onet_reference_api_v2_reference_onet__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/session/start":{"post":{"summary":"Session Start","operationId":"session_start_session_start_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/session/update":{"post":{"summary":"Session Update","operationId":"session_update_session_update_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/session/status":{"post":{"summary":"Session Status","operationId":"session_status_session_status_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/session/output":{"post":{"summary":"Session Output","operationId":"session_output_session_output_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/session/{sid}/alert":{"post":{"summary":"Session Alert","description":"Raise (or clear, when `message` is null/empty) the Playground approval banner. The human answer\nis given in the CLI — this only signals 'check your terminal'.","operationId":"session_alert_session__sid__alert_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sid","in":"path","required":true,"schema":{"type":"string","title":"Sid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/session/{sid}/limit":{"post":{"summary":"Session Limit","description":"Set (or clear, when `usd` is null) the per-session dollar spend cap. Enforced pre-flight. The\nowner's workspace default (PUT /api/v2/billing/session-cap) is a member's ceiling: they may tighten a\nsession, never raise it past the default or clear it.","operationId":"session_limit_session__sid__limit_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sid","in":"path","required":true,"schema":{"type":"string","title":"Sid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/session/{sid}/end":{"post":{"summary":"Session End","description":"Close a session (idempotent). After this, new runs carrying the session id — whether in\nbody.session_id or the X-Hyreflow-Session-Id header — no longer accrue to it (ENG-240), so a stale\nstored session can't keep billing the spent pill / burning its cap after the live run is over.","operationId":"session_end_session__sid__end_post","security":[{"HTTPBearer":[]}],"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/v2/feedback":{"post":{"summary":"Feedback Create","description":"File product feedback → a Linear issue (engine-owned), backed by a DB row. The skill/CLI POST\nhere; identity + account details are resolved server-side so the issue is always debuggable.","operationId":"feedback_create_api_v2_feedback_post","security":[{"HTTPBearer":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Feedback List","description":"Every report this workspace has filed, newest first — the Support dashboard page. NEVER returns\nissue_url/issue_identifier (no vendor leakage); title is the report's first line, derived at read\ntime so the list and the stored message can never disagree.","operationId":"feedback_list_api_v2_feedback_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/session/{sid}/feedback":{"post":{"summary":"Session Feedback","description":"Store feedback (unified with the other two ingest paths — one `feedback` row, not a separate\nsession_feedback table) and file a Linear issue (best-effort) so the team can triage it.","operationId":"session_feedback_session__sid__feedback_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sid","in":"path","required":true,"schema":{"type":"string","title":"Sid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/support":{"post":{"summary":"Support Request","description":"Public docs/dashboard feedback-widget submission. Auth is optional — docs visitors aren't signed\nin, the dashboard is — and a signed-in `technical` report also unifies onto the `feedback` table\n(Support dashboard) and files an issue, same as the other two ingest paths. Always emails the\nsupport inbox first, unchanged; persist+file happen after and are best-effort, so a DB/Linear\nhiccup never turns an email that already sent into a 500 (and a resubmit — a duplicate email).","operationId":"support_request_api_v2_support_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/session/{sid}":{"get":{"summary":"Session Get","operationId":"session_get_session__sid__get","security":[{"HTTPBearer":[]}],"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/v2/datasets":{"post":{"summary":"Datasets Create","description":"Create a dataset from inline rows (CLI uploads a CSV by sending its rows here).","operationId":"datasets_create_api_v2_datasets_post","security":[{"HTTPBearer":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Datasets List","operationId":"datasets_list_api_v2_datasets_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/datasets/{ds_id}":{"get":{"summary":"Datasets Meta","operationId":"datasets_meta_api_v2_datasets__ds_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ds_id","in":"path","required":true,"schema":{"type":"string","title":"Ds Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Datasets Delete","operationId":"datasets_delete_api_v2_datasets__ds_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ds_id","in":"path","required":true,"schema":{"type":"string","title":"Ds Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/datasets/{ds_id}/rows":{"get":{"summary":"Datasets Rows","operationId":"datasets_rows_api_v2_datasets__ds_id__rows_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ds_id","in":"path","required":true,"schema":{"type":"string","title":"Ds Id"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"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/v2/datasets/{ds_id}/csv":{"get":{"summary":"Datasets Csv","operationId":"datasets_csv_api_v2_datasets__ds_id__csv_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ds_id","in":"path","required":true,"schema":{"type":"string","title":"Ds Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/enrich/runs/{run_id}":{"get":{"summary":"Enrich Run Status","description":"An enrich run's state — free; see `_enrich_run_state`. Decision batches (/shortlist) have their\nown route.","operationId":"enrich_run_status_enrich_runs__run_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/enrich/run":{"post":{"summary":"Enrich Run","description":"Run a dataset enrichment. Body: {input: ds_id (or input_rows: [...]), steps|specs|waterfalls,\nrange: \"START:END\", in_place: bool, output_name, dry_run, preview, estimate, session_id}.\n\nLoads rows from the input dataset, runs the steps over the `range` slice (rest passed through),\nwrites a NEW dataset (or in place), and returns a SUMMARY ONLY — never the rows.\n\nCost transparency: every run returns a `cost_estimate` in `_meta`. A run is blocked with 402 and a\n`checkout_url` (when payments are configured) only when the balance can't cover a single row — the\nfloor below which it can make no progress; a balance that covers a row but not the whole batch at the\nworst-case rate runs, and carries a `partial_budget` warning. Use `dry_run`/`preview` to inspect\npayloads without charging; use `estimate: true` for an estimate-only call.","operationId":"enrich_run_enrich_run_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workflows/apply":{"post":{"summary":"Workflows Apply","description":"Create/update a workflow from a full definition (lint-gated).","operationId":"workflows_apply_api_v2_workflows_apply_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workflows/call":{"post":{"summary":"Workflows Call","description":"Queue a workflow run. Body: {workflow|workflow_id|workflow_name|name, input|payload,\nmode: live|dry_run|smoke_test, source?}.","operationId":"workflows_call_api_v2_workflows_call_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workflows":{"get":{"summary":"Workflows List","operationId":"workflows_list_api_v2_workflows_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workflows/runs":{"get":{"summary":"Workflows Recent Runs","description":"Workspace-wide recent runs across every workflow (the index-page table).","operationId":"workflows_recent_runs_api_v2_workflows_runs_get","security":[{"HTTPBearer":[]}],"parameters":[{"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/v2/workflows/schema":{"get":{"summary":"Workflows Schema","operationId":"workflows_schema_api_v2_workflows_schema_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subject","in":"query","required":false,"schema":{"type":"string","default":"","title":"Subject"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workflows/lint":{"post":{"summary":"Workflows Lint Route","description":"Validate a workflow config without saving. Accepts a full definition or a bare {config, trigger}.","operationId":"workflows_lint_route_api_v2_workflows_lint_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workflows/{workflow_id}":{"get":{"summary":"Workflows Get","operationId":"workflows_get_api_v2_workflows__workflow_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Workflows Delete","operationId":"workflows_delete_api_v2_workflows__workflow_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workflows/{workflow_id}/enable":{"post":{"summary":"Workflows Enable","operationId":"workflows_enable_api_v2_workflows__workflow_id__enable_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workflows/{workflow_id}/disable":{"post":{"summary":"Workflows Disable","operationId":"workflows_disable_api_v2_workflows__workflow_id__disable_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workflows/{workflow_id}/runs":{"get":{"summary":"Workflows Runs","operationId":"workflows_runs_api_v2_workflows__workflow_id__runs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"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/v2/workflows/{workflow_id}/runs/{run_id}":{"get":{"summary":"Workflows Run Get","operationId":"workflows_run_get_api_v2_workflows__workflow_id__runs__run_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workflows/{workflow_id}/runs/{run_id}/cancel":{"post":{"summary":"Workflows Run Cancel","operationId":"workflows_run_cancel_api_v2_workflows__workflow_id__runs__run_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"path","required":true,"schema":{"type":"string","title":"Workflow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/webhooks/inbound/{webhook_id}":{"post":{"summary":"Webhook Inbound","description":"PUBLIC inbound webhook trigger — the 128-bit URL secret IS the auth (no bearer).\nFires the workflow's published revision with the POST body as `{{input.*}}`.","operationId":"webhook_inbound_api_v2_webhooks_inbound__webhook_id__post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/webhooks/provider/{provider}/{nonce}":{"post":{"summary":"Provider Webhook","description":"PUBLIC vendor callback for a managed async job: FullEnrich, Wiza or BetterContact calls it when the\njob is done. The per-job nonce in the URL IS the auth. None of these vendors signs its callbacks.\n\nA poke, never data: the body is never read, logged or stored. The job settles through the sweeper's\nown vendor re-poll and idempotent billing, woken now instead of at its next tick. So a forged or\nreplayed call can at most trigger one free poll, and only on a job's first callback\n(`provider_jobs.poke`). Every outcome answers the same 200, so the route says nothing about which\nnonces exist and gives a vendor no reason to retry.","operationId":"provider_webhook_api_v2_webhooks_provider__provider___nonce__post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"nonce","in":"path","required":true,"schema":{"type":"string","title":"Nonce"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/providers":{"get":{"summary":"Byok List","description":"Every BYOK-eligible provider + this workspace's connection status (masked key only). A signed-in\nmember also gets `personal` on each personal-key card: their own override, or null. A non-owner never\nsees the shared workspace credential's details (masked key, last use, health, OAuth client/region) —\nonly whether the integration is connected and working.","operationId":"byok_list_api_v2_providers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/providers/{provider}":{"put":{"summary":"Byok Set","description":"Store (and, by default, live-verify) a BYOK key for `provider`.\n\nFlow: validate eligibility → unless `skip_test`, probe the SUBMITTED key against the vendor; on a\nverifiable failure return 424 with the vendor's reason and do NOT persist → on success upsert the\nencrypted key and return the masked view. `test_only=1` probes without storing (the dashboard's\n'Test connection' button). A multi-field provider (`byok.extra_fields`) also requires each of its\nextra fields in the body (e.g. `{\"api_key\": \"...\", \"agency_slug\": \"...\"}` for Loxo), and any field\ndeclaring a `format` (e.g. SourceWhale's user_email) must match it or the save 400s before anything\nis probed or stored. Storing is owner-only; any member may `test_only`.","operationId":"byok_set_api_v2_providers__provider__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"test_only","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Test Only"}},{"name":"skip_test","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip Test"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Byok Delete","description":"Remove the workspace's BYOK key for `provider`. A resold provider falls back to hyreflow-managed\ncredits; a BYOK-only one (apollo, zoominfo, …) has no fallback and stops working until a key is\nconnected again, which below Essential is refused.","operationId":"byok_delete_api_v2_providers__provider__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/providers/{provider}/test":{"post":{"summary":"Byok Test","description":"Probe a key for `provider`: the inline `api_key` (+ extra fields) if given, else the stored\nWORKSPACE ones (owner only — a member never exercises the shared key). Does not persist.","operationId":"byok_test_api_v2_providers__provider__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/providers/{provider}/toggle":{"post":{"summary":"Byok Toggle","description":"Pause/resume a stored override without discarding the key (the dashboard toggle). Body:\n{\"enabled\": bool}. 404 if no key is stored for the provider.","operationId":"byok_toggle_api_v2_providers__provider__toggle_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/providers/{provider}/me":{"get":{"summary":"Byok User Get","description":"The CALLING member's own personal override for `provider` — masked, never the shared workspace\nkey. `{\"connected\": false}` (not 404) when the member hasn't stored one.","operationId":"byok_user_get_api_v2_providers__provider__me_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Byok User Set","description":"Store (and, by default, live-verify) the CALLING member's OWN key for `provider` — from the next\ncall this member makes on it, their key replaces the shared workspace key wholesale (api_key + any\nextra fields both come from this row). Same validation as `byok_set`: required extra fields, format\ncheck, length caps, live probe (424 on a rejected key) unless `skip_test=1`.","operationId":"byok_user_set_api_v2_providers__provider__me_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"skip_test","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip Test"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Byok User Delete","description":"Remove the CALLING member's own key for `provider` (the shared workspace key, if any, is\nuntouched). An \"optional\" tool falls back to the workspace key, a \"required\" one (Spott) blocks.","operationId":"byok_user_delete_api_v2_providers__provider__me_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tools":{"get":{"summary":"List Tools","operationId":"list_tools_tools_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filter"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tools/search":{"get":{"summary":"Tools Search","description":"Ranked search over every flat tool name + summary (the entry point for picking a tool by intent,\ne.g. `?q=search software engineers`). Returns flat names you can `tools execute` directly.","operationId":"tools_search_tools_search_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tools/{tool}":{"get":{"summary":"Get Tool","operationId":"get_tool_tools__tool__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tool","in":"path","required":true,"schema":{"type":"string","title":"Tool"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tools/{tool}/{method}":{"get":{"summary":"Tools Get","description":"The `tools get` view — shows the expected payload (inputs) + cost + usage.","operationId":"tools_get_tools__tool___method__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tool","in":"path","required":true,"schema":{"type":"string","title":"Tool"}},{"name":"method","in":"path","required":true,"schema":{"type":"string","title":"Method"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Tools Execute","description":"Resolve key → run the adapter → meter → JSON. `dry_run` previews without calling/charging.\n\nBody may be a flat ``{\"payload\": {...}}`` (mapped to args/kwargs from the method's param\nkinds — the ergonomic path) and/or explicit ``{\"args\": [...], \"kwargs\": {...}}`` (escape hatch).","operationId":"tools_execute_tools__tool___method__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tool","in":"path","required":true,"schema":{"type":"string","title":"Tool"}},{"name":"method","in":"path","required":true,"schema":{"type":"string","title":"Method"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tools/{flat}":{"post":{"summary":"Tools Execute Flat","description":"Execute a flat tool name (e.g. `apollo_search_people`) — resolves to <tool>.<method>\nthen runs it. (Two-segment `/tools/{tool}/{method}` is the canonical form.)\n\nAn enrichment waterfall takes EITHER `payload` (one row) OR `rows` (up to BULK_MAX_ROWS rows, walked\nstage-major so a bulk-capable provider sees them as one job — see `enrich`). The two are exclusive,\nand `rows` has no meaning for a search waterfall.","operationId":"tools_execute_flat_tools__flat__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flat","in":"path","required":true,"schema":{"type":"string","title":"Flat"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/balance":{"get":{"summary":"Billing Balance","operationId":"billing_balance_billing_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/plans":{"get":{"summary":"Billing Plans","description":"PUBLIC — the plan table the pricing page and the CLI upsell render. No auth: these are list\nprices, and the page is read before anyone has an account. Prices are resolved live from\nreference/pricing-tiers.json, so this can never disagree with what checkout charges.","operationId":"billing_plans_api_v2_billing_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/admin/whoami":{"get":{"summary":"Admin Whoami","description":"The cheapest possible internal-access check: the gate IS the handler, so there is no body to\nget wrong and nothing to keep in sync.\n\nExists for web BFF routes that serve their OWN data instead of relaying to us — /api/ops/analytics\nreads the self-hosted analytics service directly, so it never touches an endpoint of ours and would\notherwise have no gate at all. Re-implementing require_internal in TypeScript is not an option: it\nrequires email_verified (see its docstring for the takeover class that depends on it) and\n/api/v2/me deliberately does not expose that field, so a client-side copy would admit unverified\ninternal-domain signups and would silently drop the admin-token branch.\n\nPrefer this over calling /api/v2/me for an auth check — that one also reads the workspace, the\nuser's full workspace list and the ledger balance, which is several Neon round trips on what the\ncaller polls every 30 seconds.","operationId":"admin_whoami_api_v2_admin_whoami_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/providers":{"get":{"summary":"Admin Provider Overview","description":"Internal provider-cost dashboard: per vendor, the latest polled API-credit balance, trailing\nvendor SPEND (what Hyreflow paid), and a status (ok | low_balance | renewal_due). Internal-gated —\nthis is cross-tenant ops data, not a workspace view. See provider_cost.overview().","operationId":"admin_provider_overview_api_v2_admin_providers_get","security":[{"HTTPBearer":[]}],"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/v2/admin/providers/poll":{"post":{"summary":"Admin Provider Poll","description":"Trigger a one-shot balance poll across auto_poll vendors (the live-seed / on-demand refresh).","operationId":"admin_provider_poll_api_v2_admin_providers_poll_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/providers/accounts/{account_id}":{"patch":{"summary":"Admin Provider Account Update","description":"Ops self-serve edit of one provider account — alert config (period_quota, the 50/90/100% depletion\nbaseline, and low_balance_threshold, both denominated in that provider's balance_unit) and the\nsubscription terms the Economics tab is computed from (subscription_tier / subscription_cost /\nsubscription_period / credits_included). renews_at is a plain date. Any field may be null to clear it.\nReplaces hand-written SQL for this.","operationId":"admin_provider_account_update_api_v2_admin_providers_accounts__account_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"string","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/users":{"get":{"summary":"Admin Users Overview","description":"Headline user counts (total + last-30-day signups).","operationId":"admin_users_overview_api_v2_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/users/signups":{"get":{"summary":"Admin Users Signups","description":"Signup counts per day/week over the trailing window (for the growth chart).","operationId":"admin_users_signups_api_v2_admin_users_signups_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":90,"title":"Days"}},{"name":"bucket","in":"query","required":false,"schema":{"type":"string","default":"day","title":"Bucket"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/users/{user_id}/cost":{"get":{"summary":"Admin User Cost","description":"One user's spend drilldown (identity + per-provider totals + last 50 runs). 404 if unknown.","operationId":"admin_user_cost_api_v2_admin_users__user_id__cost_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}},{"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/v2/admin/usage":{"get":{"summary":"Admin Usage","description":"Cross-tenant (tool, method) usage ranked by call volume, optionally scoped to workspaces.\n\n`workspace_ids` is a comma-separated list (the cockpit's Usage filter sends the ids it resolved\nfrom the workspaces overview). A present-but-EMPTY value is a 400, never a silent fall-through:\nusage_ranking treats a falsy list as \"no filter\", so accepting `?workspace_ids=` would serve every\ntenant's numbers under a filter the operator can see applied. No upper bound on the count — a 4xx\nthere would reach the cockpit as an indistinguishable-from-loading null, so the ceiling is URL\nlength and the client owns it.","operationId":"admin_usage_api_v2_admin_usage_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"workspace_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace Ids"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/cost":{"get":{"summary":"Admin Cost","description":"Per (workspace, provider) vendor cost breakdown, highest cost first.","operationId":"admin_cost_api_v2_admin_cost_get","security":[{"HTTPBearer":[]}],"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/v2/admin/kpis":{"get":{"summary":"Admin Kpis","description":"Headline business KPIs for the cockpit: gross profit, Stripe revenue, credits bought/used.","operationId":"admin_kpis_api_v2_admin_kpis_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/economics":{"get":{"summary":"Admin Economics","description":"Unit-economics cockpit: cash P&L, plan tiers, provider subscriptions, per-endpoint margin, reality.","operationId":"admin_economics_api_v2_admin_economics_get","security":[{"HTTPBearer":[]}],"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/v2/admin/pricing/endpoints":{"post":{"summary":"Admin Pricing Endpoints Propose","description":"Ops self-serve price change: `{\"changes\": [{\"provider\", \"method\", \"credits\"?, \"vendor_cost_usd\"?}, …]}`\nopens ONE PR that edits those methods in reference/pricing-tiers.json — the ONE place pricing is\nauthored. Nothing changes until a human merges it and the merge deploys; the cockpit keeps showing the\ncommitted price meanwhile.","operationId":"admin_pricing_endpoints_propose_api_v2_admin_pricing_endpoints_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/providers/{provider}/burn-rate":{"get":{"summary":"Admin Provider Burn Rate","description":"Daily average polled balance for one provider (the burn-rate chart).","operationId":"admin_provider_burn_rate_api_v2_admin_providers__provider__burn_rate_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"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/v2/admin/workspaces":{"get":{"summary":"Admin Workspaces","description":"Every workspace: owner, member count, granted credits, spend, remaining balance.","operationId":"admin_workspaces_api_v2_admin_workspaces_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/workspaces/{workspace_id}":{"get":{"summary":"Admin Workspace Detail","description":"One workspace's members + per-provider cost (the expandable drilldown).","operationId":"admin_workspace_detail_api_v2_admin_workspaces__workspace_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}},{"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/v2/admin/workspaces/{workspace_id}/grant":{"post":{"summary":"Admin Workspace Grant","description":"Ops-cockpit credit grant: bump the workspace's granted_credits. Positive, capped amounts only —\nlogged as a free `admin_grant` in credit_purchases (so the P&L books its cost as goodwill), and the\ngrantor + amount land in the server log.","operationId":"admin_workspace_grant_api_v2_admin_workspaces__workspace_id__grant_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/linkedin-posts":{"get":{"summary":"Admin Linkedin Posts","description":"LinkedIn reward posts awaiting review, oldest first.","operationId":"admin_linkedin_posts_api_v2_admin_linkedin_posts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/admin/linkedin-posts/{workspace_id}":{"post":{"summary":"Admin Linkedin Post Review","description":"Approve (grants the reward credits, once) or reject a pending post.\nBody: `{\"approve\": bool, \"post_url\": \"<the link that was checked>\"}`.","operationId":"admin_linkedin_post_review_api_v2_admin_linkedin_posts__workspace_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/photos/{token}":{"post":{"summary":"Photos Apply","description":"Body: the filled page (HTML). Returns it with each photo link replaced by its embedded image.","operationId":"photos_apply_api_v2_photos__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/v2/admin/pages":{"post":{"summary":"Admin Page Create","description":"Body: `{\"title\", \"html\", \"ttl_days\"?, \"recipient\"?, \"force\"?}`. `ttl_days` defaults to 30 (`null` =\nnever expires). `recipient` is `{job_url, recruiter_linkedin, recruiter_email, company_domain}`, all\noptional strings. A live page for the same recruiter + job answers 409 with `existing` unless `force`.\nReturns {slug, url, expires_at}.","operationId":"admin_page_create_api_v2_admin_pages_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Admin Page List","description":"Published pages, newest first, with status (live, expired or revoked). Opens are in PostHog\n(`shared_page_viewed`), not here.","operationId":"admin_page_list_api_v2_admin_pages_get","security":[{"HTTPBearer":[]}],"parameters":[{"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/v2/admin/pages/{slug}":{"delete":{"summary":"Admin Page Revoke","description":"Take a page down now. 404 when it is unknown or already revoked.","operationId":"admin_page_revoke_api_v2_admin_pages__slug__delete","security":[{"HTTPBearer":[]}],"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/v2/admin/workspaces/{workspace_id}/beta":{"post":{"summary":"Admin Workspace Beta","description":"Arm or clear this workspace's plan-gate exemption (`workspaces.beta`, 0039).\n\nBody: `{\"beta\": true | false}`. Armed, the workspace may buy credits, connect its own key for a\nprovider we resell while on Free — it is how a\npre-plans workspace keeps working. It is not a plan and grants no credits.\n\nClearing it ALSO pauses the workspace's stored keys for resold providers, once, here: the BYOK\nguard only refuses a new connection, so without this an ex-beta workspace would keep using its\nown managed-data keys forever. The paused providers come back in the response and the audit row.\nArming it again does NOT resume them — the workspace resumes its own credential.\n\nNote the UPDATE itself is the existence check (0 rows ⇒ 404), so there's no get-then-set race, and\nthe flag is written BEFORE the pause: a crash in between leaves the flag cleared with some keys\nlive, and re-POSTing the same body finishes the job (the flag write is idempotent).","operationId":"admin_workspace_beta_api_v2_admin_workspaces__workspace_id__beta_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/workspaces/{workspace_id}/telemetry":{"post":{"summary":"Admin Workspace Telemetry","description":"Pin this workspace's run-trace recording server-side, regardless of the caller's local consent.\n\nBody: `{\"mode\": \"on\" | \"off\" | \"default\"}` — `default` clears the override (sets it back to NULL) and\nhands the decision to the `X-Hyreflow-Telemetry` header (client consent) — which records unless that\nclient opted out. It is also where an unpinned workspace already sits, since the column has no default.\n`on` deliberately overrides a user's local opt-out, so the actor + mode are written to `security_events` as the audit\ntrail. Read the current value from the workspaces overview row or the drilldown\n(`GET /api/v2/admin/workspaces[/{id}]`); the ops cockpit's Workspaces tab renders it as an On/Off/Client\ncontrol per row.\n\nNote the UPDATE itself is the existence check (0 rows ⇒ 404), so there's no get-then-set race.","operationId":"admin_workspace_telemetry_api_v2_admin_workspaces__workspace_id__telemetry_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/workspaces/{workspace_id}/spending-cap":{"post":{"summary":"Admin Workspace Spending Cap","description":"On-call lever for a runaway spend: set or clear this workspace's spending cap without waiting for\nthe customer. Body `{\"cap\": <credits>}` to set, `{\"cap\": null}` to clear.\n\nThe cap is enforced against the trailing 30 days of charged credits, not the calendar month and not\nthe lifetime total, so the response returns `spent_30d` next to it: a cap below that figure stops the\nworkspace on its very next call. Sometimes that is exactly the intent — pick the number knowing which\none you are doing.\n\nThis overrides a setting the customer owns, so the actor and value go to `security_events`. The UPDATE\nis its own existence check (0 rows ⇒ 404), so there is no get-then-set race with the user-side route.","operationId":"admin_workspace_spending_cap_api_v2_admin_workspaces__workspace_id__spending_cap_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/workspaces/{workspace_id}/subscription":{"post":{"summary":"Admin Workspace Subscription","description":"Assign a plan by hand — Enterprise and any other sales-agreed deal. No Stripe object is created,\nso `stripe_subscription_id` stays NULL and nothing is ever charged for it: the terms in the body ARE\nthe deal. `next_grant_at = now()` so the first monthly grant lands on the next tick.\n\nBody: `{\"plan_id\", \"monthly_credits\", \"seat_limit\"?: null, \"cadence\"?: \"monthly\"|\"annual\"}` —\nevery plan includes unlimited users. The actor + terms are audited in `security_events`.","operationId":"admin_workspace_subscription_api_v2_admin_workspaces__workspace_id__subscription_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Admin Workspace Subscription Clear","description":"End an ops-assigned plan: the workspace reads as Free from the next request and stops being\ngranted. Idempotent — clearing a workspace that has no plan is a no-op, not a 404.\n\n`cap_grace_until` is cleared with it: the grant tick filters on status, so a row left mid-grace\nwould keep its untrimmed plan credits for good.","operationId":"admin_workspace_subscription_clear_api_v2_admin_workspaces__workspace_id__subscription_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/admin/sessions":{"get":{"summary":"Admin Sessions","description":"Cross-tenant live-session list for the ops cockpit: each session with its owning user email,\naggregated spend + run count, and the per-session cap. active=1 → only sessions still running.","operationId":"admin_sessions_api_v2_admin_sessions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"active","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Active"}},{"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/v2/admin/sessions/{sid}/end":{"post":{"summary":"Admin Session End","description":"Force-end ANY workspace's session (ops kill-switch). Idempotent — `ended` is True only when this\ncall flipped it from active.","operationId":"admin_session_end_api_v2_admin_sessions__sid__end_post","security":[{"HTTPBearer":[]}],"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/v2/admin/monitoring":{"get":{"summary":"Admin Monitoring","description":"Real-time-feeling ops Monitoring tab: latest alerts, worker heartbeat status, and per-provider\nerror rates (1h/15m). Polled every 5s by the UI — see ops_checks.monitoring_overview().","operationId":"admin_monitoring_api_v2_admin_monitoring_get","security":[{"HTTPBearer":[]}],"parameters":[{"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/v2/admin/monitoring/series":{"get":{"summary":"Admin Monitoring Series","description":"Time-bucketed series behind the Monitoring dashboard's charts (traffic/spend/alerts/by-provider) +\nstat-card numbers. window is hours, clamped 1..168 — see ops_checks.monitoring_series().","operationId":"admin_monitoring_series_api_v2_admin_monitoring_series_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"integer","default":24,"title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/billing/overview":{"get":{"summary":"Billing Overview","description":"Everything the billing page renders in one shot: balance, 30-day spend + cap, usage series +\nbreakdown, recent ledger, and whether Stripe is wired.","operationId":"billing_overview_api_v2_billing_overview_get","security":[{"HTTPBearer":[]}],"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/v2/billing/ledger.csv":{"get":{"summary":"Billing Ledger Csv","description":"Stream the full ledger as CSV (same incremental-yield pattern as the dataset export).","operationId":"billing_ledger_csv_api_v2_billing_ledger_csv_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/checkout":{"post":{"summary":"Billing Checkout","description":"Create a Stripe Checkout Session for a credit top-up. Returns {url, session_id} to redirect to.","operationId":"billing_checkout_api_v2_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/subscription/checkout":{"post":{"summary":"Billing Subscription Checkout","description":"Create a `mode=subscription` Checkout Session for a plan. Returns {url} to redirect to.","operationId":"billing_subscription_checkout_api_v2_billing_subscription_checkout_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/subscription/change":{"post":{"summary":"Billing Subscription Change","description":"Move an existing subscription to another plan or cadence.\n\nAn UPGRADE applies immediately and RESTARTS the billing period: the customer is charged the full new\nplan price today, and the next renewal follows the new monthly or annual cadence (ENG-686). The sold\nterms are NOT written here: the first webhook to observe the paid restart (`customer.subscription.updated` or\n`invoice.paid`) writes the terms and grants the FULL new monthly allotment on top of the customer's\nexisting balance. The later webhook sees the new terms and grants nothing again. Writing terms here\nwould make that diff see no change and the customer would pay for the new plan without its credits.\n\nA DOWNGRADE is only PARKED. `proration_behavior=none` would not defer it: that flag skips the\nproration credit while Stripe swaps the price immediately, dropping the customer's credits\nthe moment they click. The pending columns hold it until the next `invoice.paid`.","operationId":"billing_subscription_change_api_v2_billing_subscription_change_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/subscription/cancel":{"post":{"summary":"Billing Subscription Cancel","description":"Cancel at the end of the paid period, or resume (`{\"resume\": true}`) a cancellation not yet due.\n\nNot an immediate cancel: the customer paid through the end of the period and keeps the plan — and its\nmonthly grants — until Stripe fires `.deleted`.","operationId":"billing_subscription_cancel_api_v2_billing_subscription_cancel_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/portal":{"post":{"summary":"Billing Portal","description":"A hosted billing-portal session: invoices, receipts, the card on file, and plan switching.","operationId":"billing_portal_api_v2_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/checkout/resume/{session_id}":{"get":{"summary":"Billing Checkout Resume","description":"PUBLIC — the abandoned-checkout email's button (ENG-666). Mail never links a foreign domain\n(engine.md), so it lands here and goes back into the Stripe session while it is still open, else to\nthe billing page. The redirect target is only ever Stripe's own session url. Per-IP capped: each hit\nis a Stripe call, and the Stripe rate limit is shared with real checkouts.","operationId":"billing_checkout_resume_api_v2_billing_checkout_resume__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/v2/email/unsubscribe":{"get":{"summary":"Email Unsubscribe Confirm","description":"PUBLIC — gated by the HMAC in `t` (email.unsubscribe_url). A GET only shows the button: mail\nscanners prefetch every link, so opting out on GET would unsubscribe people who never clicked.","operationId":"email_unsubscribe_confirm_api_v2_email_unsubscribe_get","parameters":[{"name":"u","in":"query","required":false,"schema":{"type":"string","default":"","title":"U"}},{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Email Unsubscribe","description":"PUBLIC — the form above, and the RFC 8058 one-click target mail clients POST to. Idempotent.","operationId":"email_unsubscribe_api_v2_email_unsubscribe_post","parameters":[{"name":"u","in":"query","required":false,"schema":{"type":"string","default":"","title":"U"}},{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/webhooks/calendly":{"post":{"summary":"Calendly Webhook","description":"PUBLIC — gated only by the `Calendly-Webhook-Signature` HMAC (and a per-IP rate limit). On\n`invitee.created` (a meeting was booked) post the Slack card; every other event is acknowledged and\nignored so Calendly doesn't retry it. The Slack post is fail-soft, so a Slack outage still returns 200\n— a 5xx would make Calendly redeliver, and the notification isn't worth a duplicate later.","operationId":"calendly_webhook_api_v2_webhooks_calendly_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/billing/stripe/webhook":{"post":{"summary":"Stripe Webhook","description":"PUBLIC — gated only by signature + event dedup (and a per-IP rate limit). On a paid checkout, grant\nthe credits carried in server-set metadata (never trust client-controlled fields); on a refund or\ndispute, claw those credits back. Every branch dedups + mutates the ledger in a single transaction so\na crash can't half-apply.","operationId":"stripe_webhook_api_v2_billing_stripe_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v2/billing/spending-cap":{"put":{"summary":"Billing Set Cap","operationId":"billing_set_cap_api_v2_billing_spending_cap_put","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"summary":"Billing Clear Cap","operationId":"billing_clear_cap_api_v2_billing_spending_cap_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/session-cap":{"put":{"summary":"Billing Set Session Cap","description":"Set the workspace's DEFAULT per-session dollar cap. Inherited by sessions created after this call —\na live session keeps its own limit, and any session can override or clear it via /session/{sid}/limit.","operationId":"billing_set_session_cap_api_v2_billing_session_cap_put","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"summary":"Billing Clear Session Cap","operationId":"billing_clear_session_cap_api_v2_billing_session_cap_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/auto-top-up":{"get":{"summary":"Billing Get Auto Top Up","operationId":"billing_get_auto_top_up_api_v2_billing_auto_top_up_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"put":{"summary":"Billing Set Auto Top Up","operationId":"billing_set_auto_top_up_api_v2_billing_auto_top_up_put","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/payment-methods/setup-intent":{"post":{"summary":"Billing Pm Setup Intent","description":"Create a Stripe SetupIntent so the browser can save a card off-session via Stripe Elements.\nReturns {client_secret}. The saved card is recorded server-side on the setup_intent.succeeded webhook.","operationId":"billing_pm_setup_intent_api_v2_billing_payment_methods_setup_intent_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/payment-methods":{"get":{"summary":"Billing Pm List","operationId":"billing_pm_list_api_v2_billing_payment_methods_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/payment-methods/{pm_id}":{"delete":{"summary":"Billing Pm Delete","description":"Remove a saved card: detach it at Stripe and prune it from this workspace's auto-top-up order.","operationId":"billing_pm_delete_api_v2_billing_payment_methods__pm_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pm_id","in":"path","required":true,"schema":{"type":"string","title":"Pm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/billing/promo":{"post":{"summary":"Billing Promo","description":"Redeem a promo code → grant credits. Failure returns 400 with {reason} (the web reads .reason).","operationId":"billing_promo_api_v2_billing_promo_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/billing/linkedin-post":{"post":{"summary":"Billing Linkedin Post","description":"Submit a LinkedIn post that tags Hyreflow for the Free-plan reward. Ops reviews it before any\ncredits land. Failure returns 400 with {reason}: invalid_url | not_free | already_approved |\nduplicate_url.","operationId":"billing_linkedin_post_api_v2_billing_linkedin_post_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/keys":{"get":{"summary":"Keys List","description":"List the workspace's PLATFORM keys (metadata only — never the secret/hash). Local CLI keys\n(scope='local') live only in the machine's .env and are intentionally never listed here.","operationId":"keys_list_api_v2_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Keys Create","description":"Mint a new PLATFORM API key for the caller's workspace. The plaintext `key` is returned ONCE.\nOptional `ttl_days` sets an expiry; omit for a never-expiring key.","operationId":"keys_create_api_v2_keys_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/keys/default":{"post":{"summary":"Keys Default","description":"Get-or-create the workspace's default PLATFORM key. On first call it's minted and the plaintext\n`key` is returned ONCE (`created: true`). If it already exists, returns `{created: false}` with no\nsecret (hash-only at rest; never re-shown).","operationId":"keys_default_api_v2_keys_default_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/keys/{key_id}":{"delete":{"summary":"Keys Delete","operationId":"keys_delete_api_v2_keys__key_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/keys/{key_id}/rotate":{"post":{"summary":"Keys Rotate","description":"Revoke a key and mint a replacement. The new plaintext `key` is returned ONCE.","operationId":"keys_rotate_api_v2_keys__key_id__rotate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/me":{"get":{"summary":"Me","description":"Caller identity for `hyreflow auth status`: active workspace + user + balance + the user's\nfull list of workspaces. Owner-less implicit workspaces (dev/CI + anonymous) are hidden.","operationId":"me_api_v2_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/onboarding":{"post":{"summary":"Onboarding Set","description":"Record an answer from the first-run setup page (web console).\nEvery body field is optional but at least one is required: `first_name`, `last_name` (the person's\nname, captured as two parts), `job_role` (their current role, from a fixed dropdown),\n`referral_source` (where they heard about us, from a fixed dropdown), `crm` / `sequencer` (the tool\nthey use: a catalog provider id, \"other\", or \"none\", \"\" clears it), `crm_other` / `sequencer_other`\n(free text typed under \"Other\", only meaningful alongside `crm`/`sequencer` == \"other\", \"\" clears it),\n`website` (the account's site), `locale` (a short language code), `workspace_name` (renames the\ncaller's active workspace, same rules as `PATCH /api/v2/workspaces/{id}`), `llm` (which client they\nuse), `outcome` (how the flow ended). Passing only some fields leaves the others untouched -- e.g. an\n`outcome`-only call never clobbers a previously recorded `first_name`/`last_name`/`job_role`/`llm`/\n`website`/`locale`. `outcome` stamps `onboarding_completed_at` the first time only (first completion\nwins, never moved by a later call). `first_name`/`last_name`/`job_role`/`referral_source`/`crm`/\n`sequencer`/`crm_other`/`sequencer_other`/`llm`/`outcome` belong to the PERSON (`users`);\n`website`/`locale`/`workspace_name` are a property of the ACCOUNT (the caller's active `workspace`) -- see 0056's\nmigration comment for why. Returns the resulting state, the same shape surfaced at `/api/v2/me`'s\n`onboarding` block.","operationId":"onboarding_set_api_v2_onboarding_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspace":{"get":{"summary":"Workspace Info","description":"The caller's ACTIVE workspace (name, members, balance). `workspace_id` is null for owner-less\nimplicit accounts (dev/CI + anonymous) — not surfaced as real workspaces.","operationId":"workspace_info_api_v2_workspace_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspaces":{"get":{"summary":"Workspaces List","description":"Every workspace the caller's user owns or belongs to (the M:N view). Same projected shape as\n`/api/v2/me` (id/name/slug/role) — the slug is what `hyreflow workspace switch` takes. Carries the\ncaller's `user` too so `workspace list` can name the signed-in account without a second call.","operationId":"workspaces_list_api_v2_workspaces_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Workspaces Create","description":"Create an ADDITIONAL workspace owned by the caller (a user may own many). Enforces the 10-workspace\ncap and validates the name. Starts at 0 credits — unless the caller never had the once-per-identity\nwelcome credits (an invitee, who gets no personal workspace at sign-in), in which case they land here.","operationId":"workspaces_create_api_v2_workspaces_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspaces/{workspace_id}":{"delete":{"summary":"Workspace Delete","description":"Owner-only delete (ENG-700): schedules the workspace for deletion in workspaces.DELETE_DAYS. It keeps\nworking until then; the ops loop then hard-deletes it (cascade, plus the owner's account if it was\ntheir last workspace). 409 while a paid plan will renew.","operationId":"workspace_delete_api_v2_workspaces__workspace_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Workspace Rename","description":"Rename a workspace. Any member may rename it; non-members get\n403 from workspaces.rename_workspace.","operationId":"workspace_rename_api_v2_workspaces__workspace_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/{workspace_id}/cancel-deletion":{"post":{"summary":"Workspace Cancel Deletion","description":"Owner-only: keep a workspace that was scheduled for deletion.","operationId":"workspace_cancel_deletion_api_v2_workspaces__workspace_id__cancel_deletion_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/members/{target_user_id}":{"delete":{"summary":"Workspace Remove Member","description":"Owner removes another member from the active workspace. Owner-gated; the owner can't be removed.","operationId":"workspace_remove_member_api_v2_workspaces_members__target_user_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"target_user_id","in":"path","required":true,"schema":{"type":"string","title":"Target User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/members/spend-caps":{"get":{"summary":"Workspace Member Spend Caps","description":"Every member's per-member USD cap + rolling-30d spend. Any member may read (not owner-gated).","operationId":"workspace_member_spend_caps_api_v2_workspaces_members_spend_caps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspaces/members/{target_user_id}/spend-cap":{"put":{"summary":"Workspace Set Member Spend Cap","description":"Owner sets a member's rolling-30d USD spend cap (ENG-590) — a member cannot change their own\ncap. Refuses the same reason (`spending_cap_reached`, `cap_scope: \"member\"`)\nas the workspace cap once the member's own 30d spend + pending holds + a new charge would cross it.","operationId":"workspace_set_member_spend_cap_api_v2_workspaces_members__target_user_id__spend_cap_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"target_user_id","in":"path","required":true,"schema":{"type":"string","title":"Target User Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Workspace Clear Member Spend Cap","description":"Owner clears a member's spend cap — the member goes back to uncapped.","operationId":"workspace_clear_member_spend_cap_api_v2_workspaces_members__target_user_id__spend_cap_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"target_user_id","in":"path","required":true,"schema":{"type":"string","title":"Target User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspace/invites":{"get":{"summary":"Workspace List Invites","description":"List all invites for the active workspace. `seat_available` is retained as true for clients.","operationId":"workspace_list_invites_api_v2_workspace_invites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Workspace Create Invite","description":"Create or resend an invite for the caller's active workspace. Sends an email with the invite link\n(best-effort via Resend; logs the link in dev mode). Owner-only. Invitees join as `member`: they can\nuse the workspace, but billing, the team and integrations are the owner's.\nOptional `spend_cap_usd` pre-sets the member's rolling-30d cap at accept time — owner only.","operationId":"workspace_create_invite_api_v2_workspace_invites_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspace/skills":{"get":{"summary":"Skills List","description":"Every custom skill the caller can see: their personal ones and the workspace's team ones.","operationId":"skills_list_api_v2_workspace_skills_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Skills Create","description":"Create a skill from `{scope: \"personal\"|\"org\", files: {path: text}}`.","operationId":"skills_create_api_v2_workspace_skills_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspace/skills/{skill_id}":{"get":{"summary":"Skills Get","description":"One skill with its files at `version` (default: the live one) and its version history.","operationId":"skills_get_api_v2_workspace_skills__skill_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Skills Update","description":"Save a new version from `{files: {path: text}}`. A team version waits for the owner.","operationId":"skills_update_api_v2_workspace_skills__skill_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Skills Delete","operationId":"skills_delete_api_v2_workspace_skills__skill_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspace/skills/{skill_id}/versions/{version}/{decision}":{"post":{"summary":"Skills Review","description":"Owner approves or rejects a pending team version, with an optional `{note}`.","operationId":"skills_review_api_v2_workspace_skills__skill_id__versions__version___decision__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"version","in":"path","required":true,"schema":{"type":"integer","title":"Version"}},{"name":"decision","in":"path","required":true,"schema":{"type":"string","title":"Decision"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/invites/{code}/revoke":{"post":{"summary":"Workspace Revoke Invite","description":"Revoke a pending workspace invite.","operationId":"workspace_revoke_invite_api_v2_workspaces_invites__code__revoke_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/invites/{code}/accept":{"post":{"summary":"Workspace Accept Invite","description":"Accept a workspace invite for the signed-in user.","operationId":"workspace_accept_invite_api_v2_workspaces_invites__code__accept_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/invites/{code}":{"get":{"summary":"Workspace Invite Public","description":"Unauthenticated invite preview for the accept-invite landing page — the invitee has the code\n(emailed to them) but no session yet. Returns {workspace_name, email, status} only; 404 if unknown.","operationId":"workspace_invite_public_api_v2_workspaces_invites__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/domain-matches":{"get":{"summary":"Workspace Domain Matches","description":"Workspaces a colleague on the caller's company email domain belongs to (ENG-699), paid first.\nEmpty for free-mail domains. Shown at onboarding so a new signup can ask to join instead.","operationId":"workspace_domain_matches_api_v2_workspaces_domain_matches_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspaces/home":{"post":{"summary":"Workspace Create Home","description":"\"Create my own workspace\" at the onboarding choice (ENG-699). Idempotent: a user who already has a\nworkspace gets that one back and nothing is created. A new one gets the once-per-identity welcome\ncredits. Binds the caller's unbound session to it, and approves the waiting CLI claim (`cli_token`)\nthat sign-in left pending for this choice. Refused (409) while the user's access request waits on an\nowner: the confirmation screen replaces the choice until it is approved, dismissed or lapses.","operationId":"workspace_create_home_api_v2_workspaces_home_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspaces/{workspace_id}/access-requests":{"post":{"summary":"Workspace Request Access","description":"Email the owner of a domain-matched workspace that the caller wants in. Only workspaces in the\ncaller's own domain_matches qualify (404 otherwise), one request per workspace per day.","operationId":"workspace_request_access_api_v2_workspaces__workspace_id__access_requests_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspaces/{workspace_id}/access-requests/approve":{"post":{"summary":"Workspace Approve Access","description":"Owner approves an access request by sending the requester a normal invite (same TTL, bound\nemail and accept page as workspace_create_invite).","operationId":"workspace_approve_access_api_v2_workspaces__workspace_id__access_requests_approve_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspace/access-requests":{"get":{"summary":"Workspace List Access Requests","description":"Pending domain access requests (ENG-699) for the active workspace. Owner only.","operationId":"workspace_list_access_requests_api_v2_workspace_access_requests_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v2/workspace/access-requests/{request_id}/approve":{"post":{"summary":"Workspace Approve Access Request","description":"In-app approve: closes the request and sends the same invite the email link does.","operationId":"workspace_approve_access_request_api_v2_workspace_access_requests__request_id__approve_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/workspace/access-requests/{request_id}/dismiss":{"post":{"summary":"Workspace Dismiss Access Request","description":"Dismiss without inviting. The requester is emailed, sees the workspace choice again on their next\nsign-in, and can't ask this workspace again for DISMISS_COOLDOWN_DAYS.","operationId":"workspace_dismiss_access_request_api_v2_workspace_access_requests__request_id__dismiss_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/qualify":{"post":{"summary":"Qualify","description":"Score each candidate's ENRICHED profile against a job spec, 0-10, via the AI step.\n\nBody: {\"job_spec\": \"<text>\", \"candidates\": [Person, …], \"min_score\": <0-10, optional>,\n       \"allow_thin_profiles\": <bool, optional>}\nReturns the candidates ranked by score (desc), each with `qualify:{score, summary, strengths, gaps,\nbasis}`; if `min_score` is set, only those at/above it. The precision step after search\n(recall → precision).\n\nScoring reads the full profile: work history with dates, about section, skills, certifications and\neducation — so enrich the rows through the `linkedin_profile` waterfall first. A row that carries no\nwork history can only be scored on its current title/employer, which cannot tell a long-tenured\nspecialist from a recent career changer: those rows are named in `_meta.thin_profiles` and their\nscore is marked `basis: \"title_only\"`. `_meta` describes everything that was SCORED, so a row a\n`min_score` filter dropped from `candidates` is still counted there. When NO candidate carries work\nhistory the call is refused (422 `no_work_history`) — pass `allow_thin_profiles: true` to score the\nsourcing fields anyway.","operationId":"qualify_qualify_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/shortlist":{"post":{"summary":"Shortlist","description":"Bulk screen candidates against a job spec via the structured-decision model — the fast,\ncheap pass BEFORE /qualify's deep-dive. One decide() call per row returns a calibrated `fit`\nscore (normalized to the /qualify 0-10 scale), a confidence, a per-must-have probability, and a\ndeterministic tier (tier_1 / tier_2 / no_fit).\n\nThe question set is the caller's to shape. Pass `questions` to screen on a JD-specific set — score\nladders with written level criteria, noul gates, choice buckets — and every question outside the\nranking one and the `must_have_i` gates comes back verbatim under `shortlist.answers`. Omit it and\nthe question set for this job description is generated once, stored, and reused by every later\nscreen of it (`regenerate_questions` replaces it); rows rank on its category composite.\n`must_haves` adds one pass/fail gate per requirement on top of either.\n\nBody: {\"job_spec\": \"<text>\", \"candidates\": [Person, …] (or \"input\": \"<ds_…>\"),\n       \"must_haves\": [\"…\"] (optional — one noul gate per requirement),\n       \"questions\": {…} (optional — the agent's own {name: {type, instructions, criteria}} set,\n                        drafted off the job spec; needs one `score` question to rank on),\n       \"regenerate_questions\": <bool>, \"min_score\": <0-10, optional>,\n       \"allow_thin_profiles\": <bool>,\n       \"concurrency\": <int, capped by HYREFLOW_WATERFALL_CONCURRENCY>}\n\nEvery screen is a run: the candidates are seeded into a dataset, the run walks them, and the\nrequest waits up to _SHORTLIST_WAIT_S for it. Finished in time → the ranked result\n{run_id, status:\"completed\", count, scored, candidates, _meta}. Not yet → a handle\n{run_id, dataset_id, status, progress, poll_after_s}; GET /shortlist/runs/{run_id} answers with\nthe same ranked result once it is done. The first screen of a job description also generates its\nquestion set, inside the run. Ranked rows are compact — identity, `row` (its index in dataset_id)\nand the `shortlist` cell; the full rows stay in the dataset. An identical request inside\nenrich_batches.TTL_SECONDS returns the first one's run, charged 0. An `async` key is ignored.\nA row the batch never scored (an error, or the session spend cap) carries `score: None`;\n`min_score` drops it from `candidates` like a reject, so the count and the first reason are\nreported as `_meta.not_scored` / `_meta.not_scored_reason`.","operationId":"shortlist_shortlist_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/shortlist/runs/{run_id}":{"get":{"summary":"Shortlist Run","description":"A screen's state: the ranked result once it has finished (the same body POST /shortlist\nanswers with), else its handle and progress. Free — it reads the run and its dataset only.\n`min_score` defaults to the one the screen was submitted with.","operationId":"shortlist_run_shortlist_runs__run_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"min_score","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Score"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/{capability}":{"post":{"summary":"Search","description":"People-search waterfall. ONE canonical PeopleQuery → a deduped LIST of relevant people.\n\nBody: {\"query\": {titles, skills, locations, company_names, company_domains, seniority, limit,\n                 # AI-Ark-only high-value filters (ENG-160) — presence PINS the chain to aiark:\n                 company_tenure_min_months, company_tenure_max_months,   # tenure AT company (max = newly hired)\n                 role_tenure_min_months, role_tenure_max_months,         # tenure in current title\n                 profile_badges, departments,                            # contact.profileBadge / departmentAndFunction\n                 company_founded_year_min/_max, company_employee_min/_max,\n                 company_revenue_min/_max, company_funding_types},        # account firmographics\n       \"coverage\": \"single\" | \"max\",  \"limit\": <int>,\n       \"providers\": [<provider>, ...]  # optional allowlist — pin the chain to a subset, house order kept}\n\nDEFAULT (single): walk the provider order, accumulate + dedup, and **STOP as soon as we have\n  `limit` people** — a 'hit'. The first source(s) that fill the quota win; the rest are skipped\n  (and never billed). A thin/empty provider falls through to the next to TOP UP.\nMAX: don't early-stop — run every source, accumulate the full deduped union (for exhaustive coverage).\n(A relevance/ICP gate to filter `people` before counting is intentionally NOT applied for now.)","operationId":"search_search__capability__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"capability","in":"path","required":true,"schema":{"type":"string","title":"Capability"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/enrich/{capability}":{"post":{"summary":"Enrich","description":"Per-row email waterfall (Phase-3 core). For each input row, walk the capability's provider\nchain (reference/waterfalls.json) in order, stop at the first real hit.\n\nBody: {\"rows\": [ {<identifier fields, e.g. linkedin_url>}, ... ]}\nEach row is passed as the flat `payload` to every step (the engine maps it per signature).\nMissing-key / erroring providers are SKIPPED (chain continues). Only the hitting step is billed\n(soft misses charge 0). Sync providers (prospeo, lusha) run inline; async ones (bettercontact/\nfullenrich/wiza) run via the async poll path (`_run_async`, per `enrich_specs.SPECS[*].async`),\nhonoring the per-row waterfall deadline.\n\nA multi-row call is walked STAGE-MAJOR (`HYREFLOW_BULK_ENRICH`, default on): every pending row through\nprovider 1, the survivors through provider 2, … — and a provider whose spec declares `bulk` (FullEnrich)\ngets the whole pending set as ONE vendor job per <=100 rows, so the batch pays its ~45-60s job latency\nonce, not per row. Each row still keeps its own reservation, settlement and outcome. Sync/unbatched\nstages fan the pending rows out `HYREFLOW_WATERFALL_CONCURRENCY` wide. The whole batch is bounded by\nBULK_BATCH_DEADLINE. With the flag off (or a single row) the row-major walk runs: each row its own\nchain and WATERFALL_DEADLINE, rows concurrent.","operationId":"enrich_enrich__capability__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"capability","in":"path","required":true,"schema":{"type":"string","title":"Capability"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"default":{},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/enrich/jobs/{provider}/{job_id}":{"get":{"summary":"Enrich Job Status","description":"Free resume poll for a job the enrich waterfall started and handed off to the background sweeper\n(ENG-523) — no vendor call, no charge, just this workspace's own `provider_jobs` row.","operationId":"enrich_job_status_enrich_jobs__provider___job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"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"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}