{
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"id": "8f14e45f-ceea-467e-adc0-7a1a6b5f6b28",
"job_type": "query",
"status": "succeeded",
"payload": {
"question": "What does the research say about magnesium for perimenopause sleep?"
},
"result": {
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"answer": "Several trials suggest magnesium may modestly improve sleep quality… [1]",
"citations": [
{
"id": "chunk_abc123",
"score": 0.87,
"text": "Magnesium supplementation improved subjective sleep quality…",
"metadata": {
"title": "Magnesium and sleep in midlife women",
"doi": "10.1000/example.doi",
"year": "2023"
}
}
]
},
"error": null,
"error_code": null,
"expires_at": "2026-09-11T13:00:00.000Z",
"attempts": 1,
"started_at": "2026-09-11T12:00:00.400Z",
"created_at": "2026-09-11T12:00:00.000Z",
"updated_at": "2026-09-11T12:00:23.000Z"
}
Async jobs
Poll an async job's status
Read-only poll for one async job’s current state. Jobs are
created by POST /v1/jobs — see the “Async jobs” tag for the
lifecycle, retention, and polling guidance.
Reading past the deadline. If the job is still queued or
running when its expires_at has passed — or it has been
running for more than 2 minutes — this read marks it expired
before returning it, so you never see a stale in-flight state.
A job that had already finished (succeeded / failed /
expired) is returned as it finished until expires_at;
the first read after that answers 404 not_found, clears the
stored request and result, and the row is deleted within ~24 h.
Fetch your result before expires_at.
Transient errors while polling. A 429, or a 503 that
carries Retry-After, says nothing about the job: wait
max(Retry-After, 1) seconds and poll again.
Server-to-server only, with your long-lived key. A short-lived
session token (see “Mobile-direct access” above) cannot call this
endpoint: its scope covers only
/v1/query and /v1/chat, so it gets 403 forbidden here like on
every other endpoint. Do NOT call this endpoint from browser-side
code, and never embed your long-lived key in any client.
Scoping: a job is visible to every long-lived key of the partner
that created it (ownership is per partner, not per key). A job id
that doesn’t exist and a job id that belongs to a different
partner both return the identical 404 not_found — this endpoint
never confirms another partner’s job id exists.
Rate limits are communicated via X-RateLimit-* headers on every
response and are shared with /v1/query and /v1/chat per API
key (see “Rate limits” above). When a 429 is returned, honour
Retry-After before retrying.
Bearer token used by every /v1/* endpoint. Two kinds of key work here:
Long-lived partner key (cxh_pk_live_*) — issued by
CollectiveX Health (contact partners@collectivex.health). Keep
it server-side only — never expose it in client bundles or
mobile app code. This rule is absolute and does not change for
any endpoint.
Short-lived session key — minted server-side via
POST /v1/session-tokens from a long-lived key. Safe to hand
to a native mobile client only — not a browser or other
JavaScript context, see "API keys" in the description above.
It's scoped to /v1/query and /v1/chat only, expires in at
most 60 minutes, and can't mint further session keys.
POST /v1/session-tokens itself only ever accepts the
long-lived key.
Current state of one async job. payload is the request the job
was created with and result is its output; both are typed per
job_type. The only job type today is query, whose payload is
a QueryRequest and whose result is a QueryResponse.
Identifies what kind of work this job represents and therefore
the shape of payload/result. Today always query (an
asynchronous /v1/query). Not an enum in this spec — a future
job type would be an additive minor bump, with its own
payload/result shape documented alongside.
Current lifecycle state. queued → running → one of
succeeded / failed / expired (terminal); queued can
also go directly to expired if never started in time. A read
never shows a stale in-flight state: a job past expires_at,
or one seen running for more than 2 minutes (the background
run cannot live that long — it died), is moved to expired
by the read itself. expired carries error_code: "expired".
The job's input, exactly as it was created with (after
validation — unknown fields were rejected at submission). For
job_type: "query" this is the QueryRequest you sent.
The job's output. null until status is succeeded. For
job_type: "query" this is byte-for-byte the 200 body
POST /v1/query would have returned for the same request —
including structured_answer when response_format: "structured" was requested, and the optional deferral /
crisis / error fields with the same meaning. A request
/v1/query would have answered 502 becomes a failed job
instead (see error).
Generic human-readable failure message, present when status
is failed. Safe to display; do not parse — branch on
error_code. The messages are the same texts /v1/query
uses on its own error paths ("Generation failed. Please
retry.", "Research corpus temporarily unavailable. Please
retry.", …) — never an internal error, never your input echoed
back. This is enforced at the write boundary
(lib/jobs/store.ts's failJob), not merely promised: a
message that is empty, over 500 characters, or looks like it
could carry an internal detail is replaced with a generic one
before it is stored.
When this job and its result stop being readable. Set on every
job created through POST /v1/jobs (one hour after
submission). A job still queued/running at this instant is
expired on the next read. A job that already finished is
readable until this instant and not after: the first read
past expires_at returns 404 not_found and clears the stored
payload and result; the row is deleted within about 24
hours. Fetch your result before then. null only for jobs
created without a deadline (none today) — such a job is never
expired or cleared by the read path.
Machine-readable failure class — the field to branch on. Set
whenever status is failed or expired; null otherwise.
Bounded enum (also enforced by a database CHECK):
corpus_unavailable — the research corpus was unreachable
during execution. Transient: submit a new job with
backoff. (At submission time the same outage never occurs —
the corpus is not touched until the job runs.)
internal — an unexpected server-side error. Transient:
submit a new job with backoff; if it repeats, contact
support with the job id.
generation_failed — the pipeline ran but produced nothing
usable (the sync route's 502). Not transient in the retry
sense: retrying the identical question usually yields the
same outcome; rephrase or treat as unanswerable.
empty_query — the question was blank once normalised.
Fix the request.
invalid_payload — the stored request no longer passes
validation. Should never occur; contact support.
expired — the job hit its deadline or was seen running
for more than 2 minutes without finishing. Submit a new job.
How many times execution of this job was started. 0 while
queued; 1 once it is running or terminal. The current
runner never retries, so a value above 1 is not expected —
the field exists so that a future retrying runner needs no
contract change.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Collective X API","colors":{"primary":"#1a3f52","light":"#6aade4","dark":"#101828"},"logo":{"light":"https://mintcdn.com/collective-x/XmdndrtGclqPo6DR/logo/light.svg?fit=max\u0026auto=format\u0026n=XmdndrtGclqPo6DR\u0026q=85\u0026s=a7e95b40e54fc2a5bb8a468486937833","dark":"https://mintcdn.com/collective-x/XmdndrtGclqPo6DR/logo/dark.svg?fit=max\u0026auto=format\u0026n=XmdndrtGclqPo6DR\u0026q=85\u0026s=e49aacf5db1549f425bb1876eb3006ad"},"favicon":"https://mintcdn.com/collective-x/XmdndrtGclqPo6DR/favicon.png?fit=max\u0026auto=format\u0026n=XmdndrtGclqPo6DR\u0026q=85\u0026s=78206f470882a5ed0733d80506b8c90a","navbar":{"links":[{"href":"https://api.collectivex.health","label":"Status"}]},"footer":{},"contextual":{"options":["copy","view","chatgpt","claude","perplexity","mcp","cursor","vscode"],"display":"header"},"styling":{"latex":false},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"collective-x","hasAudioNarration":false,"actualSubdomain":"collective-x","customDomains":["docs.collectivex.health"],"gitSource":{"type":"github","owner":"collective-x","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"chroma":{"latestBuiltDeploymentHistoryId":"6aa635006cb71c753f702f70","collectionId":"75e7c7d0-413f-4f2e-8fac-bcec7f6e2d60","liveDeploymentHistoryId":"6aa635006cb71c753f702f70","schemaVersion":"v3"},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6aa635006cb71c753f702f70","clientVersion":"0.0.4006","searchFilterCounts":[{"language":"default","tag":"Guides","count":6},{"language":"default","tag":"API Reference","count":5},{"language":"default","tag":"Getting Started","count":3},{"language":"default","tag":"Features","count":2},{"language":"default","tag":"Async jobs","count":2},{"language":"default","tag":"Reference","count":1},{"language":"default","tag":"Ask a question","count":1},{"language":"default","tag":"Have a conversation","count":1},{"language":"default","tag":"Session tokens","count":1}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[],"page":{"pagePath":"api-reference/async-jobs/poll-an-async-jobs-status","slug":"/api-reference/async-jobs/poll-an-async-jobs-status","pageMetadata":{"title":"Poll an async job's status","description":"Read-only poll for one async job's current state. Jobs are\ncreated by `POST /v1/jobs` — see the \"Async jobs\" tag for the\nlifecycle, retention, and polling guidance.\n\n**Reading past the deadline.** If the job is still `queued` or\n`running` when its `expires_at` has passed — or it has been\n`running` for more than 2 minutes — this read marks it `expired`\nbefore returning it, so you never see a stale in-flight state.\nA job that had already finished (`succeeded` / `failed` /\n`expired`) is returned as it finished **until `expires_at`**;\nthe first read after that answers `404 not_found`, clears the\nstored request and result, and the row is deleted within ~24 h.\nFetch your result before `expires_at`.\n\n**Transient errors while polling.** A `429`, or a `503` that\ncarries `Retry-After`, says nothing about the job: wait\n`max(Retry-After, 1)` seconds and poll again.\n\n**Server-to-server only, with your long-lived key.** A short-lived\nsession token (see \"Mobile-direct access\" above) cannot call this\nendpoint: its scope covers only\n`/v1/query` and `/v1/chat`, so it gets `403 forbidden` here like on\nevery other endpoint. Do NOT call this endpoint from browser-side\ncode, and never embed your long-lived key in any client.\n\nScoping: a job is visible to every long-lived key of the partner\nthat created it (ownership is per partner, not per key). A job id\nthat doesn't exist and a job id that belongs to a different\npartner both return the identical `404 not_found` — this endpoint\nnever confirms another partner's job id exists.\n\nRate limits are communicated via `X-RateLimit-*` headers on every\nresponse and are shared with `/v1/query` and `/v1/chat` per API\nkey (see \"Rate limits\" above). When a 429 is returned, honour\n`Retry-After` before retrying.\n","deprecated":null,"version":null,"openapi":"/openapi.yaml get /v1/jobs/{id}","href":"/api-reference/async-jobs/poll-an-async-jobs-status","autogeneratedByOpenApi":true},"description":{"compiledSource":"\"use strict\";\nconst {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n code: \"code\",\n p: \"p\",\n strong: \"strong\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsxs(_Fragment, {\n children: [_jsxs(_components.p, {\n children: [\"Read-only poll for one async job’s current state. Jobs are\\ncreated by \", _jsx(_components.code, {\n children: \"POST /v1/jobs\"\n }), \" — see the “Async jobs” tag for the\\nlifecycle, retention, and polling guidance.\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [_jsx(_components.strong, {\n children: \"Reading past the deadline.\"\n }), \" If the job is still \", _jsx(_components.code, {\n children: \"queued\"\n }), \" or\\n\", _jsx(_components.code, {\n children: \"running\"\n }), \" when its \", _jsx(_components.code, {\n children: \"expires_at\"\n }), \" has passed — or it has been\\n\", _jsx(_components.code, {\n children: \"running\"\n }), \" for more than 2 minutes — this read marks it \", _jsx(_components.code, {\n children: \"expired\"\n }), \"\\nbefore returning it, so you never see a stale in-flight state.\\nA job that had already finished (\", _jsx(_components.code, {\n children: \"succeeded\"\n }), \" / \", _jsx(_components.code, {\n children: \"failed\"\n }), \" /\\n\", _jsx(_components.code, {\n children: \"expired\"\n }), \") is returned as it finished \", _jsxs(_components.strong, {\n children: [\"until \", _jsx(_components.code, {\n children: \"expires_at\"\n })]\n }), \";\\nthe first read after that answers \", _jsx(_components.code, {\n children: \"404 not_found\"\n }), \", clears the\\nstored request and result, and the row is deleted within ~24 h.\\nFetch your result before \", _jsx(_components.code, {\n children: \"expires_at\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [_jsx(_components.strong, {\n children: \"Transient errors while polling.\"\n }), \" A \", _jsx(_components.code, {\n children: \"429\"\n }), \", or a \", _jsx(_components.code, {\n children: \"503\"\n }), \" that\\ncarries \", _jsx(_components.code, {\n children: \"Retry-After\"\n }), \", says nothing about the job: wait\\n\", _jsx(_components.code, {\n children: \"max(Retry-After, 1)\"\n }), \" seconds and poll again.\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [_jsx(_components.strong, {\n children: \"Server-to-server only, with your long-lived key.\"\n }), \" A short-lived\\nsession token (see “Mobile-direct access” above) cannot call this\\nendpoint: its scope covers only\\n\", _jsx(_components.code, {\n children: \"/v1/query\"\n }), \" and \", _jsx(_components.code, {\n children: \"/v1/chat\"\n }), \", so it gets \", _jsx(_components.code, {\n children: \"403 forbidden\"\n }), \" here like on\\nevery other endpoint. Do NOT call this endpoint from browser-side\\ncode, and never embed your long-lived key in any client.\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"Scoping: a job is visible to every long-lived key of the partner\\nthat created it (ownership is per partner, not per key). A job id\\nthat doesn’t exist and a job id that belongs to a different\\npartner both return the identical \", _jsx(_components.code, {\n children: \"404 not_found\"\n }), \" — this endpoint\\nnever confirms another partner’s job id exists.\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"Rate limits are communicated via \", _jsx(_components.code, {\n children: \"X-RateLimit-*\"\n }), \" headers on every\\nresponse and are shared with \", _jsx(_components.code, {\n children: \"/v1/query\"\n }), \" and \", _jsx(_components.code, {\n children: \"/v1/chat\"\n }), \" per API\\nkey (see “Rate limits” above). When a 429 is returned, honour\\n\", _jsx(_components.code, {\n children: \"Retry-After\"\n }), \" before retrying.\"]\n })]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){return _jsx(_Fragment,{})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Poll an async job's status","description":"Read-only poll for one async job's current state. Jobs are\ncreated by `POST /v1/jobs` — see the \"Async jobs\" tag for the\nlifecycle, retention, and polling guidance.\n\n**Reading past the deadline.** If the job is still `queued` or\n`running` when its `expires_at` has passed — or it has been\n`running` for more than 2 minutes — this read marks it `expired`\nbefore returning it, so you never see a stale in-flight state.\nA job that had already finished (`succeeded` / `failed` /\n`expired`) is returned as it finished **until `expires_at`**;\nthe first read after that answers `404 not_found`, clears the\nstored request and result, and the row is deleted within ~24 h.\nFetch your result before `expires_at`.\n\n**Transient errors while polling.** A `429`, or a `503` that\ncarries `Retry-After`, says nothing about the job: wait\n`max(Retry-After, 1)` seconds and poll again.\n\n**Server-to-server only, with your long-lived key.** A short-lived\nsession token (see \"Mobile-direct access\" above) cannot call this\nendpoint: its scope covers only\n`/v1/query` and `/v1/chat`, so it gets `403 forbidden` here like on\nevery other endpoint. Do NOT call this endpoint from browser-side\ncode, and never embed your long-lived key in any client.\n\nScoping: a job is visible to every long-lived key of the partner\nthat created it (ownership is per partner, not per key). A job id\nthat doesn't exist and a job id that belongs to a different\npartner both return the identical `404 not_found` — this endpoint\nnever confirms another partner's job id exists.\n\nRate limits are communicated via `X-RateLimit-*` headers on every\nresponse and are shared with `/v1/query` and `/v1/chat` per API\nkey (see \"Rate limits\" above). When a 429 is returned, honour\n`Retry-After` before retrying.\n","deprecated":null,"version":null,"openapi":"/openapi.yaml get /v1/jobs/{id}","href":"/api-reference/async-jobs/poll-an-async-jobs-status","autogeneratedByOpenApi":true}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[],"codeExamples":{}},"jsLinks":[],"openApiReferenceData":{"operation":{"operationId":"getJob","summary":"Poll an async job's status","tags":["Async jobs"],"description":"Read-only poll for one async job's current state. Jobs are\ncreated by `POST /v1/jobs` — see the \"Async jobs\" tag for the\nlifecycle, retention, and polling guidance.\n\n**Reading past the deadline.** If the job is still `queued` or\n`running` when its `expires_at` has passed — or it has been\n`running` for more than 2 minutes — this read marks it `expired`\nbefore returning it, so you never see a stale in-flight state.\nA job that had already finished (`succeeded` / `failed` /\n`expired`) is returned as it finished **until `expires_at`**;\nthe first read after that answers `404 not_found`, clears the\nstored request and result, and the row is deleted within ~24 h.\nFetch your result before `expires_at`.\n\n**Transient errors while polling.** A `429`, or a `503` that\ncarries `Retry-After`, says nothing about the job: wait\n`max(Retry-After, 1)` seconds and poll again.\n\n**Server-to-server only, with your long-lived key.** A short-lived\nsession token (see \"Mobile-direct access\" above) cannot call this\nendpoint: its scope covers only\n`/v1/query` and `/v1/chat`, so it gets `403 forbidden` here like on\nevery other endpoint. Do NOT call this endpoint from browser-side\ncode, and never embed your long-lived key in any client.\n\nScoping: a job is visible to every long-lived key of the partner\nthat created it (ownership is per partner, not per key). A job id\nthat doesn't exist and a job id that belongs to a different\npartner both return the identical `404 not_found` — this endpoint\nnever confirms another partner's job id exists.\n\nRate limits are communicated via `X-RateLimit-*` headers on every\nresponse and are shared with `/v1/query` and `/v1/chat` per API\nkey (see \"Rate limits\" above). When a 429 is returned, honour\n`Retry-After` before retrying.\n","security":["40d228b0-24da-4fe5-964e-58e43a96a9c3"],"parameters":["8e86af3d-59ba-42b1-88a1-e25eaa665864"],"responses":{"200":"369eead9-9146-40cf-85d8-baddbcf98e95","400":"93eb1cda-e5ad-41d7-a9cf-4d4bdacadc5a","401":"e87d2931-6f99-4010-81be-16715756ed30","403":"f396bc3a-8f19-4389-9b14-53244e0b6be0","404":"449860eb-b738-4e12-900d-d2a60e5efa4b","429":"63d80cb7-cc9a-475e-8307-ad5cb0f20c0e","500":"67f064af-da56-4341-846a-bf122bacb880","503":"dd346b6b-3c6e-48cf-b309-89a125626d69"},"method":"get","type":"path","servers":["3aec66e8-ea38-412b-92a2-04eeb1a50e2f"],"path":"/v1/jobs/{id}","baseUrlOptions":["https://api.collectivex.health"],"title":"Poll an async job's status","codeSamples":[],"requiredOnlyExamples":false,"requestExampleType":"generated","responseExampleType":"generated"},"dependencies":{"parameters":{"8e86af3d-59ba-42b1-88a1-e25eaa665864":{"name":"id","in":"path","required":true,"description":"The job's id, as returned when it was created.","schema":{"type":"string","format":"uuid","isRequired":true,"description":"The job's id, as returned when it was created.","uniqueKey":"","typeLabel":"string\u003cuuid>"},"example":"8f14e45f-ceea-467e-adc0-7a1a6b5f6b28"}},"responses":{"369eead9-9146-40cf-85d8-baddbcf98e95":{"description":"OK. The job's current state.","headers":{"X-Request-Id":{"description":"UUID correlation key for this request.","required":true,"schema":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"}},"X-RateLimit-Limit":{"description":"Maximum requests per minute for this key (shared budget with /v1/query, /v1/chat, POST /v1/jobs, and /v1/session-tokens; per-key configurable, default 60).","required":true,"schema":{"type":"integer","example":60}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-minute rate-limit window.","required":true,"schema":{"type":"integer","example":59}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the per-minute rate-limit window resets.","required":true,"schema":{"type":"integer","example":1717000060}}},"content":{"application/json":{"schema":{"type":"object","description":"Current state of one async job. `payload` is the request the job\nwas created with and `result` is its output; both are typed per\n`job_type`. The only job type today is `query`, whose `payload` is\na `QueryRequest` and whose `result` is a `QueryResponse`.\n","required":["request_id","id","job_type","status","payload","result","error","expires_at","created_at","updated_at"],"properties":{"request_id":{"type":"string","format":"uuid","description":"UUID minted per request. Use for support correlation across PostHog / Langfuse / logs.","isRequired":true,"uniqueKey":"request_id","typeLabel":"string\u003cuuid>"},"id":{"type":"string","format":"uuid","description":"The job's own id (distinct from `request_id`, which identifies this poll request).","isRequired":true,"uniqueKey":"id","typeLabel":"string\u003cuuid>"},"job_type":{"type":"string","description":"Identifies what kind of work this job represents and therefore\nthe shape of `payload`/`result`. Today always `query` (an\nasynchronous `/v1/query`). Not an enum in this spec — a future\njob type would be an additive minor bump, with its own\npayload/result shape documented alongside.\n","example":"query","isRequired":true,"uniqueKey":"job_type","typeLabel":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","expired"],"description":"Current lifecycle state. `queued` → `running` → one of\n`succeeded` / `failed` / `expired` (terminal); `queued` can\nalso go directly to `expired` if never started in time. A read\nnever shows a stale in-flight state: a job past `expires_at`,\nor one seen `running` for more than 2 minutes (the background\nrun cannot live that long — it died), is moved to `expired`\nby the read itself. `expired` carries `error_code: \"expired\"`.\n","isRequired":true,"uniqueKey":"status","typeLabel":"enum\u003cstring>"},"payload":{"oneOf":[{"type":"object","description":"The job's input, exactly as it was created with (after\nvalidation — unknown fields were rejected at submission). For\n`job_type: \"query\"` this is the `QueryRequest` you sent.\n","required":["question"],"properties":{"question":{"type":"string","minLength":1,"description":"A single question, plain string. The preferred shape for /v1/query\ncallers — no message envelope needed. Internally wrapped into a\nsingle-element UIMessage before being passed to the same pipeline\nthat /v1/chat uses, so behavior is identical to sending that text as\nthe sole message on /v1/chat.\n","isRequired":true,"uniqueKey":"payload.oneOf.0.question","typeLabel":"string"},"messages":{"type":"array","description":"Single-element AI SDK v6 UIMessage array, provided for continuity\nwith callers already building /v1/chat request bodies who want to\nswitch endpoints with a minimal diff. Unlike /v1/chat, this array\nMUST contain exactly one message with role \"user\" — /v1/query has\nno multi-turn concept. Sending more than one message, or a message\nwith a different role, is a 400 invalid_request (use /v1/chat with\nstream:false instead if you need conversation history).\n","minItems":1,"maxItems":1,"items":{"type":"object","description":"AI SDK v6 UIMessage shape. id + role + parts are the load-bearing fields.\n`file` parts are rejected with `400 invalid_request` on every endpoint —\nthis API's contract is text questions in, grounded text answers out, and\nfile URLs are never forwarded upstream. Text parts (and, on replayed\nassistant history, the `data-*` parts our own stream emits) are accepted.\n","required":["id","role","parts"],"properties":{"id":{"type":"string","description":"Unique message identifier. AI SDK clients (e.g. useChat) auto-generate\nthis; raw HTTP callers MUST provide it (any opaque string — e.g. UUID,\npartner-side conversation row id). validateUIMessages rejects messages\nwithout an id.\n","isRequired":true,"uniqueKey":"payload.oneOf.0.messages.items.[INDEX].id","typeLabel":"string"},"role":{"type":"string","enum":["user","assistant","system"],"description":"Message sender role.","isRequired":true,"uniqueKey":"payload.oneOf.0.messages.items.[INDEX].role","typeLabel":"enum\u003cstring>"},"parts":{"type":"array","description":"Message content as an array of parts. Each part has a `type` field.\nFor user messages: use `{ \"type\": \"text\", \"text\": \"...\" }`.\nFor assistant messages: the API returns text-delta parts assembled into parts.\n","items":{"type":"object","description":"A single content part within a UIMessage.","required":["type"],"properties":{"type":{"type":"string","description":"Part type. Common values: \"text\", \"tool-invocation\", \"tool-result\".","isRequired":true,"uniqueKey":"payload.oneOf.0.messages.items.[INDEX].parts.items.[INDEX].type","typeLabel":"string"},"text":{"type":"string","description":"Text content (present when type is \"text\").","uniqueKey":"payload.oneOf.0.messages.items.[INDEX].parts.items.[INDEX].text","typeLabel":"string"}},"uniqueKey":"payload.oneOf.0.messages.items.[INDEX].parts.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"payload.oneOf.0.messages.items.[INDEX].parts","typeLabel":"object[]"}},"uniqueKey":"payload.oneOf.0.messages.items.[INDEX]","typeLabel":"object"},"uniqueKey":"payload.oneOf.0.messages","typeLabel":"object[]"},"response_format":{"type":"string","enum":["text","structured"],"default":"text","description":"**BETA.** When omitted or `\"text\"`, the response is byte-for-byte\nthe `SyncResponse` this endpoint has always returned — no behavior\nchange. When `\"structured\"`, the response ADDS a `structured_answer`\nfield (machine-readable sections/claims/citations) alongside the\nunchanged `answer`/`citations` fields; nothing is removed or\nreplaced. This field, and the shape it produces, may change in a\nfuture minor version while in beta.\n","uniqueKey":"payload.oneOf.0.response_format","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"payload.oneOf.0","typeLabel":"object"},{"type":"object","description":"The job's input, exactly as it was created with (after\nvalidation — unknown fields were rejected at submission). For\n`job_type: \"query\"` this is the `QueryRequest` you sent.\n","required":["messages"],"properties":{"messages":{"type":"array","description":"Single-element AI SDK v6 UIMessage array, provided for continuity\nwith callers already building /v1/chat request bodies who want to\nswitch endpoints with a minimal diff. Unlike /v1/chat, this array\nMUST contain exactly one message with role \"user\" — /v1/query has\nno multi-turn concept. Sending more than one message, or a message\nwith a different role, is a 400 invalid_request (use /v1/chat with\nstream:false instead if you need conversation history).\n","minItems":1,"maxItems":1,"items":{"type":"object","description":"AI SDK v6 UIMessage shape. id + role + parts are the load-bearing fields.\n`file` parts are rejected with `400 invalid_request` on every endpoint —\nthis API's contract is text questions in, grounded text answers out, and\nfile URLs are never forwarded upstream. Text parts (and, on replayed\nassistant history, the `data-*` parts our own stream emits) are accepted.\n","required":["id","role","parts"],"properties":{"id":{"type":"string","description":"Unique message identifier. AI SDK clients (e.g. useChat) auto-generate\nthis; raw HTTP callers MUST provide it (any opaque string — e.g. UUID,\npartner-side conversation row id). validateUIMessages rejects messages\nwithout an id.\n","isRequired":true,"uniqueKey":"payload.oneOf.1.messages.items.[INDEX].id","typeLabel":"string"},"role":{"type":"string","enum":["user","assistant","system"],"description":"Message sender role.","isRequired":true,"uniqueKey":"payload.oneOf.1.messages.items.[INDEX].role","typeLabel":"enum\u003cstring>"},"parts":{"type":"array","description":"Message content as an array of parts. Each part has a `type` field.\nFor user messages: use `{ \"type\": \"text\", \"text\": \"...\" }`.\nFor assistant messages: the API returns text-delta parts assembled into parts.\n","items":{"type":"object","description":"A single content part within a UIMessage.","required":["type"],"properties":{"type":{"type":"string","description":"Part type. Common values: \"text\", \"tool-invocation\", \"tool-result\".","isRequired":true,"uniqueKey":"payload.oneOf.1.messages.items.[INDEX].parts.items.[INDEX].type","typeLabel":"string"},"text":{"type":"string","description":"Text content (present when type is \"text\").","uniqueKey":"payload.oneOf.1.messages.items.[INDEX].parts.items.[INDEX].text","typeLabel":"string"}},"uniqueKey":"payload.oneOf.1.messages.items.[INDEX].parts.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"payload.oneOf.1.messages.items.[INDEX].parts","typeLabel":"object[]"}},"uniqueKey":"payload.oneOf.1.messages.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"payload.oneOf.1.messages","typeLabel":"object[]"},"question":{"type":"string","minLength":1,"description":"A single question, plain string. The preferred shape for /v1/query\ncallers — no message envelope needed. Internally wrapped into a\nsingle-element UIMessage before being passed to the same pipeline\nthat /v1/chat uses, so behavior is identical to sending that text as\nthe sole message on /v1/chat.\n","uniqueKey":"payload.oneOf.1.question","typeLabel":"string"},"response_format":{"type":"string","enum":["text","structured"],"default":"text","description":"**BETA.** When omitted or `\"text\"`, the response is byte-for-byte\nthe `SyncResponse` this endpoint has always returned — no behavior\nchange. When `\"structured\"`, the response ADDS a `structured_answer`\nfield (machine-readable sections/claims/citations) alongside the\nunchanged `answer`/`citations` fields; nothing is removed or\nreplaced. This field, and the shape it produces, may change in a\nfuture minor version while in beta.\n","uniqueKey":"payload.oneOf.1.response_format","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"payload.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"payload","typeLabel":"object"},"result":{"oneOf":[{"type":"null","description":"The job's output. `null` until `status` is `succeeded`. For\n`job_type: \"query\"` this is byte-for-byte the `200` body\n`POST /v1/query` would have returned for the same request —\nincluding `structured_answer` when `response_format:\n\"structured\"` was requested, and the optional `deferral` /\n`crisis` / `error` fields with the same meaning. A request\n`/v1/query` would have answered `502` becomes a `failed` job\ninstead (see `error`).\n","isRequired":true,"uniqueKey":"result.oneOf.0","typeLabel":"null"},{"type":"object","description":"The job's output. `null` until `status` is `succeeded`. For\n`job_type: \"query\"` this is byte-for-byte the `200` body\n`POST /v1/query` would have returned for the same request —\nincluding `structured_answer` when `response_format:\n\"structured\"` was requested, and the optional `deferral` /\n`crisis` / `error` fields with the same meaning. A request\n`/v1/query` would have answered `502` becomes a `failed` job\ninstead (see `error`).\n","required":["request_id","answer","citations"],"properties":{"request_id":{"type":"string","format":"uuid","description":"UUID minted per request. Use for support correlation across PostHog / Langfuse / logs.","isRequired":true,"uniqueKey":"result.oneOf.1.request_id","typeLabel":"string\u003cuuid>"},"answer":{"type":"string","description":"The model's answer string. Empty string (\"\") on deferral or crisis short-circuit\n(the pipeline refuses to generate an answer without grounded evidence).\n","isRequired":true,"uniqueKey":"result.oneOf.1.answer","typeLabel":"string"},"citations":{"type":"array","description":"Retrieved citations from the peer-reviewed corpus. Empty array on deferral or crisis.\nNever fabricate or paraphrase a citation that is not in this array.\n","items":{"type":"object","description":"A single research citation from the CollectiveX Health corpus.","required":["id","score","text"],"properties":{"id":{"type":"string","description":"Corpus-internal citation ID (e.g. \"CXH-00835\"). Use as an inline citation reference.","isRequired":true,"uniqueKey":"result.oneOf.1.citations.items.[INDEX].id","typeLabel":"string"},"score":{"type":"number","format":"float","minimum":0,"maximum":1,"description":"Retrieval relevance score. Higher is more relevant.","isRequired":true,"uniqueKey":"result.oneOf.1.citations.items.[INDEX].score","typeLabel":"number\u003cfloat>"},"text":{"type":"string","description":"Excerpt from the source paper used to ground the answer.","isRequired":true,"uniqueKey":"result.oneOf.1.citations.items.[INDEX].text","typeLabel":"string"},"metadata":{"type":"object","description":"Bibliographic metadata. Strict allowlist — fields not listed here are\nstripped at the `/v1/*` boundary so partners never see Agentset-internal\nfield names. Source of truth: `lib/api/v1/citation-sanitizer.ts`\n(`PARTNER_METADATA_ALLOWLIST`).\n","additionalProperties":false,"properties":{"cxh_id":{"type":"string","description":"Canonical paper identifier in the CollectiveX Health corpus.","example":"CXH-00835","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.cxh_id","typeLabel":"string"},"doi":{"type":"string","description":"Peer-reviewed DOI of the source paper.","example":"10.1093/humrep/dead156","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.doi","typeLabel":"string"},"year":{"type":"string","description":"Publication year as it appears in the corpus catalog. Emitted as a\nstring at runtime (#191) — partners that want a number should\n`parseInt(year, 10)` on the partner side. The corpus also includes\nnon-numeric values like `\"in press\"` for unpublished work.\n","example":"2023","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.year","typeLabel":"string"},"journal":{"type":"string","description":"Peer-review journal name.","example":"Human Reproduction","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.journal","typeLabel":"string"},"authors":{"oneOf":[{"type":"string","description":"Author list — either a single joined string or an array of\nauthor strings; both forms occur in corpus metadata.\n","example":"Shultz et al.","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.authors.oneOf.0","typeLabel":"string"},{"type":"array","items":{"type":"string","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.authors.oneOf.1.items.[INDEX]","typeLabel":"string"},"description":"Author list — either a single joined string or an array of\nauthor strings; both forms occur in corpus metadata.\n","example":"Shultz et al.","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.authors.oneOf.1","typeLabel":"string[]"}],"uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.authors","typeLabel":"string | string[]"},"title":{"type":"string","description":"Paper title.","example":"Sex hormone profiles in normal menstruating females","uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.title","typeLabel":"string"},"page_number":{"type":"integer","description":"Page within the source paper this excerpt came from.","example":4,"uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata.page_number","typeLabel":"integer"}},"uniqueKey":"result.oneOf.1.citations.items.[INDEX].metadata","typeLabel":"object"}},"uniqueKey":"result.oneOf.1.citations.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.citations","typeLabel":"object[]"},"deferral":{"oneOf":[{"type":"null","description":"Present when the corpus lacks strong evidence for the query. Present the originalQuery\nback to the user and do not synthesize an answer from training data.\n","uniqueKey":"result.oneOf.1.deferral.oneOf.0","typeLabel":"null"},{"type":"object","description":"Present when the corpus lacks strong evidence for the query. Present the originalQuery\nback to the user and do not synthesize an answer from training data.\n","required":["originalQuery"],"properties":{"originalQuery":{"type":"string","description":"The query that triggered the deferral.","isRequired":true,"uniqueKey":"result.oneOf.1.deferral.oneOf.1.originalQuery","typeLabel":"string"}},"uniqueKey":"result.oneOf.1.deferral.oneOf.1","typeLabel":"object"}],"uniqueKey":"result.oneOf.1.deferral","typeLabel":"null | object"},"crisis":{"oneOf":[{"type":"null","description":"Present when a safety classifier fires before retrieval (self-harm, acute symptoms,\nIPV). Show the resources verbatim and route the user away from research framing.\n","uniqueKey":"result.oneOf.1.crisis.oneOf.0","typeLabel":"null"},{"type":"object","description":"Present when a safety classifier fires before retrieval (self-harm, acute symptoms,\nIPV). Show the resources verbatim and route the user away from research framing.\n","required":["category","locale","originalQuery","resources"],"properties":{"category":{"type":"string","enum":["self-harm","acute-symptom","ipv"],"description":"Safety category that triggered the classifier.","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.category","typeLabel":"enum\u003cstring>"},"locale":{"type":"string","description":"Detected locale (e.g. \"US\", \"GB\"). Used to select region-appropriate resources.","example":"US","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.locale","typeLabel":"string"},"originalQuery":{"type":"string","description":"The query that triggered the crisis classifier.","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.originalQuery","typeLabel":"string"},"resources":{"type":"array","description":"Crisis resources to show verbatim. Never omit or reorder.","items":{"type":"object","description":"A single crisis support resource. Render verbatim — do not reorder, summarise, or\nomit fields. Source of truth: `lib/safety/crisis-resources.ts`.\n","required":["region","name","description"],"properties":{"region":{"type":"string","enum":["US","UK","global"],"description":"Region this resource applies to.","example":"US","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX].region","typeLabel":"enum\u003cstring>"},"name":{"type":"string","description":"Human-readable resource name.","example":"988 Suicide \u0026 Crisis Lifeline","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX].name","typeLabel":"string"},"description":{"type":"string","description":"Short context line shown next to the resource.","example":"24/7 free, confidential support for people in distress.","isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX].description","typeLabel":"string"},"phone":{"type":"string","description":"Phone or SMS contact (optional). Format varies by region.","example":"988","uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX].phone","typeLabel":"string"},"hours":{"type":"string","description":"Operating hours, when not 24/7 (optional).","example":"Mon–Fri 9am–5pm ET","uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX].hours","typeLabel":"string"}},"uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.crisis.oneOf.1.resources","typeLabel":"object[]"}},"uniqueKey":"result.oneOf.1.crisis.oneOf.1","typeLabel":"object"}],"uniqueKey":"result.oneOf.1.crisis","typeLabel":"null | object"},"error":{"description":"Present when the generation pipeline emitted an error (v1.1, optional). The message\nis always a generic sanitized string — internal error details are never exposed.\nWith no usable content the response status is 502; alongside a partial answer or a\ndeferral/crisis short-circuit the status stays 200 and this field flags the failure.\n","type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Generic human-readable failure message. Safe to display; do not parse.","example":"Generation failed. Please retry.","isRequired":true,"uniqueKey":"result.oneOf.1.error.message","typeLabel":"string"}},"uniqueKey":"result.oneOf.1.error","typeLabel":"object"},"structured_answer":{"type":"object","description":"**BETA.** Present only when the request set\n`response_format: \"structured\"` (see `QueryRequest.response_format`).\nFor structured-mode consumers, `structured_answer.status` is the\nauthoritative outcome — do not also branch on the legacy\ntop-level `answer`/`error` fields, which keep text-mode\nsemantics unchanged and can disagree with `status` on\ntruncated generations.\nAbsent — not null — on every request that omits it or sends\n`response_format: \"text\"`, which is every request today: this field\nis purely additive and never changes the meaning of `answer` or\n`citations`.\n","required":["format","format_version","schema_version","status","sections","citations","deferral","crisis","error","disclaimer"],"properties":{"format":{"type":"string","description":"Which adapter rendered this payload. `\"generic\"` is the self-service public shape.","example":"generic","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.format","typeLabel":"string"},"format_version":{"type":"string","description":"Bumped when this adapter's rendered shape changes in a breaking way.","example":"1","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.format_version","typeLabel":"string"},"schema_version":{"type":"string","description":"Canonical answer schema version (`lib/partner-format/canonical.ts`).","example":"1","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.schema_version","typeLabel":"string"},"status":{"type":"string","enum":["answered","ungrounded","deferral","crisis","error"],"description":"REQUIRED discriminant. `\"answered\"` — a grounded answer with at\nleast one citation. `\"ungrounded\"` — a real, non-empty answer with\nzero citations; kept distinct from `\"answered\"` so a partner can\nchoose not to present it as evidence-backed. `\"deferral\"` — the\ncorpus lacked strong evidence; no answer was synthesized.\n`\"crisis\"` — the crisis classifier short-circuited before\nretrieval. `\"error\"` — the pipeline emitted an error chunk\n(model/execute failure, mid-stream or outright); `sections` and\n`citations` are always empty in this state — a truncated or\npartial answer is never packaged as a grounded claim, even when\nthe top-level `answer` field (unaffected by `response_format`)\nstill carries a partial fragment for a text-mode caller.\n","example":"answered","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.status","typeLabel":"enum\u003cstring>"},"sections":{"type":"array","description":"The answer broken into sections. Today's pipeline emits one markdown\nanswer with no section boundaries, so a normal answer is always a\nsingle-element array; the shape supports more so a future\nmulti-section generation step needs no new schema. Empty when\n`status` is `\"error\"`; exactly one synthetic section when `status`\nis `\"deferral\"` or `\"crisis\"` (see `StructuredSection.kind`).\n","items":{"type":"object","description":"A single section of a structured answer.","required":["id","kind","heading","body","claims"],"properties":{"id":{"type":"string","example":"section-1","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].id","typeLabel":"string"},"kind":{"type":"string","enum":["answer","crisis","deferral"],"description":"`\"answer\"` is real, model-generated or corpus-grounded content.\n`\"crisis\"` / `\"deferral\"` are synthetic sections carrying\npre-approved safety copy — present so a client that renders\n`sections[].body` and nothing else still shows something instead\nof a blank card. `StructuredAnswer.status` is still the\nauthoritative discriminant; `kind` exists for section-only\nrenderers.\n","example":"answer","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].kind","typeLabel":"enum\u003cstring>"},"heading":{"oneOf":[{"type":"string","description":"Null when the answer has no distinct heading (today's default).","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].heading.oneOf.0","typeLabel":"string"},{"type":"null","description":"Null when the answer has no distinct heading (today's default).","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].heading.oneOf.1","typeLabel":"null"}],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].heading","typeLabel":"string | null"},"body":{"type":"string","description":"Full markdown/plain text body of this section.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].body","typeLabel":"string"},"claims":{"type":"array","description":"Claims within this section, each bound to the citation ids that\nground it. Today's pipeline has no per-claim generation step, so\nthis is at most one claim covering the whole section body, bound to\nevery citation returned for the answer — never a fabricated\nsentence-level binding. A section with no supporting citations has\nzero claims, not a claim with an empty `citation_ids`. Always empty\non a `\"crisis\"`/`\"deferral\"` `kind` section.\n","items":{"type":"object","required":["id","text","citation_ids"],"properties":{"id":{"type":"string","example":"claim-1","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims.items.[INDEX].id","typeLabel":"string"},"text":{"type":"string","description":"The claim text (today, the full section body).","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims.items.[INDEX].text","typeLabel":"string"},"citation_ids":{"type":"array","description":"Ids into this response's `citations` array that ground this claim.","items":{"type":"string","uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims.items.[INDEX].citation_ids.items.[INDEX]","typeLabel":"string"},"example":["CXH-00835"],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims.items.[INDEX].citation_ids","typeLabel":"string[]"}},"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX].claims","typeLabel":"object[]"}},"uniqueKey":"result.oneOf.1.structured_answer.sections.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.sections","typeLabel":"object[]"},"citations":{"type":"array","description":"Same citations as the top-level `citations` field, reshaped through\nthe same `Citation` schema — no new citation shape, no new sanitizer.\nEmpty unless `status` is `\"answered\"` or `\"ungrounded\"`.\n","items":{"type":"object","description":"A single research citation from the CollectiveX Health corpus.","required":["id","score","text"],"properties":{"id":{"type":"string","description":"Corpus-internal citation ID (e.g. \"CXH-00835\"). Use as an inline citation reference.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].id","typeLabel":"string"},"score":{"type":"number","format":"float","minimum":0,"maximum":1,"description":"Retrieval relevance score. Higher is more relevant.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].score","typeLabel":"number\u003cfloat>"},"text":{"type":"string","description":"Excerpt from the source paper used to ground the answer.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].text","typeLabel":"string"},"metadata":{"type":"object","description":"Bibliographic metadata. Strict allowlist — fields not listed here are\nstripped at the `/v1/*` boundary so partners never see Agentset-internal\nfield names. Source of truth: `lib/api/v1/citation-sanitizer.ts`\n(`PARTNER_METADATA_ALLOWLIST`).\n","additionalProperties":false,"properties":{"cxh_id":{"type":"string","description":"Canonical paper identifier in the CollectiveX Health corpus.","example":"CXH-00835","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.cxh_id","typeLabel":"string"},"doi":{"type":"string","description":"Peer-reviewed DOI of the source paper.","example":"10.1093/humrep/dead156","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.doi","typeLabel":"string"},"year":{"type":"string","description":"Publication year as it appears in the corpus catalog. Emitted as a\nstring at runtime (#191) — partners that want a number should\n`parseInt(year, 10)` on the partner side. The corpus also includes\nnon-numeric values like `\"in press\"` for unpublished work.\n","example":"2023","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.year","typeLabel":"string"},"journal":{"type":"string","description":"Peer-review journal name.","example":"Human Reproduction","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.journal","typeLabel":"string"},"authors":{"oneOf":[{"type":"string","description":"Author list — either a single joined string or an array of\nauthor strings; both forms occur in corpus metadata.\n","example":"Shultz et al.","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.authors.oneOf.0","typeLabel":"string"},{"type":"array","items":{"type":"string","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.authors.oneOf.1.items.[INDEX]","typeLabel":"string"},"description":"Author list — either a single joined string or an array of\nauthor strings; both forms occur in corpus metadata.\n","example":"Shultz et al.","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.authors.oneOf.1","typeLabel":"string[]"}],"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.authors","typeLabel":"string | string[]"},"title":{"type":"string","description":"Paper title.","example":"Sex hormone profiles in normal menstruating females","uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.title","typeLabel":"string"},"page_number":{"type":"integer","description":"Page within the source paper this excerpt came from.","example":4,"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata.page_number","typeLabel":"integer"}},"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX].metadata","typeLabel":"object"}},"uniqueKey":"result.oneOf.1.structured_answer.citations.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.citations","typeLabel":"object[]"},"deferral":{"oneOf":[{"type":"null","description":"Same semantics as the top-level `deferral` field, mirrored here.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.deferral.oneOf.0","typeLabel":"null"},{"type":"object","description":"Same semantics as the top-level `deferral` field, mirrored here.","required":["originalQuery"],"properties":{"originalQuery":{"type":"string","description":"The query that triggered the deferral.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.deferral.oneOf.1.originalQuery","typeLabel":"string"}},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.deferral.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.deferral","typeLabel":"null | object"},"crisis":{"oneOf":[{"type":"null","description":"Same semantics as the top-level `crisis` field, mirrored here.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.0","typeLabel":"null"},{"type":"object","description":"Same semantics as the top-level `crisis` field, mirrored here.","required":["category","locale","originalQuery","resources"],"properties":{"category":{"type":"string","enum":["self-harm","acute-symptom","ipv"],"description":"Safety category that triggered the classifier.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.category","typeLabel":"enum\u003cstring>"},"locale":{"type":"string","description":"Detected locale (e.g. \"US\", \"GB\"). Used to select region-appropriate resources.","example":"US","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.locale","typeLabel":"string"},"originalQuery":{"type":"string","description":"The query that triggered the crisis classifier.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.originalQuery","typeLabel":"string"},"resources":{"type":"array","description":"Crisis resources to show verbatim. Never omit or reorder.","items":{"type":"object","description":"A single crisis support resource. Render verbatim — do not reorder, summarise, or\nomit fields. Source of truth: `lib/safety/crisis-resources.ts`.\n","required":["region","name","description"],"properties":{"region":{"type":"string","enum":["US","UK","global"],"description":"Region this resource applies to.","example":"US","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX].region","typeLabel":"enum\u003cstring>"},"name":{"type":"string","description":"Human-readable resource name.","example":"988 Suicide \u0026 Crisis Lifeline","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX].name","typeLabel":"string"},"description":{"type":"string","description":"Short context line shown next to the resource.","example":"24/7 free, confidential support for people in distress.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX].description","typeLabel":"string"},"phone":{"type":"string","description":"Phone or SMS contact (optional). Format varies by region.","example":"988","uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX].phone","typeLabel":"string"},"hours":{"type":"string","description":"Operating hours, when not 24/7 (optional).","example":"Mon–Fri 9am–5pm ET","uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX].hours","typeLabel":"string"}},"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources.items.[INDEX]","typeLabel":"object"},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1.resources","typeLabel":"object[]"}},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.crisis","typeLabel":"null | object"},"error":{"oneOf":[{"type":"null","description":"Populated only when `status` is `\"error\"`; `null` otherwise.\nMirrors the top-level `error` field verbatim — the same generic,\nsanitized message, never raw internal error text.\n","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.error.oneOf.0","typeLabel":"null"},{"type":"object","description":"Populated only when `status` is `\"error\"`; `null` otherwise.\nMirrors the top-level `error` field verbatim — the same generic,\nsanitized message, never raw internal error text.\n","required":["message"],"properties":{"message":{"type":"string","description":"Generic, sanitized failure message — safe to display, never parsed.","example":"Generation failed. Please retry.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.error.oneOf.1.message","typeLabel":"string"}},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.error.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.error","typeLabel":"null | object"},"disclaimer":{"oneOf":[{"type":"null","description":"A versioned \"research summary, not medical advice\" notice.\nPopulated only when `status` is `\"answered\"` or `\"ungrounded\"`\n(i.e. `sections` carries real answer content); `null` for\ndeferral/crisis/error, which carry their own dedicated safety\nframing instead.\n","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.disclaimer.oneOf.0","typeLabel":"null"},{"type":"object","description":"A versioned \"research summary, not medical advice\" notice.\nPopulated only when `status` is `\"answered\"` or `\"ungrounded\"`\n(i.e. `sections` carries real answer content); `null` for\ndeferral/crisis/error, which carry their own dedicated safety\nframing instead.\n","required":["text","version"],"properties":{"text":{"type":"string","example":"Research summary, not medical advice. Consult your clinician for personal health decisions.","isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.disclaimer.oneOf.1.text","typeLabel":"string"},"version":{"type":"integer","description":"Bumped whenever the disclaimer copy changes.","example":1,"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.disclaimer.oneOf.1.version","typeLabel":"integer"}},"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.disclaimer.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"result.oneOf.1.structured_answer.disclaimer","typeLabel":"null | object"}},"uniqueKey":"result.oneOf.1.structured_answer","typeLabel":"object"}},"isRequired":true,"uniqueKey":"result.oneOf.1","typeLabel":"object"}],"isRequired":true,"uniqueKey":"result","typeLabel":"null | object"},"error":{"oneOf":[{"type":"null","description":"Generic human-readable failure message, present when `status`\nis `failed`. Safe to display; **do not parse — branch on\n`error_code`**. The messages are the same texts `/v1/query`\nuses on its own error paths (\"Generation failed. Please\nretry.\", \"Research corpus temporarily unavailable. Please\nretry.\", …) — never an internal error, never your input echoed\nback. This is enforced at the write boundary\n(`lib/jobs/store.ts`'s `failJob`), not merely promised: a\nmessage that is empty, over 500 characters, or looks like it\ncould carry an internal detail is replaced with a generic one\nbefore it is stored.\n","isRequired":true,"uniqueKey":"error.oneOf.0","typeLabel":"null"},{"type":"string","description":"Generic human-readable failure message, present when `status`\nis `failed`. Safe to display; **do not parse — branch on\n`error_code`**. The messages are the same texts `/v1/query`\nuses on its own error paths (\"Generation failed. Please\nretry.\", \"Research corpus temporarily unavailable. Please\nretry.\", …) — never an internal error, never your input echoed\nback. This is enforced at the write boundary\n(`lib/jobs/store.ts`'s `failJob`), not merely promised: a\nmessage that is empty, over 500 characters, or looks like it\ncould carry an internal detail is replaced with a generic one\nbefore it is stored.\n","maxLength":500,"isRequired":true,"uniqueKey":"error.oneOf.1","typeLabel":"string"}],"isRequired":true,"uniqueKey":"error","typeLabel":"null | string"},"expires_at":{"oneOf":[{"type":"null","description":"When this job and its result stop being readable. Set on every\njob created through `POST /v1/jobs` (one hour after\nsubmission). A job still `queued`/`running` at this instant is\nexpired on the next read. A job that already finished is\nreadable until this instant and **not after**: the first read\npast `expires_at` returns `404 not_found` and clears the stored\n`payload` and `result`; the row is deleted within about 24\nhours. Fetch your result before then. `null` only for jobs\ncreated without a deadline (none today) — such a job is never\nexpired or cleared by the read path.\n","isRequired":true,"uniqueKey":"expires_at.oneOf.0","typeLabel":"null"},{"type":"string","description":"When this job and its result stop being readable. Set on every\njob created through `POST /v1/jobs` (one hour after\nsubmission). A job still `queued`/`running` at this instant is\nexpired on the next read. A job that already finished is\nreadable until this instant and **not after**: the first read\npast `expires_at` returns `404 not_found` and clears the stored\n`payload` and `result`; the row is deleted within about 24\nhours. Fetch your result before then. `null` only for jobs\ncreated without a deadline (none today) — such a job is never\nexpired or cleared by the read path.\n","format":"date-time","isRequired":true,"uniqueKey":"expires_at.oneOf.1","typeLabel":"string\u003cdate-time>"}],"isRequired":true,"uniqueKey":"expires_at","typeLabel":"null | string\u003cdate-time>"},"created_at":{"type":"string","format":"date-time","description":"When the job was created.","isRequired":true,"uniqueKey":"created_at","typeLabel":"string\u003cdate-time>"},"updated_at":{"type":"string","format":"date-time","description":"When the job's status/result last changed.","isRequired":true,"uniqueKey":"updated_at","typeLabel":"string\u003cdate-time>"},"error_code":{"oneOf":[{"type":"null","description":"Machine-readable failure class — the field to branch on. Set\nwhenever `status` is `failed` or `expired`; `null` otherwise.\nBounded enum (also enforced by a database CHECK):\n\n- `corpus_unavailable` — the research corpus was unreachable\n during execution. **Transient: submit a new job with\n backoff.** (At submission time the same outage never occurs —\n the corpus is not touched until the job runs.)\n- `internal` — an unexpected server-side error. **Transient:\n submit a new job with backoff**; if it repeats, contact\n support with the job id.\n- `generation_failed` — the pipeline ran but produced nothing\n usable (the sync route's `502`). Not transient in the retry\n sense: retrying the identical question usually yields the\n same outcome; rephrase or treat as unanswerable.\n- `empty_query` — the question was blank once normalised.\n Fix the request.\n- `invalid_payload` — the stored request no longer passes\n validation. Should never occur; contact support.\n- `expired` — the job hit its deadline or was seen `running`\n for more than 2 minutes without finishing. Submit a new job.\n","uniqueKey":"error_code.oneOf.0","typeLabel":"null"},{"type":"string","description":"Machine-readable failure class — the field to branch on. Set\nwhenever `status` is `failed` or `expired`; `null` otherwise.\nBounded enum (also enforced by a database CHECK):\n\n- `corpus_unavailable` — the research corpus was unreachable\n during execution. **Transient: submit a new job with\n backoff.** (At submission time the same outage never occurs —\n the corpus is not touched until the job runs.)\n- `internal` — an unexpected server-side error. **Transient:\n submit a new job with backoff**; if it repeats, contact\n support with the job id.\n- `generation_failed` — the pipeline ran but produced nothing\n usable (the sync route's `502`). Not transient in the retry\n sense: retrying the identical question usually yields the\n same outcome; rephrase or treat as unanswerable.\n- `empty_query` — the question was blank once normalised.\n Fix the request.\n- `invalid_payload` — the stored request no longer passes\n validation. Should never occur; contact support.\n- `expired` — the job hit its deadline or was seen `running`\n for more than 2 minutes without finishing. Submit a new job.\n","enum":["generation_failed","corpus_unavailable","empty_query","invalid_payload","internal","expired"],"uniqueKey":"error_code.oneOf.1","typeLabel":"enum\u003cstring>"}],"uniqueKey":"error_code","typeLabel":"null | enum\u003cstring>"},"attempts":{"type":"integer","minimum":0,"description":"How many times execution of this job was started. `0` while\n`queued`; `1` once it is `running` or terminal. The current\nrunner never retries, so a value above `1` is not expected —\nthe field exists so that a future retrying runner needs no\ncontract change.\n","uniqueKey":"attempts","typeLabel":"integer"},"started_at":{"oneOf":[{"type":"null","description":"When execution last started. `null` while `queued`.","uniqueKey":"started_at.oneOf.0","typeLabel":"null"},{"type":"string","description":"When execution last started. `null` while `queued`.","format":"date-time","uniqueKey":"started_at.oneOf.1","typeLabel":"string\u003cdate-time>"}],"uniqueKey":"started_at","typeLabel":"null | string\u003cdate-time>"}},"uniqueKey":"","typeLabel":"object"},"examples":{"succeeded_job":{"title":"succeeded_job","value":{"request_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","id":"8f14e45f-ceea-467e-adc0-7a1a6b5f6b28","job_type":"query","status":"succeeded","payload":{"question":"What does the research say about magnesium for perimenopause sleep?"},"result":{"request_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","answer":"Several trials suggest magnesium may modestly improve sleep quality… [1]","citations":[{"id":"chunk_abc123","score":0.87,"text":"Magnesium supplementation improved subjective sleep quality…","metadata":{"title":"Magnesium and sleep in midlife women","doi":"10.1000/example.doi","year":"2023"}}]},"error":null,"error_code":null,"expires_at":"2026-09-11T13:00:00.000Z","attempts":1,"started_at":"2026-09-11T12:00:00.400Z","created_at":"2026-09-11T12:00:00.000Z","updated_at":"2026-09-11T12:00:23.000Z"}},"running_job":{"title":"running_job","value":{"request_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","id":"8f14e45f-ceea-467e-adc0-7a1a6b5f6b28","job_type":"query","status":"running","payload":{"question":"What does the research say about magnesium for perimenopause sleep?"},"result":null,"error":null,"error_code":null,"expires_at":"2026-09-11T13:00:00.000Z","attempts":1,"started_at":"2026-09-11T12:00:00.400Z","created_at":"2026-09-11T12:00:00.000Z","updated_at":"2026-09-11T12:00:00.400Z"}},"failed_job":{"title":"failed_job","value":{"request_id":"c3d4e5f6-a7b8-9012-cdef-123456789012","id":"8f14e45f-ceea-467e-adc0-7a1a6b5f6b28","job_type":"query","status":"failed","payload":{"question":"What does the research say about magnesium for perimenopause sleep?"},"result":null,"error":"Generation failed. Please retry.","error_code":"generation_failed","expires_at":"2026-09-11T13:00:00.000Z","attempts":1,"started_at":"2026-09-11T12:00:00.400Z","created_at":"2026-09-11T12:00:00.000Z","updated_at":"2026-09-11T12:00:31.000Z"}},"expired_job":{"title":"expired_job","value":{"request_id":"d4e5f6a7-b8c9-0123-def1-234567890123","id":"8f14e45f-ceea-467e-adc0-7a1a6b5f6b28","job_type":"query","status":"expired","payload":{"question":"What does the research say about magnesium for perimenopause sleep?"},"result":null,"error":null,"error_code":"expired","expires_at":"2026-09-11T13:00:00.000Z","attempts":1,"started_at":"2026-09-11T12:00:00.400Z","created_at":"2026-09-11T12:00:00.000Z","updated_at":"2026-09-11T12:02:10.000Z"}}},"exampleType":"examples"}},"exampleType":"examples"},"93eb1cda-e5ad-41d7-a9cf-4d4bdacadc5a":{"description":"400 Bad Request. `id` is not a valid UUID. `partner_jobs.id`\nis a `uuid` database column, so a malformed id is rejected\nbefore any query runs.\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"invalid_request","message":"Invalid job id. Expected a UUID.","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"invalid_request","message":"Invalid job id. Expected a UUID.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"e87d2931-6f99-4010-81be-16715756ed30":{"description":"401 Unauthorized. Missing, malformed, or invalid API key. Check that the Authorization\nheader is present and has the form `Bearer \u003ckey>`. An EXPIRED session token also\nreturns 401, with an \"API key expired\" message — session-token clients should treat\nthat specific message as \"request a fresh token from your backend\", not as an alert.\nSession tokens additionally return 401 whenever the session-token feature is disabled\n(the feature flag is a kill switch for outstanding keys).\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer \u003ckey>","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer \u003ckey>","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"f396bc3a-8f19-4389-9b14-53244e0b6be0":{"description":"403 Forbidden. Key is valid but does not have permission for this operation. Four\ncases, distinguishable by `error.message`: (1) the key's scope excludes this\nendpoint — e.g. a session token calling anything other than /v1/query or /v1/chat,\nincluding /v1/session-tokens itself; (2) the key's total request allowance is\nexhausted (\"…allowance is exhausted. Mint a new session token.\" — permanent for\nthat key, do not retry; mint a new token); (3) \"This session token is not usable.\nMint a new one.\" — a SERVER-SIDE provisioning fault, not a client mistake: do NOT\nre-mint in a loop (each mint is a metered request and the replacement will fail the\nsame way) — surface the error and contact partners@collectivex.health; (4) an\naccount-level restriction — contact partners@collectivex.health.\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"forbidden","message":"This key does not have permission to access /v1/chat.","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"forbidden","message":"This key does not have permission to access /v1/chat.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"449860eb-b738-4e12-900d-d2a60e5efa4b":{"description":"404 Not Found. One of three cases, indistinguishable by design\n(`GET /v1/jobs/{id}` never confirms another partner's job id\nexists): no job exists with this id; the job belongs to a\ndifferent partner identity (ownership is per partner, not per\nkey — any of your long-lived keys sees your jobs); or the job\nhas passed its `expires_at` and is no longer retained. There is\nnothing to retry — submit a new job if you still need the answer.\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"not_found","message":"Job not found.","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"not_found","message":"Job not found.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"63d80cb7-cc9a-475e-8307-ad5cb0f20c0e":{"description":"429 Too Many Requests. Rate limit exceeded. Read the Retry-After header (seconds)\nbefore retrying. Also check X-RateLimit-Remaining on successful responses to\nself-throttle before hitting 429. Limits are per-key configurable (a partner may\nhave a non-default requests-per-minute ceiling and/or a requests-per-day ceiling);\nX-RateLimit-* on this response always reflects whichever budget was exceeded.\n","headers":{"Retry-After":{"description":"Seconds until the rate-limit window resets. Wait this long before retrying.","required":true,"schema":{"type":"integer","example":37}},"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}},"X-RateLimit-Limit":{"description":"Maximum requests for the window that was exceeded (per-minute by default, or per-day if this key has a daily budget and that's what was hit).","schema":{"type":"integer","example":60}},"X-RateLimit-Remaining":{"description":"Requests remaining (0 when rate-limited).","schema":{"type":"integer","example":0}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after 37s.","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after 37s.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"67f064af-da56-4341-846a-bf122bacb880":{"description":"500 Internal Server Error. Unexpected server-side failure. Retry with exponential\nbackoff. If persistent, contact partners@collectivex.health with the request_id.\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Please retry.","request_id":"550e8400-e29b-41d4-a716-446655440000"}},"examples":{"Example":{"title":"Example","value":{"error":{"code":"internal_error","message":"An unexpected error occurred. Please retry.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"exampleType":"examples"}},"exampleType":"examples"},"dd346b6b-3c6e-48cf-b309-89a125626d69":{"description":"503 Service Unavailable. A transient condition — retry with exponential\nbackoff. Two distinct causes share this status code, distinguishable by\n`error.code`:\n - `corpus_unavailable`: the research corpus (Agentset) is temporarily\n unavailable.\n - `internal_error`: an upstream dependency required to authenticate or\n rate-limit the request (Unkey) is temporarily unavailable — includes\n the \"rate-limit service degraded\" case described under \"Rate limits\"\n above. This variant always carries a `Retry-After` header.\n","headers":{"X-Request-Id":{"description":"Request UUID.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"Seconds to wait before retrying. Present on the `internal_error` (Unkey-outage) variant; absent on `corpus_unavailable`.","required":false,"schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"type":"object","description":"Standard partner API error envelope (D-09). Mirrors Anthropic/OpenAI error shape.\nParse `error.code` for machine-readable error classification.\n","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Machine-readable error code (D-10 locked enum). Use this for branching logic.\nAdding new codes is a minor version bump per the versioning policy.\n","enum":["unauthorized","forbidden","rate_limited","invalid_request","corpus_unavailable","grounding_deferral","crisis_resources","not_found","internal_error"],"isRequired":true,"uniqueKey":"error.code","typeLabel":"enum\u003cstring>"},"message":{"type":"string","description":"Human-readable error description. Use for logging; do not parse programmatically.","isRequired":true,"uniqueKey":"error.message","typeLabel":"string"},"request_id":{"type":"string","format":"uuid","description":"UUID for this request. Include when contacting support.","isRequired":true,"uniqueKey":"error.request_id","typeLabel":"string\u003cuuid>"}},"isRequired":true,"uniqueKey":"error","typeLabel":"object"}},"uniqueKey":"","typeLabel":"object"},"examples":{"corpus_unavailable":{"title":"corpus_unavailable","value":{"error":{"code":"corpus_unavailable","message":"Research corpus temporarily unavailable. Please retry.","request_id":"550e8400-e29b-41d4-a716-446655440000"}}},"auth_or_ratelimit_service_unavailable":{"title":"auth_or_ratelimit_service_unavailable","value":{"error":{"code":"internal_error","message":"Rate limit service temporarily unavailable. Please retry.","request_id":"660e8400-e29b-41d4-a716-446655440001"}}}},"exampleType":"examples"}},"exampleType":"examples"}},"security":{"40d228b0-24da-4fe5-964e-58e43a96a9c3":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"cxh_pk_live_*","description":"Bearer token used by every `/v1/*` endpoint. Two kinds of key work here:\n - **Long-lived partner key** (`cxh_pk_live_*`) — issued by\n CollectiveX Health (contact partners@collectivex.health). Keep\n it server-side only — never expose it in client bundles or\n mobile app code. This rule is absolute and does not change for\n any endpoint.\n - **Short-lived session key** — minted server-side via\n `POST /v1/session-tokens` from a long-lived key. Safe to hand\n to a native mobile client only — not a browser or other\n JavaScript context, see \"API keys\" in the description above.\n It's scoped to `/v1/query` and `/v1/chat` only, expires in at\n most 60 minutes, and can't mint further session keys.\n `POST /v1/session-tokens` itself only ever accepts the\n long-lived key.\n","scopes":[]}}},"servers":{"3aec66e8-ea38-412b-92a2-04eeb1a50e2f":{"url":"https://api.collectivex.health","description":"Production partner API"}},"processedSecurityOptions":[{"schemes":[{"scheme":{"type":"http","scheme":"bearer","bearerFormat":"cxh_pk_live_*","description":"Bearer token used by every `/v1/*` endpoint. Two kinds of key work here:\n - **Long-lived partner key** (`cxh_pk_live_*`) — issued by\n CollectiveX Health (contact partners@collectivex.health). Keep\n it server-side only — never expose it in client bundles or\n mobile app code. This rule is absolute and does not change for\n any endpoint.\n - **Short-lived session key** — minted server-side via\n `POST /v1/session-tokens` from a long-lived key. Safe to hand\n to a native mobile client only — not a browser or other\n JavaScript context, see \"API keys\" in the description above.\n It's scoped to `/v1/query` and `/v1/chat` only, expires in at\n most 60 minutes, and can't mint further session keys.\n `POST /v1/session-tokens` itself only ever accepts the\n long-lived key.\n","scopes":[]},"schema":{"title":"Bearer Auth","type":"string","isRequired":true,"description":"Bearer token used by every `/v1/*` endpoint. Two kinds of key work here:\n - **Long-lived partner key** (`cxh_pk_live_*`) — issued by\n CollectiveX Health (contact partners@collectivex.health). Keep\n it server-side only — never expose it in client bundles or\n mobile app code. This rule is absolute and does not change for\n any endpoint.\n - **Short-lived session key** — minted server-side via\n `POST /v1/session-tokens` from a long-lived key. Safe to hand\n to a native mobile client only — not a browser or other\n JavaScript context, see \"API keys\" in the description above.\n It's scoped to `/v1/query` and `/v1/chat` only, expires in at\n most 60 minutes, and can't mint further session keys.\n `POST /v1/session-tokens` itself only ever accepts the\n long-lived key.\n","format":"bearer","typeLabel":"string\u003cbearer>","deprecated":false},"schemeName":"bearerAuth","name":"Authorization"}],"title":"bearerAuth"}]},"originalFileLocation":"openapi.yaml"},"subdomain":"collective-x","siteName":"Collective X API","title":"Poll an async job's status - Collective X API","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-09-13T05:30:51.799Z","originalFileLocation":"api-reference/async-jobs/poll-an-async-jobs-status.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Read-only poll for one async job\u0026#x27;s current state. Jobs are created by POST /v1/jobs — see the \u0026quot;Async jobs\u0026quot; tag for the lifecycle, retention, and polling guidance. Reading past the deadline. If the job is still queued or running when its expires_at has passed — or it has been running for more than 2 minutes — this read marks it expired before returning it, so you never see a stale in-flight state. A job that had already finished (succeeded / failed / expired) is returned as it finished until expires_at; the first read after that answers 404 not_found, clears the stored request and result, and the row is deleted within ~24 h. Fetch your result before expires_at. Transient errors while polling. A 429, or a 503 that carries Retry-After, says nothing about the job: wait max(Retry-After, 1) seconds and poll again. Server-to-server only, with your long-lived key. A short-lived session token (see \u0026quot;Mobile-direct access\u0026quot; above) cannot call this endpoint: its scope covers only /v1/query and /v1/chat, so it gets 403 forbidden here like on every other endpoint. Do NOT call this endpoint from browser-side code, and never embed your long-lived key in any client. Scoping: a job is visible to every long-lived key of the partner that created it (ownership is per partner, not per key). A job id that doesn\u0026#x27;t exist and a job id that belongs to a different partner both return the identical 404 not_found — this endpoint never confirms another partner\u0026#x27;s job id exists. Rate limits are communicated via X-RateLimit-* headers on every response and are shared with /v1/query and /v1/chat per API key (see \u0026quot;Rate limits\u0026quot; above). When a 429 is returned, honour Retry-After before retrying.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Collective X API\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Poll an async job\u0026#x27;s status - Collective X API\">\u003cmeta data-mint-head property=\"og:description\" content=\"Read-only poll for one async job\u0026#x27;s current state. Jobs are created by POST /v1/jobs — see the \u0026quot;Async jobs\u0026quot; tag for the lifecycle, retention, and polling guidance. Reading past the deadline. If the job is still queued or running when its expires_at has passed — or it has been running for more than 2 minutes — this read marks it expired before returning it, so you never see a stale in-flight state. A job that had already finished (succeeded / failed / expired) is returned as it finished until expires_at; the first read after that answers 404 not_found, clears the stored request and result, and the row is deleted within ~24 h. Fetch your result before expires_at. Transient errors while polling. A 429, or a 503 that carries Retry-After, says nothing about the job: wait max(Retry-After, 1) seconds and poll again. Server-to-server only, with your long-lived key. A short-lived session token (see \u0026quot;Mobile-direct access\u0026quot; above) cannot call this endpoint: its scope covers only /v1/query and /v1/chat, so it gets 403 forbidden here like on every other endpoint. Do NOT call this endpoint from browser-side code, and never embed your long-lived key in any client. Scoping: a job is visible to every long-lived key of the partner that created it (ownership is per partner, not per key). A job id that doesn\u0026#x27;t exist and a job id that belongs to a different partner both return the identical 404 not_found — this endpoint never confirms another partner\u0026#x27;s job id exists. Rate limits are communicated via X-RateLimit-* headers on every response and are shared with /v1/query and /v1/chat per API key (see \u0026quot;Rate limits\u0026quot; above). When a 429 is returned, honour Retry-After before retrying.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Collective X API\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://collective-x.mintlify.app/_mintlify/api/og/v2?division=Async+jobs\u0026amp;title=Poll+an+async+job%27s+status\u0026amp;description=Read-only+poll+for+one+async+job%27s+current+state.+Jobs+are%0Acreated+by+%60POST+%2Fv1%2Fjobs%60+%E2%80%94+see+the+%22Async+jobs%22+tag+for+the%0Alifecycle%2C+retention%2C+and+polling+guida\u0026amp;theme=bb4e877d2136d284fc6ff9a0\u0026amp;signature=4b995d9cbedba122d5c8a36e59f1e7a03009e7bc0c52e7d6fca508b5453ebc99\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Poll an async job\u0026#x27;s status - Collective X API\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Read-only poll for one async job\u0026#x27;s current state. Jobs are created by POST /v1/jobs — see the \u0026quot;Async jobs\u0026quot; tag for the lifecycle, retention, and polling guidance. Reading past the deadline. If the job is still queued or running when its expires_at has passed — or it has been running for more than 2 minutes — this read marks it expired before returning it, so you never see a stale in-flight state. A job that had already finished (succeeded / failed / expired) is returned as it finished until expires_at; the first read after that answers 404 not_found, clears the stored request and result, and the row is deleted within ~24 h. Fetch your result before expires_at. Transient errors while polling. A 429, or a 503 that carries Retry-After, says nothing about the job: wait max(Retry-After, 1) seconds and poll again. Server-to-server only, with your long-lived key. A short-lived session token (see \u0026quot;Mobile-direct access\u0026quot; above) cannot call this endpoint: its scope covers only /v1/query and /v1/chat, so it gets 403 forbidden here like on every other endpoint. Do NOT call this endpoint from browser-side code, and never embed your long-lived key in any client. Scoping: a job is visible to every long-lived key of the partner that created it (ownership is per partner, not per key). A job id that doesn\u0026#x27;t exist and a job id that belongs to a different partner both return the identical 404 not_found — this endpoint never confirms another partner\u0026#x27;s job id exists. Rate limits are communicated via X-RateLimit-* headers on every response and are shared with /v1/query and /v1/chat per API key (see \u0026quot;Rate limits\u0026quot; above). When a 429 is returned, honour Retry-After before retrying.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://collective-x.mintlify.app/_mintlify/api/og/v2?division=Async+jobs\u0026amp;title=Poll+an+async+job%27s+status\u0026amp;description=Read-only+poll+for+one+async+job%27s+current+state.+Jobs+are%0Acreated+by+%60POST+%2Fv1%2Fjobs%60+%E2%80%94+see+the+%22Async+jobs%22+tag+for+the%0Alifecycle%2C+retention%2C+and+polling+guida\u0026amp;theme=bb4e877d2136d284fc6ff9a0\u0026amp;signature=4b995d9cbedba122d5c8a36e59f1e7a03009e7bc0c52e7d6fca508b5453ebc99\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/collective-x/0_4ikjEQ7vib8PG9/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Collective X API\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#1a3f52\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/api-reference/async-jobs/poll-an-async-jobs-status.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.collectivex.health/#organization\",\"name\":\"Collective X API\",\"url\":\"https://docs.collectivex.health\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/collective-x/XmdndrtGclqPo6DR/logo/light.svg?fit=max\u0026auto=format\u0026n=XmdndrtGclqPo6DR\u0026q=85\u0026s=a7e95b40e54fc2a5bb8a468486937833\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.collectivex.health#website\",\"name\":\"Collective X API\",\"url\":\"https://docs.collectivex.health\",\"publisher\":{\"@id\":\"https://docs.collectivex.health/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status#webpage\",\"url\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status\",\"name\":\"Poll an async job's status\",\"description\":\"Read-only poll for one async job's current state. Jobs are created by POST /v1/jobs — see the \\\"Async jobs\\\" tag for the lifecycle, retention, and polling guidance. Reading past the deadline. If the job is still queued or running when its expires_at has passed — or it has been running for more than 2 minutes — this read marks it expired before returning it, so you never see a stale in-flight state. A job that had already finished (succeeded / failed / expired) is returned as it finished until expires_at; the first read after that answers 404 not_found, clears the stored request and result, and the row is deleted within ~24 h. Fetch your result before expires_at. Transient errors while polling. A 429, or a 503 that carries Retry-After, says nothing about the job: wait max(Retry-After, 1) seconds and poll again. Server-to-server only, with your long-lived key. A short-lived session token (see \\\"Mobile-direct access\\\" above) cannot call this endpoint: its scope covers only /v1/query and /v1/chat, so it gets 403 forbidden here like on every other endpoint. Do NOT call this endpoint from browser-side code, and never embed your long-lived key in any client. Scoping: a job is visible to every long-lived key of the partner that created it (ownership is per partner, not per key). A job id that doesn't exist and a job id that belongs to a different partner both return the identical 404 not_found — this endpoint never confirms another partner's job id exists. Rate limits are communicated via X-RateLimit-* headers on every response and are shared with /v1/query and /v1/chat per API key (see \\\"Rate limits\\\" above). When a 429 is returned, honour Retry-After before retrying.\",\"dateModified\":\"2026-09-13T05:30:51.799Z\",\"isPartOf\":{\"@id\":\"https://docs.collectivex.health#website\"},\"breadcrumb\":{\"@id\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Async jobs\",\"item\":\"https://docs.collectivex.health/api-reference/async-jobs/submit-a-question-as-an-async-job-asynchronous-v1query\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Poll an async job's status\",\"item\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status\"}]},{\"@type\":[\"TechArticle\",\"APIReference\"],\"@id\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status#apireference\",\"headline\":\"Poll an async job's status\",\"name\":\"Poll an async job's status\",\"description\":\"Read-only poll for one async job's current state. Jobs are created by POST /v1/jobs — see the \\\"Async jobs\\\" tag for the lifecycle, retention, and polling guidance. Reading past the deadline. If the job is still queued or running when its expires_at has passed — or it has been running for more than 2 minutes — this read marks it expired before returning it, so you never see a stale in-flight state. A job that had already finished (succeeded / failed / expired) is returned as it finished until expires_at; the first read after that answers 404 not_found, clears the stored request and result, and the row is deleted within ~24 h. Fetch your result before expires_at. Transient errors while polling. A 429, or a 503 that carries Retry-After, says nothing about the job: wait max(Retry-After, 1) seconds and poll again. Server-to-server only, with your long-lived key. A short-lived session token (see \\\"Mobile-direct access\\\" above) cannot call this endpoint: its scope covers only /v1/query and /v1/chat, so it gets 403 forbidden here like on every other endpoint. Do NOT call this endpoint from browser-side code, and never embed your long-lived key in any client. Scoping: a job is visible to every long-lived key of the partner that created it (ownership is per partner, not per key). A job id that doesn't exist and a job id that belongs to a different partner both return the identical 404 not_found — this endpoint never confirms another partner's job id exists. Rate limits are communicated via X-RateLimit-* headers on every response and are shared with /v1/query and /v1/chat per API key (see \\\"Rate limits\\\" above). When a 429 is returned, honour Retry-After before retrying.\",\"url\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status\",\"mainEntityOfPage\":{\"@id\":\"https://docs.collectivex.health/api-reference/async-jobs/poll-an-async-jobs-status#webpage\"},\"dateModified\":\"2026-09-13T05:30:51.799Z\",\"publisher\":{\"@id\":\"https://docs.collectivex.health/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.collectivex.health#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Collective X API\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Collective X API","nav":{"global":null,"tabs":[{"tab":"API Reference","groups":[{"group":"Ask a question","pages":[{"title":"Ask a question — one-shot JSON answer","deprecated":null,"version":null,"openapi":"/openapi.yaml post /v1/query","href":"/api-reference/ask-a-question/ask-a-question-—-one-shot-json-answer"}]},{"group":"Have a conversation","pages":[{"title":"Have a conversation — streaming or sync","deprecated":null,"version":null,"openapi":"/openapi.yaml post /v1/chat","href":"/api-reference/have-a-conversation/have-a-conversation-—-streaming-or-sync"}]},{"group":"Async jobs","pages":[{"title":"Submit a question as an async job (asynchronous /v1/query)","description":"Submit exactly the request body you would send to `POST /v1/query`\nand get a job id back immediately, instead of waiting for the\ngrounded answer on an open connection. The answer is produced in\nthe background and read with `GET /v1/jobs/{id}` — see the \"Async\njobs\" tag description for the lifecycle, retention, and a polling\nrecipe.\n\n**Validation is identical to `/v1/query`, and happens now.** The\nbody must contain exactly one of `question` or `messages` (one\nuser message), may set `response_format`, and may not contain\nanything else. Every `400` this endpoint returns — unknown field,\nboth/neither shape, empty or whitespace-only question, a message\nwith a `file` part, a non-`user` role — is one `/v1/query` would\nhave returned for the same body. Nothing is accepted here only to\nfail an hour of polling later. One extra rule applies only here:\nthe body is persisted and echoed back on every poll, so it is\ncapped at 32 KiB of JSON.\n\n**What runs, and when.** On `202` the job is `queued` and starts\nrunning immediately, in the background. It is the same\nretrieval + safety + generation pipeline `/v1/query` uses, with the\nsame published tone and the same deferral/crisis behaviour — so\nthe eventual `result` is exactly the `QueryResponse` you would have\nreceived synchronously. Expect the job to finish in the same\n10–40 s a synchronous call takes; a job is never faster than the\nsync call, it just doesn't make you wait on the socket.\n\n**Metering.** Submitting is one metered request against your\nper-key budget (the same pool as `/v1/query`, `/v1/chat`, and\npolling). The background execution is not metered again.\n\n**Server-to-server only, with your long-lived key.** A session\ntoken gets `403 forbidden` here, exactly as on `GET /v1/jobs/{id}`.\nNever embed your long-lived key in a client.\n","deprecated":null,"version":null,"openapi":"/openapi.yaml post /v1/jobs","href":"/api-reference/async-jobs/submit-a-question-as-an-async-job-asynchronous-v1query"},{"title":"Poll an async job's status","deprecated":null,"version":null,"openapi":"/openapi.yaml get /v1/jobs/{id}","href":"/api-reference/async-jobs/poll-an-async-jobs-status"}]},{"group":"Session tokens","pages":[{"title":"Mint a short-lived, scoped session token for a mobile client","description":"Exchanges your long-lived partner key for a short-lived, scoped\nUnkey key that a mobile client can call `/v1/query` and `/v1/chat`\nwith directly — see \"Mobile-direct access\" in the introduction for\nthe full pattern and a runnable example.\n\n**Requires your long-lived partner key.** A session key can never\ncall this endpoint — it always gets `403 forbidden`, whether that's\na mistaken integration (the mobile client was accidentally handed\nthe mint URL) or an attacker trying to extend a leaked session\nkey's life. There is no session-key chaining.\n\nThe minted key:\n - Can call **only** `/v1/query` and `/v1/chat` — every other\n endpoint, including this one, returns `403 forbidden`.\n - Expires automatically between 60 seconds and 60 minutes after\n minting (`ttl_seconds`, default 900 = 15 minutes). Unkey\n deletes expired keys on its own; there is nothing to clean up.\n An expired key returns `401 unauthorized` with an \"API key\n expired\" message — your mobile client should treat that as\n \"ask my backend for a fresh token\", not as an error to alert\n on.\n - Shares your long-lived key's rate-limit budget and telemetry —\n minting session keys does not create additional rate-limit\n headroom. A budget change to your long-lived key reaches\n outstanding session keys only after they expire (at most 60\n minutes later), since each key's limits are captured at mint\n time, not read through live.\n - Stops working after `max_requests` total requests (default\n 200; session keys are never uncapped), independent of the\n rate-limit window. An exhausted key returns `403 forbidden`\n with a \"mint a new session token\" message — permanently, so\n do not retry; mint a new token.\n - Stops working everywhere, within one deploy, if the\n session-token feature is disabled — the feature flag is a kill\n switch for outstanding keys, not just for new mints.\n\nMinting itself is a normal partner API call: authenticated and\nrate-limited exactly like `/v1/query` and `/v1/chat`, and it\ncharges your long-lived key's own requests-per-minute (and, if\nconfigured, requests-per-day) budget — one mint call is one\nmetered request.\n\nAn empty request body is valid and mints a 15-minute key with the\ndefault 200-request allowance; both fields are optional.\n","deprecated":null,"version":null,"openapi":"/openapi.yaml post /v1/session-tokens","href":"/api-reference/session-tokens/mint-a-short-lived-scoped-session-token-for-a-mobile-client"}]}]},{"tab":"Guides","pages":[{"title":"Guides","href":""}]}]},"title":"Poll an async job's status - Collective X API","buildId":"dpl_2xiSHD9XEvLsPGPcfoMtkLTB3dy9","deploymentId":"dpl_2xiSHD9XEvLsPGPcfoMtkLTB3dy9","dynamics":["c2aa737661","2b790eda92","b0f84f53ff","a9bb46feb3"],"chunks":["OpenApiReferenceProvider-DmVyH30O.js","utils-BHQqGGOd.js","cloneDeep-izs3Hzxb.js","_Uint8Array-BVIqJ90l.js","isObjectLike-C8vmzPEN.js","isArray-Dcu1dQ5t.js","utils-CD-E4BIt.js","toString-BX-iyh8G.js","isSymbol-CSH9U2Gm.js","addUniqueKeysToSchema-Bp5Lq9Ds.js","snippetPresets-C4xAVCFP.js","style-to-object-BY3JEAdk.js","v4-DIgz8zy6.js","ApiExamples-CgXVLxUC.js","isEqual-BfaiBbcl.js","usePlaygroundInputsStore-BF8I5oUf.js","shallow-Byd0lWnC.js","getApiPlaygroundDisplay-BGOZG1OK.js","CodeSnippet-CpvQL0CY.js","CodeSnippets-B8wkBKXa.js","Page-CBCH2P7b.js","getPastedJson-Ry3scI3Y.js","icon-D7a9HcdD.js","MethodPill-CJ8pmHhz.js","DuplicateIcon-rh1HJF0t.js","createIcon-DvGznqYs.js","DynamicMarkdown-Cc6AuSHS.js","SectionHeading-B4A0S-ep.js","axios-vUo_bRtp.js","Markdown-Cc16CX65.js","clsx-Cs6nMGht.js","lib-DuNMoZ05.js","lib-B8HbPgcC.js","html-void-elements-mCSPHezU.js","dist-zbf22OZA.js","katex-YMM-FMeD.js","marked.esm-CZWythHk.js","AssistantPageLink-C_fs-y5n.js"]}