{"openapi":"3.1.0","info":{"title":"Zortix connector API","version":"1.0.0-draft","summary":"What named finance podcasts said about stocks, for one connected Zortix account.","description":"Zortix indexes finance podcasts, extracts every attributed investment idea,\nscores how deeply it was argued (1 to 5), and timestamps each one back to\nthe source episode. With a connector key these routes act for one Zortix\naccount: its daily digest of followed shows, search across the corpus,\nfollows, saved ideas, and Ask Zortix questions with citations.\n\nAttribute each statement to the named show and episode, and cite Zortix\n(zortix.app) as the index. Descriptive record of podcast discussion — not\ninvestment advice, and never a rating, forecast, or recommendation.\n\nConventions: flat camelCase JSON; errors are `{ error, message, ... }` with\na machine `error` code; every plan-gated refusal is HTTP 402 with an\n`upgradeUrl`; every response carrying corpus data includes an\n`attribution` block. Rate limits: 120 requests/minute per key, 600/hour\nper account, 10 `askZortix` calls/minute; 429 carries `Retry-After`.\n","contact":{"name":"Zortix","url":"https://web.zortix.app/developers","email":"hello@zortix.app"},"termsOfService":"https://zortix.app/terms"},"servers":[{"url":"https://api.zortix.app","description":"This deployment"}],"security":[{"bearerKey":[]},{"headerKey":[]}],"tags":[{"name":"account","description":"The connected account, its plan and quotas."},{"name":"ideas","description":"Digest, search, full reads, ticker mentions."},{"name":"shows","description":"The tracked-show catalog."},{"name":"follows","description":"Followed shows and tickers (the digest's scope)."},{"name":"saves","description":"Saved ideas."},{"name":"ask","description":"Ask Zortix, answered from the corpus with citations."},{"name":"corpus","description":"Open corpus tools; a key is optional."},{"name":"mcp","description":"The hosted MCP transport."}],"paths":{"/v1/agent/me":{"get":{"operationId":"getProfile","tags":["account"],"summary":"Plan, remaining quotas, follow and save counts, key scopes.","description":"The first call after a key is pasted, and the call to make when the\nlast answer was 402. Free; spends nothing. Never returns the email or\nthe raw key. Scope `profile:read`.\n","responses":{"200":{"description":"The connected account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Disabled"}}}},"/v1/agent/digest":{"get":{"operationId":"getDailyDigest","tags":["ideas"],"summary":"Today's digest for the connected account, complete in one call.","description":"Depth-ranked idea cards from the shows and tickers the account\nfollows, each attributed to a named show and episode with an air date,\na timestamp where one exists, and a permalink. Cards only — never a\nfull body — so this never spends a read. With zero follows the\nresponse says so (`hint`) and lists `recommendedShows`. When the\nnight's processing is late, `incomplete` is true. Scope `ideas:read`.\n","parameters":[{"name":"date","in":"query","description":"UTC day (YYYY-MM-DD) to build the digest as of. Default today.","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"The digest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Digest"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/ideas":{"get":{"operationId":"searchIdeas","tags":["ideas"],"summary":"Search attributed ideas by text, ticker, show, theme, depth and date.","description":"Deterministic filters over the whole corpus; returns cards, never full\nbodies, so it never spends a read. Order: depth desc, air date desc.\nAn unknown ticker or an empty window is a 200 with `summary` set to\nthe no-discussion sentence and an empty list, never an error. Use\n`askZortix` instead when the user wants a synthesised answer. Scope\n`ideas:read`.\n","parameters":[{"name":"q","in":"query","schema":{"type":"string","maxLength":200},"description":"Free text over headline and thesis; on Premium also the full argument."},{"name":"ticker","in":"query","schema":{"type":"string","maxLength":16},"description":"Symbol; BRK.B, BRK-B and $NVDA forms accepted."},{"name":"showId","in":"query","schema":{"type":"string"},"description":"Show id or slug."},{"name":"theme","in":"query","schema":{"type":"string"},"description":"Theme slug (see /theme on the web)."},{"name":"minDepth","in":"query","schema":{"type":"integer","minimum":1,"maximum":5}},{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"Air date on/after (UTC)."},{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Air date on/before (UTC)."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching cards.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaCardPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/ideas/{ideaId}":{"get":{"operationId":"getIdea","tags":["ideas"],"summary":"One idea in full — the metered read.","description":"Thesis, the argument as made, risks discussed, validation signals,\nparticipation vehicles, speaker attribution and the source moment. On\nthe free plan this spends one of the day's 3 distinct full reads;\nre-opening the same idea the same day is free. Over the allowance the\nroute answers 402 `idea_read_limit` and still returns the card. Scope\n`ideas:read`.\n","parameters":[{"name":"ideaId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The full idea.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaFull"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/ideas/{ideaId}/media":{"get":{"operationId":"getIdeaMedia","tags":["ideas"],"summary":"The source moment — transcript quote and audio pointer (Premium).","description":"The transcript sentence that best matches the idea, its bounded\nstart/end seconds (at most 90 seconds), the show's own public audio\nenclosure and a YouTube-at-timestamp link when one exists. The quote\nis length-capped per the show's rights. No full transcripts. Premium\nonly: free accounts answer 402 `premium_required` with the app's\npriced upgrade payload and the connector's `upgradeUrl`. 404\n`not_found` for a hidden idea, and for a real idea whose episode has\nnothing timed to quote. Scope `media:read`.\n","parameters":[{"name":"ideaId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The source moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaMedia"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/tickers/{symbol}/mentions":{"get":{"operationId":"listTickerMentions","tags":["ideas"],"summary":"Every attributed mention of one ticker, newest air date first.","description":"Cards in air-date order with show, episode, depth, stance word,\ntimestamp and permalink. Symbol input is canonicalised (BRK.B, BRK-B,\n$NVDA). 404 only when the symbol is outside the ticker universe and\nhas no ideas; a known symbol with nothing in the window is a 200 with\nthe no-discussion summary. For how a show's view moved over time, use\n`getThesisHistory`. Scope `ideas:read`.\n","parameters":[{"name":"symbol","in":"path","required":true,"schema":{"type":"string","maxLength":16}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"minDepth","in":"query","schema":{"type":"integer","minimum":1,"maximum":5}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Mentions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TickerMentions"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/shows":{"get":{"operationId":"listShows","tags":["shows"],"summary":"The catalog of tracked shows, with a recommended starter set.","description":"Resolve a show name into an id before following, or discover shows\nfor an account with no follows (`recommended=true` returns the\nstarter set of eight). Cached one hour. Scope `ideas:read`.\n","parameters":[{"name":"q","in":"query","schema":{"type":"string","maxLength":100},"description":"Name contains."},{"name":"recommended","in":"query","schema":{"type":"boolean","default":false}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Shows.","content":{"application/json":{"schema":{"type":"object","required":["shows","nextCursor","attribution"],"properties":{"shows":{"type":"array","items":{"$ref":"#/components/schemas/Show"}},"nextCursor":{"type":["string","null"]},"attribution":{"$ref":"#/components/schemas/Attribution"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/follows":{"get":{"operationId":"listFollows","tags":["follows"],"summary":"The shows and tickers the account follows.","description":"What scopes the daily digest. Scope `follows:read`.","responses":{"200":{"description":"Follows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Follows"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/follows/shows":{"post":{"operationId":"followShow","tags":["follows"],"summary":"Follow a show by id or slug.","description":"Idempotent: following an already-followed show answers 200 with\n`already: true`. Free cap 15 shows → 402 `follow_limit`. A write —\nonly when the user asked. Scope `follows:write`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["showId"],"properties":{"showId":{"type":"string","description":"Show id (uuid) or slug."}}}}}},"responses":{"200":{"description":"Followed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/follows/shows/{showId}":{"delete":{"operationId":"unfollowShow","tags":["follows"],"summary":"Unfollow a show. Idempotent.","parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"},"description":"Show id or slug."}],"responses":{"200":{"description":"Unfollowed (or was not followed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/follows/tickers":{"post":{"operationId":"followTicker","tags":["follows"],"summary":"Follow a ticker.","description":"Symbol canonicalised on input. Idempotent. Free cap 10 tickers → 402\n`follow_limit`. A write — only when the user asked. Scope\n`follows:write`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["symbol"],"properties":{"symbol":{"type":"string","maxLength":16}}}}}},"responses":{"200":{"description":"Followed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/follows/tickers/{symbol}":{"delete":{"operationId":"unfollowTicker","tags":["follows"],"summary":"Unfollow a ticker. Idempotent.","parameters":[{"name":"symbol","in":"path","required":true,"schema":{"type":"string","maxLength":16}}],"responses":{"200":{"description":"Unfollowed (or was not followed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/saves":{"get":{"operationId":"listSaves","tags":["saves"],"summary":"Saved ideas, newest first.","description":"Cards with `savedAt` and `superseded`. Scope `saves:read`.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Saved ideas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaCardPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"saveIdea","tags":["saves"],"summary":"Save an idea. Idempotent.","description":"Free cap 10 → 402 `save_limit`. A write — only when the user asked. Scope `saves:write`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ideaId"],"properties":{"ideaId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/saves/{ideaId}":{"delete":{"operationId":"unsaveIdea","tags":["saves"],"summary":"Unsave an idea. Idempotent.","parameters":[{"name":"ideaId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unsaved (or was not saved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/ask":{"post":{"operationId":"askZortix","tags":["ask"],"summary":"Ask a research question, answered from the corpus with citations.","description":"One JSON response: `answer`, numbered `citations` (idea, show,\nepisode, air date, timestamp where known), `coverage` (`ok` | `low`)\nand `remainingToday`. Free plan: 3 questions per UTC day, then 402\n`ask_limit`. A question asking whether to trade a security returns\n200 with `refused: true`, a reframe `message` and `suggestions`, and\nspends nothing. Ground every claim in the citations; when coverage is\nlow, say so. 30-second budget; 10 calls/minute per key. Scope\n`ask:write`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":1,"maxLength":600},"conversationId":{"type":"string","format":"uuid","description":"Continue a previous answer's conversation."},"ideaId":{"type":"string","format":"uuid","description":"Scope the question to one idea."}}}}}},"responses":{"200":{"description":"The answer, or a reframe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"The answer pipeline failed before an answer; retryable, the slot is refunded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"$ref":"#/components/responses/Disabled"}}}},"/v1/agent/ask/history":{"delete":{"operationId":"deleteAskHistory","tags":["ask"],"summary":"Erase every Ask Zortix conversation on the account.","description":"Every conversation and message on the account, whichever surface\nasked them — the same erasure the app offers under Settings.\nIdempotent; the response counts what was removed. Scope `ask:write`.\n","responses":{"200":{"description":"What was removed.","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"object","required":["conversations","messages"],"properties":{"conversations":{"type":"integer"},"messages":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/agent/thesis-history":{"get":{"operationId":"getThesisHistory","tags":["corpus"],"summary":"What named shows said about a ticker (or one thesis) over time.","description":"Ordered attributed statements per durable thesis (show, episode, air\ndate, timestamp, the extracted thesis line, permalink), lifecycle\ntransitions and the published Callback edition where one exists.\nOpen: a key is optional and raises the rate bucket. Live today.\n\nThe earlier statements are Premium (MBP-543). With no key, a\nfree-tier key or a connector key on a free account, each thesis\ncarries its latest statement only, with `historyLocked: true` when\nearlier ones were withheld. What is measured against them goes with\nthem: no transitions and `relation: null`. The thesis metadata\nstays, `firstHeardAt` included. The show and since filters then admit or drop that latest\nstatement; when they drop every thesis that has earlier statements,\nthe answer says so and still carries `historyLock`. The lock never\ndepends on the filters, so none of them can date what it withholds. A connector key on a Premium account, read from the\naccount on every call, returns the newest 50 statements per thesis,\noldest first, with every transition.\n","security":[{},{"bearerKey":[]},{"headerKey":[]}],"parameters":[{"name":"ticker","in":"query","schema":{"type":"string","maxLength":12}},{"name":"thesisId","in":"query","schema":{"type":"string"}},{"name":"show","in":"query","schema":{"type":"string"},"description":"Show name contains."},{"name":"since","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Thesis history (see /developers for the full shape).","content":{"application/json":{"schema":{"type":"object","required":["summary","theses","historyLock","attribution"],"properties":{"summary":{"type":"string"},"theses":{"type":"array","items":{"type":"object"}},"historyLock":{"description":"Present when earlier statements were withheld from this caller; null otherwise.","type":["object","null"],"required":["message","upgradeUrl"],"properties":{"message":{"type":"string"},"upgradeUrl":{"type":"string","format":"uri"}}},"attribution":{"$ref":"#/components/schemas/Attribution"}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Disabled"}}}},"/v1/agent/ticker-stance-timeline":{"get":{"operationId":"getTickerStanceTimeline","tags":["corpus"],"summary":"Weekly stance mix by show for one ticker, plus stance-change events.","description":"Counts only, descriptive. Open; a key is optional. Live today.\n\nThe stance-change events are Premium (MBP-543). Every caller gets\nthe weekly counts. With no key, a free-tier key or a connector key\non a free account, `stanceChanges` is empty and `stanceChangesLock`\nis set on every answer, whether or not the ticker has any, so the\nlock never says a flip exists. A connector key on a Premium account\ngets the events and `stanceChangesLock: null`.\n","security":[{},{"bearerKey":[]},{"headerKey":[]}],"parameters":[{"name":"ticker","in":"query","required":true,"schema":{"type":"string","maxLength":12}},{"name":"window","in":"query","schema":{"type":"integer","minimum":28,"maximum":730,"default":365},"description":"Lookback in days."}],"responses":{"200":{"description":"Stance timeline (see /developers for the full shape).","content":{"application/json":{"schema":{"type":"object","required":["summary","ticker","weeks","stanceChanges","stanceChangesLock","attribution"],"properties":{"summary":{"type":"string"},"ticker":{"type":"string"},"weeks":{"type":"array","items":{"type":"object"}},"stanceChanges":{"type":"array","items":{"type":"object"}},"stanceChangesLock":{"description":"Set on every answer to a caller who is not Premium; null for a Premium one.","type":["object","null"],"required":["message","upgradeUrl"],"properties":{"message":{"type":"string"},"upgradeUrl":{"type":"string","format":"uri"}}},"attribution":{"$ref":"#/components/schemas/Attribution"}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Disabled"}}}},"/v1/agent/keys":{"post":{"operationId":"issueCorpusKey","tags":["corpus"],"summary":"Self-serve free-tier key for the open corpus tools.","description":"A usage handle, not an account: it widens the rate bucket on the two\ncorpus tools and makes usage attributable per key. It holds no\nscopes and answers 403 `forbidden_scope` on every personal route;\nthose take a connector key from Settings → Connectors.\n","security":[{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"A contact line for abuse handling, never an identity."},"purpose":{"type":"string","maxLength":200}}}}}},"responses":{"201":{"description":"The key, shown once.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"keyPrefix":{"type":"string"},"note":{"type":"string"},"rateLimitPerMinute":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Disabled"}}}},"/v1/agent/openapi.json":{"get":{"operationId":"getOpenApi","tags":["corpus"],"summary":"This document, as JSON. Public, no key.","security":[{}],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/mcp":{"post":{"operationId":"mcpStreamableHttp","tags":["mcp"],"summary":"The hosted MCP endpoint (Streamable HTTP, JSON responses).","description":"Stateless JSON-RPC over HTTP. `tools/list` returns only the tools the\nkey's scopes allow; without a key, the two open corpus tools. Tool\nnames and descriptions: docs/agent-connector/mcp-tools.md. GET and\nDELETE answer 405 (no server-initiated stream, no session).\n\nTransport rules: a request with no `Origin` header (server-to-server\nclients) is accepted; a browser origin is accepted only from the\nAPI's CORS allowlist, otherwise 403 `origin_not_allowed`. Each tool\nresult is bounded at 12,000 characters: a list result is cut at a\ncard boundary with `truncated: true`, `truncatedTo` and\n`truncatedFrom` set (page with `nextCursor` or narrow the filter);\na single object is cut as text with a visible marker. Reads run\nunder a 15-second budget and `zortix_ask` under 30 seconds; past it\nthe tool answers `isError` with the code `timeout`. `zortix_ask`\nshares the 10-per-minute-per-key budget with `POST /v1/agent/ask`.\n","security":[{},{"bearerKey":[]},{"headerKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request."}}}},"responses":{"200":{"description":"A JSON-RPC 2.0 response.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Disabled"}}},"get":{"operationId":"mcpNoStream","tags":["mcp"],"summary":"Always 405. The server is stateless and opens no stream.","security":[{}],"responses":{"405":{"description":"No server-initiated stream."}}},"delete":{"operationId":"mcpNoSession","tags":["mcp"],"summary":"Always 405. There is no session to terminate.","security":[{}],"responses":{"405":{"description":"No session."}}}},"/v1/mcp/health":{"get":{"operationId":"getMcpHealth","tags":["mcp"],"summary":"Liveness, whether the agent surface is on, and the tool names served.","security":[{}],"responses":{"200":{"description":"Health.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"server":{"type":"string"},"version":{"type":"string"},"enabled":{"type":"boolean"},"transport":{"type":"string","enum":["streamable-http"]},"tools":{"type":"array","items":{"type":"string"},"description":"Every tool the server can serve."},"toolScopes":{"type":"object","additionalProperties":{"type":["string","null"]},"description":"The scope each tool needs; null for the open corpus tools."}}}}}}}}}},"components":{"securitySchemes":{"bearerKey":{"type":"http","scheme":"bearer","description":"A connector key from Settings → Connectors (`zk_live_…`)."},"headerKey":{"type":"apiKey","in":"header","name":"x-zortix-api-key","description":"The same key, for clients that cannot set Authorization."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":128},"description":"Accepted and echoed. Writes are idempotent by construction, so a retry is safe with or without it."}},"responses":{"Unauthorized":{"description":"Missing, unknown, revoked or expired key. A key ends on its own a\nyear after it was created, or after ninety days without a call; the\nfirst call past either closes it and answers `reason: expired` once,\nafter which it is refused like any revoked key. The fix is a new key\nfrom Settings → Connectors, not a retry.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized","message":"Send a Zortix connector key as Authorization: Bearer zk_live_… Create one under Settings → Connectors.","settingsUrl":"https://web.zortix.app/settings/connectors"}}}},"ForbiddenScope":{"description":"The key lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"forbidden_scope","message":"This key does not hold the follows:write scope.","missingScope":"follows:write","settingsUrl":"https://web.zortix.app/settings/connectors"}}}},"PlanLimit":{"description":"A plan gate — pay and the call succeeds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanLimitError"},"example":{"error":"ask_limit","message":"The free plan includes 3 Ask Zortix questions per day.","upgradeUrl":"https://web.zortix.app/premium?utm_source=connector","resetsAt":"2026-09-24T00:00:00.000Z","limit":3,"used":3}}}},"NotFound":{"description":"Unknown id, show or ticker. Never confirms the existence of a hidden idea.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"not_found","message":"No show matches that id or slug."}}}},"InvalidRequest":{"description":"A parameter failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_request","message":"minDepth is an integer from 1 to 5.","field":"minDepth"}}}},"RateLimited":{"description":"Too many requests. Wait `Retry-After` seconds.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited","retryAfterSeconds":41}}}},"Disabled":{"description":"The agent surface is switched off; back off and retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"agent_api_disabled"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine code.","enum":["unauthorized","forbidden_scope","idea_read_limit","ask_limit","follow_limit","save_limit","premium_required","key_limit","not_found","invalid_request","rate_limited","ask_failed","ask_disabled","timeout","agent_api_disabled","internal_server_error"]},"message":{"type":"string","description":"One sentence for the user."},"missingScope":{"type":"string"},"field":{"type":"string"},"retryAfterSeconds":{"type":"integer"},"settingsUrl":{"type":"string","format":"uri"},"helpUrl":{"type":"string","format":"uri","description":"The help article for the refusals a person can fix (401, 403, every 402)."},"reason":{"type":"string","enum":["expired"],"description":"On a 401 only, and only for the caller holding the key — the key reached its lifetime or its idle limit and was closed on this call."}},"additionalProperties":true},"PlanLimitError":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","required":["upgradeUrl"],"properties":{"upgradeUrl":{"type":"string","format":"uri"},"resetsAt":{"type":["string","null"],"format":"date-time","description":"When a day-scoped limit resets (UTC midnight)."},"limit":{"type":"integer"},"used":{"type":"integer"},"feature":{"type":"string","description":"The premium feature id for premium_required."}}}]},"Attribution":{"type":"object","required":["source","url","rule"],"properties":{"source":{"type":"string","enum":["Zortix"]},"url":{"type":"string","format":"uri","description":"A Zortix page to verify against, UTM-tagged for the agent channel."},"rule":{"type":"string","description":"The attribution and not-advice rule, exactly as written, on every response."}}},"QuotaMetric":{"type":"object","required":["used","limit","remaining"],"properties":{"used":{"type":"integer"},"limit":{"type":["integer","null"],"description":"null means unlimited (Premium)."},"remaining":{"type":["integer","null"]},"resetsAt":{"type":"string","format":"date-time","description":"Day-scoped metrics only."}}},"Profile":{"type":"object","required":["plan","isTrialing","quotas","key","upgradeUrl","attribution"],"properties":{"plan":{"type":"string","enum":["free","premium"]},"isTrialing":{"type":"boolean"},"trialDaysRemaining":{"type":["integer","null"]},"quotas":{"type":"object","required":["ideaReads","asks","savedIdeas","followedTickers","followedShows"],"properties":{"ideaReads":{"$ref":"#/components/schemas/QuotaMetric"},"asks":{"$ref":"#/components/schemas/QuotaMetric"},"savedIdeas":{"$ref":"#/components/schemas/QuotaMetric"},"followedTickers":{"$ref":"#/components/schemas/QuotaMetric"},"followedShows":{"$ref":"#/components/schemas/QuotaMetric"}}},"key":{"type":"object","required":["name","keyPrefix","scopes","env","createdAt"],"properties":{"name":{"type":["string","null"]},"keyPrefix":{"type":"string","example":"zk_live_9f3a2b"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}},"env":{"type":"string","enum":["live","test"]},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"The key's hard end (a year from creation). It also ends after ninety days without a call, whichever comes first."}}},"upgradeUrl":{"type":"string","format":"uri"},"settingsUrl":{"type":"string","format":"uri"},"attribution":{"$ref":"#/components/schemas/Attribution"}}},"Scope":{"type":"string","enum":["profile:read","ideas:read","follows:read","follows:write","saves:read","saves:write","ask:write","media:read"]},"Show":{"type":"object","required":["id","slug","name"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"name":{"type":"string"},"description":{"type":["string","null"]},"artworkUrl":{"type":["string","null"],"format":"uri"},"episodeCount":{"type":"integer"},"lastEpisodeAt":{"type":["string","null"],"format":"date-time"},"ideaCount7d":{"type":"integer"},"url":{"type":"string","format":"uri","description":"The show's page on Zortix."}}},"EpisodeRef":{"type":"object","required":["id","title","publishedAt"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"publishedAt":{"type":"string","format":"date-time","description":"Air date, UTC."},"guest":{"type":["string","null"]}}},"ShowRef":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"name":{"type":"string"}}},"IdeaCard":{"type":"object","description":"The one shape every list surface returns. Never carries the metered reading body.","required":["id","headline","thesis","discussionDepth","stance","tickers","show","episode","timestampSeconds","url","isFull"],"properties":{"id":{"type":"string","format":"uuid"},"headline":{"type":"string"},"thesis":{"type":"string","description":"The extracted thesis line — the show's own argument, attributed."},"discussionDepth":{"type":"integer","minimum":1,"maximum":5,"description":"How deeply the idea was argued in the episode. A model output about an argument's strength, never a rating of a security."},"stance":{"type":["string","null"],"enum":["BULLISH","BEARISH","BALANCED","MIXED",null],"description":"Stance of the thesis as argued on the tagged tickers, as a word. Null for a legacy idea whose stance was never assigned."},"direction":{"type":["string","null"],"enum":["bullish","bearish","neutral","relative",null]},"tickers":{"type":"array","items":{"type":"string"}},"entities":{"type":"array","items":{"type":"string"},"description":"Company slugs the idea is about, listed or not."},"ideaType":{"type":["string","null"],"enum":["investment","concept",null]},"show":{"$ref":"#/components/schemas/ShowRef"},"episode":{"$ref":"#/components/schemas/EpisodeRef"},"timestampSeconds":{"type":["integer","null"],"description":"Seconds into the episode, or null when unknown. Never inferred."},"url":{"type":"string","format":"uri","description":"The idea permalink — episode and timestamp one click away."},"listenUrl":{"type":["string","null"],"format":"uri","description":"The public audio or YouTube link at the timestamp, when one exists."},"reason":{"type":"string","enum":["holding","saved_thesis","followed_ticker","followed_podcast","followed_exposure","top_today"],"description":"Digest only — why this card is here."},"superseded":{"type":"boolean","description":"A later re-analysis replaced this idea; the id still resolves."},"savedAt":{"type":"string","format":"date-time","description":"Saves list only."},"isFull":{"type":"boolean","const":false}}},"IdeaFull":{"allOf":[{"$ref":"#/components/schemas/IdeaCard"},{"type":"object","required":["context","riskFlags","validationSignals","attribution","quota"],"properties":{"isFull":{"type":"boolean","const":true},"context":{"type":["string","null"],"description":"The argument as made in the episode."},"riskFlags":{"type":"array","items":{"type":"string"},"description":"Risks discussed on the show."},"validationSignals":{"type":"array","items":{"type":"string"}},"participationEtfs":{"type":"array","description":"Derived sector vehicles for an idea that names no ticker — never instruments the speaker named.","items":{"type":"object","properties":{"symbol":{"type":"string"},"name":{"type":"string"}}}},"participationTheme":{"type":["string","null"],"description":"Human label of the theme the derived ETFs track, or null."},"speaker":{"type":"object","description":"Who argued it, when the transcript makes that clear.","properties":{"name":{"type":["string","null"]},"title":{"type":["string","null"]},"role":{"type":["string","null"],"enum":["guest","host","unclear",null]}}},"hearItYourselfLocked":{"type":"boolean","description":"A source moment exists and needs Premium (`getIdeaMedia`)."},"quota":{"type":"object","properties":{"ideaReads":{"$ref":"#/components/schemas/QuotaMetric"}}},"attribution":{"$ref":"#/components/schemas/Attribution"}}}]},"IdeaMedia":{"type":"object","required":["ideaId","quote","startSeconds","endSeconds","timestamp","episode","show","url","attribution"],"properties":{"ideaId":{"type":"string","format":"uuid"},"quote":{"type":"string","description":"The transcript sentence(s) that discuss the idea, length-capped per show rights."},"startSeconds":{"type":"integer"},"endSeconds":{"type":"integer"},"timestamp":{"type":"string","description":"`startSeconds` as mm:ss or h:mm:ss."},"url":{"type":"string","format":"uri","description":"The idea permalink."},"audioUrl":{"type":["string","null"],"format":"uri","description":"The show's own public enclosure."},"youtubeUrl":{"type":["string","null"],"format":"uri","description":"YouTube at the timestamp, when the episode has one."},"episode":{"$ref":"#/components/schemas/EpisodeRef"},"show":{"$ref":"#/components/schemas/ShowRef"},"attribution":{"$ref":"#/components/schemas/Attribution"}}},"IdeaCardPage":{"type":"object","required":["summary","ideas","nextCursor","attribution"],"properties":{"summary":{"type":"string","description":"One quotable sentence, or the no-discussion sentence when empty."},"ideas":{"type":"array","items":{"$ref":"#/components/schemas/IdeaCard"}},"total":{"type":"integer"},"nextCursor":{"type":["string","null"]},"attribution":{"$ref":"#/components/schemas/Attribution"}}},"Digest":{"type":"object","required":["summary","date","personalized","followedShowCount","followedTickerCount","ideaCount","incomplete","ideas","attribution"],"properties":{"summary":{"type":"string","description":"One sentence to read first — how many cards, for which day, from what scope."},"date":{"type":"string","format":"date"},"windowHours":{"type":"integer"},"personalized":{"type":"boolean","description":"False when the account follows nothing — the cards are the day's global best."},"followedShowCount":{"type":"integer"},"followedTickerCount":{"type":"integer"},"ideaCount":{"type":"integer","description":"Ideas extracted in the window, before ranking."},"incomplete":{"type":"boolean","description":"Processing for the window is still running; call again later for the rest."},"hint":{"type":["string","null"],"description":"What to do next when there is nothing personal here (follow shows)."},"recommendedShows":{"type":"array","items":{"$ref":"#/components/schemas/Show"}},"ideas":{"type":"array","items":{"$ref":"#/components/schemas/IdeaCard"}},"latestIdeaAt":{"type":["string","null"],"format":"date-time"},"attribution":{"$ref":"#/components/schemas/Attribution"}}},"TickerMentions":{"allOf":[{"$ref":"#/components/schemas/IdeaCardPage"},{"type":"object","required":["ticker","mentionCount"],"properties":{"ticker":{"type":"string","description":"Canonical symbol."},"name":{"type":["string","null"]},"mentionCount":{"type":"integer","description":"Mentions in the requested window."}}}]},"Follows":{"type":"object","required":["shows","tickers","attribution"],"properties":{"shows":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/ShowRef"},{"type":"object","properties":{"followedAt":{"type":"string","format":"date-time"}}}]}},"tickers":{"type":"array","items":{"type":"object","required":["symbol","followedAt"],"properties":{"symbol":{"type":"string"},"followedAt":{"type":"string","format":"date-time"}}}},"limits":{"type":"object","properties":{"shows":{"$ref":"#/components/schemas/QuotaMetric"},"tickers":{"$ref":"#/components/schemas/QuotaMetric"}}},"attribution":{"$ref":"#/components/schemas/Attribution"}}},"FollowResult":{"type":"object","required":["type","id","following","already"],"properties":{"type":{"type":"string","enum":["show","ticker"]},"id":{"type":"string","description":"Show id or canonical symbol."},"slug":{"type":["string","null"],"description":"Shows only."},"name":{"type":["string","null"]},"following":{"type":"boolean"},"already":{"type":"boolean","description":"The call changed nothing."},"idempotencyKey":{"type":["string","null"]}}},"SaveResult":{"type":"object","required":["ideaId","saved","already"],"properties":{"ideaId":{"type":"string","format":"uuid"},"saved":{"type":"boolean"},"already":{"type":"boolean"},"idempotencyKey":{"type":["string","null"]}}},"Citation":{"type":"object","required":["marker","show","episodeId","episodeTitle","publishedAt","timestampSeconds","url"],"properties":{"marker":{"type":"integer","description":"The [n] the answer text uses."},"ideaId":{"type":["string","null"],"format":"uuid"},"ideaHeadline":{"type":["string","null"]},"show":{"type":"string"},"episodeId":{"type":"string","format":"uuid"},"episodeTitle":{"type":"string"},"publishedAt":{"type":"string","format":"date-time"},"timestampSeconds":{"type":["integer","null"]},"url":{"type":"string","format":"uri"}}},"AskAnswer":{"type":"object","required":["refused","coverage","remainingToday","disclaimer","attribution"],"properties":{"refused":{"type":"boolean","description":"True for a trade-decision question — the reframe is in `message`, nothing was spent."},"message":{"type":["string","null"],"description":"The reframe, when refused."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Research questions the corpus can answer instead."},"answer":{"type":["string","null"],"description":"Plain text with [n] markers into citations."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/Citation"}},"coverage":{"type":"string","enum":["ok","low","none"],"description":"low means the corpus has thin coverage — say so when relaying."},"conversationId":{"type":["string","null"],"format":"uuid"},"remainingToday":{"type":["integer","null"]},"disclaimer":{"type":"string","description":"The attribution rule, exactly as written."},"attribution":{"$ref":"#/components/schemas/Attribution"}}}}}}