The live chat loop and the workspace it lives in: projects, threads, messages, attachments, runs, artifacts, and the approval gates that pause runs for human review.
Common responses
These responses apply across endpoints in this section unless an endpoint documents an additional response.
Status
Meaning
Body
200
Request succeeded.
Endpoint response object.
201
Resource created.
Created resource object.
400
Invalid request.
Error envelope.
401
Missing or invalid bearer token.
Error envelope.
404
Resource not found or not visible to the current organization.
Error envelope.
Projects
List projects
Return projects the caller can access within their organization — projects they created, plus projects where they're an explicit member. Projects the caller isn't on are omitted entirely, not just hidden behind a 403. Tri-state filter on archived state: omit `isArchived` (or send `isArchived=false`) to get only active projects; send `isArchived=true` to get only archived projects. Each item contains the full project object plus list-view aggregates: `conversation_count` (threads bound to the project), `file_count` (project files with status `ready`), `member_count` (project_members rows, plus the creator if the creator has no explicit member row), `member_preview` — up to 5 `{user_id, display_name}` entries, creator first, then members ordered by when they were added — and `viewer_role` (`admin` or `member`), the calling user's own role on that project: the creator is always `admin` (even with no explicit member row, or a `member` row of their own), otherwise it's their `project_members` row's role. Aggregates are computed with a handful of batched queries over the returned page, not one query per project.
GET/v1/projects
Query parameters
Name
Type
Required
Description
isArchived
boolean
Optional
Filters by status. `true` returns only archived projects; `false` returns only active. Omit for the default (active only). Mirrors the `isDeleted` filter on `/admin/organizations`.
Create a new project within the caller's organization. Project names must be unique among active projects in the same organization — a 409 is returned if an active project with the same name already exists. The full project object is returned on success with HTTP 201.
POST/v1/projects
Request body
Name
Type
Required
Description
name
string
Required
Display name for the project. Trimmed; must be between 1 and 255 characters. Must be unique among active projects in the organization.
description
string
Optional
Optional free-text description of the project. Maximum 5000 characters. Pass null to explicitly clear an existing description.
{
"id": "24d9ed5b-b683-43d5-bea4-8adc9f8dd09b",
"organization_id": "f1ba15e0-ebe8-4187-afe6-03ccb25b8815",
"name": "Smoke test project (batch 5)",
"description": "Created during the docs upgrade smoke test.",
"status": "active",
"created_by": "0ce6409a-3bfa-4e2f-883a-5a696b451d8b",
"created_at": "2026-05-18T08:34:39.659Z",
"updated_at": "2026-05-18T08:34:39.659Z"
}
Get project
Get a single project by ID. The caller must be able to access the project — its creator or an explicit member. Returns 404 if the project does not exist, belongs to a different organization, or the caller isn't on it; all three cases are indistinguishable.
Update a project's name, description, or status. All fields are optional — send only what you want to change. Renaming to a name already in use by another active project in the same organization returns 409. Only a project admin may update a project — the project's creator counts as an implicit admin. A member without the admin role gets 403; a caller who can't access the project at all (or an unknown/cross-org project id) gets 404.
PATCH/v1/projects/:id
Path parameters
Name
Type
Required
Description
id
string
Required
The id value from the endpoint path.
Request body
Name
Type
Required
Description
name
string
Optional
New display name. Trimmed; 1–255 characters. Must be unique among active projects in the organization.
description
string
Optional
Updated description. Pass `null` to clear. Maximum 5000 characters.
Resource not found, or not accessible to the caller at all.
`{ error, code, request_id }`
400
Request body failed validation.
`{ error, code, request_id }`
200 OKjson
{
"id": "9960c593-9a95-42a8-b26b-a50b4bd915e2",
"organization_id": "f1ba15e0-ebe8-4187-afe6-03ccb25b8815",
"name": "Consulting Pipeline Command Center v2",
"description": "Expanded to cover utilization forecasting and staffing capacity planning.",
"status": "active",
"created_by": "39a69cb1-c14c-481d-85fb-fd43c8046440",
"created_at": "2026-04-13T17:17:37.817Z",
"updated_at": "2026-05-21T10:30:00.000Z"
}
Archive project
Soft-delete a project by setting its status to `archived`. The project remains queryable via `GET /projects?isArchived=true`. Same authorization as PATCH /projects/:id: only a project admin may archive (creator counts as an implicit admin). Returns 204 on success, 403 if the caller is a member but not an admin, or 404 if the project doesn't exist or isn't accessible to the caller at all.
Resource not found, or not accessible to the caller at all.
`{ error, code, request_id }`
204 No Contenttext
List project members
List the members of a project — the users who can read and reply in the project's threads. Returns a cursor-paginated array of member records, each with the user id, the member's role (`admin` or `member`), who added them, and when. The caller must be able to access the project (its creator or an explicit member); another organization's project, a nonexistent project, or a project the caller isn't on all return 404 identically.
Add a user to a project. Only a project admin (or the project's creator) may add members. The member is created with the requested role, defaulting to `member`. Returns 201 with the new member record. A 409 is returned if the user is already a member of the project.
POST/v1/projects/:id/members
Path parameters
Name
Type
Required
Description
id
string
Required
The project ID to add the member to.
Request body
Name
Type
Required
Description
userId
string
Required
UUID of the user to add. Must belong to the caller's organization.
role
string
Optional
Member role. One of `admin` or `member`. Defaults to `member`.
Change a member's role within a project. Only a project admin may change roles. Returns the updated member record. A 404 is returned if the user is not a member of the project.
Remove a member from a project. A user may always remove themselves; removing another member requires project admin. Returns 204 on success. A 404 is returned if the user is not a member of the project.
Forbidden — only a project admin can remove other members.
`{ error, code, request_id }`
404
Resource not found in the caller's organization.
`{ error, code, request_id }`
204 No Contenttext
Search
Search
Search visible organization resources — threads (by title), agents, workflows, and projects — across the calling organization. Threads are further scoped to those created by the calling user. Results are grouped by resource type under the `results` key. Each type is limited to `limit` results (default 10, max 25).
GET/v1/search
Query parameters
Name
Type
Required
Description
q
string
Required
Search query. Case-insensitive substring match.
limit
number
Optional
Maximum results per resource type. Default 10, maximum 25.
Initiate a two-step direct-to-storage upload for a project file — a document shared by every thread in the project, as opposed to a per-thread attachment. Step 1 — call this endpoint to create a pending project file record and receive a pre-signed PUT URL. Step 2 — PUT the raw file bytes directly to that URL (no Authorization header). Step 3 — call POST /project-files/:id/commit to mark the file ready, which also enqueues it for ingestion. The pre-signed URL expires in 15 minutes. Only a project member may upload (the project's creator counts as an implicit member); an org member who is not on the project gets 403.
POST/v1/projects/:id/files/upload-url
Path parameters
Name
Type
Required
Description
id
string
Required
The project ID this file will belong to.
Request body
Name
Type
Required
Description
filename
string
Required
Original file name including extension. Maximum 512 characters.
mimeType
string
Required
MIME type of the file, e.g. application/pdf. Maximum 255 characters.
Finalise a pending project file after the client has PUT the file bytes to the pre-signed upload URL. The server transitions the file's status from pending to ready AND, in the same database transaction, enqueues a `project_file.ingest` outbox command (`{ project_file_id }`) that the worker pipeline picks up to extract, chunk, and embed the file's content for project search. Only a project member may commit (creator counts as a member). Files already in a non-pending state return 409.
POST/v1/project-files/:id/commit
Path parameters
Name
Type
Required
Description
id
string
Required
The project file ID returned by POST /projects/:id/files/upload-url.
Example requestbash
curl https://api.cortex.cognit-dx.com/v1/project-files/d7001e74-aee2-4b27-a2e5-7a54d5eec698/commit \
-X POST \
-H "Authorization: Bearer $CORTEX_TOKEN"
Response codes
Status
Meaning
Body
401
Missing or invalid credential.
`{ error, code, request_id }`
403
Forbidden — only a project member can commit this file.
List the files uploaded to a project. The caller must be able to access the project (its creator or an explicit member) — an org member who isn't on the project gets 404, the same as a nonexistent project. Tri-state filter on deleted state: omit `isDeleted` (or send `isDeleted=false`) to get only non-deleted files (`pending`, `ready`, `failed`); send `isDeleted=true` to get only soft-deleted files. Results are ordered newest first.
GET/v1/projects/:id/files
Path parameters
Name
Type
Required
Description
id
string
Required
The project ID whose files to list.
Query parameters
Name
Type
Required
Description
isDeleted
boolean
Optional
`true` returns only soft-deleted files; `false` (or omitted) returns only non-deleted files.
Generate a short-lived presigned GET URL for downloading a project file's underlying blob. The URL expires after 1 hour. The caller must be able to access the file's project (its creator or an explicit member). Returns 404 if the file does not exist, belongs to a different organization, has been soft-deleted, or the caller isn't on the project — all four cases are indistinguishable (deleted files 404 rather than 410 for the same reason).
Soft-delete a project file (sets `status` to `deleted`); the row is retained but drops out of the default file list and download stops working. Only a project member may delete (creator counts as a member). Note: this does not remove the file's `project_file_chunks` rows — those are cleaned up by the worker's reindex path, not by delete itself — but Phase 2 search filters project files on `status = 'ready'`, so a deleted file's stale chunks never surface in search results in the meantime.
Forbidden — only a project member can delete this file.
`{ error, code, request_id }`
404
Resource not found in the caller's organization.
`{ error, code, request_id }`
204 No Contenttext
Threads
List threads
Return the threads visible to the authenticated user within their organization (their own, plus project threads they can see), newest first, one page at a time. Filters are query parameters and are camelCase on the wire. Each thread carries its ID, title, source, project, pin state and timestamps.
GET/v1/threads
Query parameters
Name
Type
Required
Description
search
string
Optional
Case-insensitive match on the title.
createdFrom
string
Optional
ISO datetime; only threads created at or after it.
createdTo
string
Optional
ISO datetime; only threads created at or before it.
projectId
string
Optional
Only threads in this project.
createdByType
string
Optional
"user", "agent" or "workflow".
createdById
string
Optional
UUID of the user, agent or workflow that started the thread.
isPinned
boolean
Optional
`true` for pinned threads only, `false` for unpinned only. The member app fetches pinned threads with this so they survive falling off the first page of recent chats.
limit
integer
Optional
Page size, 1–100. Default 20.
cursor
string
Optional
Opaque cursor from the previous page's `next_cursor`.
Create a new thread owned by the authenticated user. Call this endpoint before posting messages or starting a run — every message and run must belong to a thread. Optionally bind the thread to a specific agent by providing an agentId; when bound, each user message is automatically routed through the agent orchestrator instead of spawning a plain task. To create the thread inside a project, the caller must be able to access that project (its creator or an explicit project member) — otherwise the request returns 404, the same response as an unknown project id.
POST/v1/threads
Request body
Name
Type
Required
Description
title
string
Optional
Display title for the thread. If omitted the title is null until auto-generated by the generate-thread-title endpoint. Maximum 255 characters.
projectId
string
Optional
Optional project UUID. When set, project members can access the thread.
createdByType
string
Optional
Who started the thread: "user" (default), "agent", or "workflow". For `user`, the server always uses the authenticated user as `createdById`. For `agent` or `workflow`, `createdById` must reference an active resource in the organization.
createdById
string
Optional
UUID of the user, agent, or workflow that started the thread. Defaults to the authenticated user when `createdByType` is omitted or `user`.
Fetch a single accessible thread by ID. The response includes `active_run_id`, which is the thread's current `running` or `awaiting` run, or null. Clients can use it to resume an in-flight response after a reload, in another tab, or when browser-local state was lost. Accessible means the authenticated user created the thread, has triggered a run on it, or is a member (or the creator) of the project it belongs to — anyone else gets 404, indistinguishable from a nonexistent thread.
Update an existing thread. Only `title`, `projectId` and `isPinned` are mutable — creator fields and audit timestamps cannot be patched after creation. Pass `title: null` to clear the title. Pinning is bookkeeping, not use: changing `isPinned` leaves `updated_at` alone, so a pinned or unpinned thread keeps its place in recency-ordered lists. Renaming/reassigning a thread is a closed set: the thread's user-type creator, or — for a project-scoped thread — a project admin (the project's creator counts as an implicit admin). A caller with no visibility into the thread at all gets 404; a caller who can see the thread (e.g. a plain project member) but isn't in that set gets 403.
PATCH/v1/threads/:id
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID to update.
Request body
Name
Type
Required
Description
title
string
Optional
New display title. Max 255 characters. Pass null to clear. Any other field is silently ignored.
projectId
string
Optional
Move the thread into a project the caller can access, or pass null to take it out of its project.
isPinned
boolean
Optional
Pin (`true`) or unpin (`false`) the thread. Pinned threads are shown in their own section above recent chats in the member app. `is_pinned` on the wire.
Permanently delete a thread. All messages, attachments, artifacts, and run records belonging to the thread are cascaded by foreign-key constraints. Thread deletion is irreversible. Deletion is a closed set: the thread's user-type creator, or — for a project-scoped thread — a project admin (the project's creator counts as an implicit admin). A caller with no visibility into the thread gets 404; a caller who can see it (e.g. a plain project member) but isn't in that set gets 403. Returns 204 No Content on success.
The thread is visible to the caller but they aren't the creator or a project admin.
`{ error, code, request_id }`
404
Resource not found, or not visible to the caller at all.
`{ error, code, request_id }`
204 No Contenttext
Generate thread title
Generate a short AI-written title (3–6 words) from a single message and save it on the thread. Calls Claude Haiku directly (not the worker pipeline) for fast, cheap inference. Idempotent in two cases: if the thread already has a non-empty title, the request is a no-op and the existing thread is returned unchanged; if the server is missing an Anthropic API key, the request is also a no-op (silent fallback so a missing key never blocks the chat UI). Wrapping quotes and trailing punctuation are stripped from the model output. Same authorization as PATCH /threads/:id: the thread's creator or (for a project-scoped thread) a project admin; a visible-but-unauthorized caller gets 403, an invisible thread gets 404.
POST/v1/threads/:id/generate-title
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID to title.
Request body
Name
Type
Required
Description
message
string
Required
Source text used to generate the title — typically the user's first message in the thread. 1–4000 characters.
Example requestbash
curl https://api.cortex.cognit-dx.com/v1/threads/9f7c9d3a-1234-5678-9abc-def012345678/generate-title \
-X POST \
-H "Authorization: Bearer $CORTEX_TOKEN" \
-H "Content-Type: application/json" \
--data '{"message":"Summarize the Q1 pipeline."}'
Response codes
Status
Meaning
Body
401
Missing or invalid credential.
`{ error, code, request_id }`
403
The thread is visible to the caller but they aren't the creator or a project admin.
`{ error, code, request_id }`
404
Resource not found, or not visible to the caller at all.
Return messages in a thread ordered by sequence number ascending. The response is cursor-paginated; follow `next_cursor` while `has_more` is true to read the complete transcript. Run output messages include `run_id`. Inaccessible threads return 404.
GET/v1/threads/:id/messages
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID whose messages to list.
Query parameters
Name
Type
Required
Description
limit
number
Optional
Page size from 1 to 100. Defaults to 20.
cursor
string
Optional
Opaque cursor returned as `next_cursor` by the previous page.
Post a message to a thread. Accepts `role: "user"` or `role: "system"`.
**User role (auto-trigger):** When `role: "user"` is sent, the server inserts the message and immediately creates a `conversation_run` (status: `queued`) and emits a worker event to start processing. The response is `{ message, run: { id, status } }`. This is the primary way to send a chat message and trigger execution.
**Resuming an awaiting run:** if the thread's active run is paused (`awaiting`) at a clarification interrupt (an act called `ask_user`) and the author is the run's originator, this message is treated as the answer: it resumes that run (response `{ message, run: { id, status: "running" }, queued: false }`) instead of starting a new one. The question itself is durable: when the run paused, the worker persisted it as an `assistant` message whose `content_blocks` carry an `act` block (`kind: "question"`, with `body.text`, `body.options`, `body.multi_select`); on resume the answer is stamped onto that block as `body.answer`, so transcripts always show what was asked and what was chosen. Messages from other users, or additional messages while a run is active, are stored `pending` and queued (`{ message, run: null, queued: true }`). If the organization has data-leakage-protection rules, the message content is evaluated BEFORE it is stored or dispatched: a blocked message returns 422 with code `dlp_blocked` and is never persisted or sent to a model.
**System role (no trigger):** When `role: "system"` is sent, the message is written to the thread's transcript as context for the next run but no run is created. The response is `{ message }`. Use this to inject runtime hints, caller identity, or feature flags before explicitly starting a run.
`assistant` and `tool` messages are written only by the worker — clients cannot post them.
Returns 201 with the message (and run, when role is user).
POST/v1/threads/:id/messages
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID.
Request body
Name
Type
Required
Description
role
string
Required
`"user"` to post a user message and auto-trigger a run; `"system"` to inject context without triggering. Any other value is rejected with 400.
content
string
Required
Plain-text message content. Min 1 character.
contentBlocks
array
Optional
Structured content blocks for multimodal messages. Optional — when omitted the server derives a single text block from `content`.
attachmentIds
array
Optional
UUIDs of thread attachments to include with this message. Only applies when `role: "user"`. The legacy alias `documentIds` is still accepted.
agentId
string
Optional
UUID of an agent to route this run to. Only applies when `role: "user"`.
workflowId
string
Optional
UUID of a workflow to execute for this run. Only applies when `role: "user"`. Mutually exclusive with `agentId`.
Edit the plain-text content of a **system** message (the ones you injected via `POST /threads/:id/messages`). Use this when you need to amend an injected note before the next run. Editing the content of a user/assistant message returns 403 `non_system_content_immutable` — those messages are the durable model/user transcript and must not be rewritten by the API.
The owning thread must belong to the authenticated user; another user's message returns 404.
PATCH/v1/messages/:id
Path parameters
Name
Type
Required
Description
id
string
Required
The message ID to update.
Request body
Name
Type
Required
Description
content
string
Optional
Replacement plain-text content. System messages only — patching `content` on a user/assistant message returns 403 `non_system_content_immutable`. Min 1 character.
Permanently remove a single message from the transcript. Messages referenced as a run's durable output cannot be deleted; those return 409 `run_output_immutable` so a terminal run never loses its reply. The owning thread must be accessible to the authenticated user; inaccessible messages return 404. Returns 204 No Content on success; a repeat delete returns 404.
Initiate a two-step direct-to-storage upload for a user attachment. Step 1 — call this endpoint to create a pending attachment record and receive a pre-signed PUT URL. Step 2 — PUT the raw file bytes directly to that URL (no Authorization header; the signature is embedded in the URL). Step 3 — call POST /attachments/:id/commit to mark the attachment ready. The pre-signed URL expires in 15 minutes; if the PUT is not completed within that window, discard the attachment ID and start over.
POST/v1/attachments/upload-url
Request body
Name
Type
Required
Description
threadId
string
Required
UUID of the thread this attachment will belong to.
filename
string
Required
Original file name including extension. Maximum 255 characters.
mimeType
string
Required
MIME type of the file, e.g. application/pdf or image/png. Maximum 100 characters.
Finalise a pending attachment after the client has successfully PUT the file bytes to the pre-signed upload URL. The server transitions the attachment's status from pending to ready, making it available for context assembly, search, and download. Calling commit before the blob has been uploaded is valid (the storage backend does not gate the status transition), but the attachment will be unusable until the bytes are present. Attachments already in a non-pending state return 409.
POST/v1/attachments/:id/commit
Path parameters
Name
Type
Required
Description
id
string
Required
The attachment ID returned by POST /attachments/upload-url.
Example requestbash
curl https://api.cortex.cognit-dx.com/v1/attachments/9f7c9d3a-1234-5678-9abc-def012345678/commit \
-X POST \
-H "Authorization: Bearer $CORTEX_TOKEN"
Generate a short-lived presigned GET URL for downloading an attachment's underlying file. The URL expires after 1 hour. Returns 404 if the attachment does not exist or belongs to a different organization, and 410 if the attachment has been soft-deleted.
List non-deleted user uploads for a thread. By default returns root-level attachments (`parent_id` is null). Pass `parent_id` to list children of a zip or archive bundle.
Soft-delete an attachment by setting its status to `deleted`. The underlying blob is not immediately purged. Returns 204 on success. Returns 404 if the attachment does not exist or belongs to a different organization.
Explicit run creation. Atomically inserts a user message (using `input.content`) and creates a `conversation_run`. Use this endpoint when you want finer-grained control than the auto-trigger on `POST /threads/:id/messages { role: "user" }` — for example, when you need to specify `agentId` or `workflowId` without coupling that choice to the message post. Returns 201 with `{ run, message }`.
POST/v1/threads/:id/runs
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID to create the run in.
Request body
Name
Type
Required
Description
input
object
Optional
Input for the run. Shape: `{ content: string, contentBlocks?: array }`. Creates a user message from `content` and queues a run.
agentId
string
Optional
UUID of an agent to route the run to. When omitted, the run uses chat mode unless `workflowId` is set.
workflowId
string
Optional
UUID of a workflow to execute. Mutually exclusive with `agentId`. When provided, the classifier is skipped. `model`, `tool`, prompt-style `skill`, `agent`, and nested `workflow` steps run through the worker graph.
Return the durable catch-up snapshot for a run: the run record, complete act DAG, current partial text for running acts, artifacts, and the final output message when present. A terminal run is delivered only when both `output_message_id` and `output_message` are present. This is the authoritative state clients should check after any terminal SSE hint.
Open the run's snapshot-plus-live Server-Sent Events stream. The API subscribes to Redis Pub/Sub first, sends one `snapshot` frame containing the durable PostgreSQL state, then forwards ephemeral token, activity, act, artifact, interrupt, error, and lifecycle events. There is no replay cursor or `Last-Event-ID` support. Reconnect to the same URL and rebuild from the next snapshot. After a terminal lifecycle hint, confirm delivery with `GET /threads/:id/runs/:runId` and require terminal status plus a readable output message.
Cancel an in-flight or awaiting run. Takes no body. In one transaction the API persists an assistant cancellation reply, links it through `output_message_id`, marks the run cancelled, and enqueues worker cleanup. The worker then cancels active acts and drains the next queued turn. Returns 202 with the already-cancelled run; 409 if the run was already terminal.
POST/v1/threads/:id/runs/:runId/cancel
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID that owns the run.
runId
string
Required
The run ID to cancel.
Example requestbash
curl https://api.cortex.cognit-dx.com/v1/threads/9f7c9d3a-1234-5678-9abc-def012345678/runs/43f6a165-46a6-4e68-9de8-68201c7f4421/cancel \
-X POST \
-H "Authorization: Bearer $CORTEX_TOKEN"
Approve a run that is paused (`awaiting`) at an approval interrupt — i.e. an act that called `request_approval`. The API queues a resume command and the worker continues the act from where it paused, informing the model the action was approved. Pass `interruptId` (the awaiting act's ID) to target a specific gate; if omitted, the run's newest awaiting act is used. Returns 202 `{ run: { id, status: "running" } }`; 409 if the run is not awaiting.
POST/v1/threads/:id/runs/:runId/approve
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID that owns the run.
runId
string
Required
The run ID to approve.
Request body
Name
Type
Required
Description
interruptId
string
Optional
Optional awaiting act ID to approve. If omitted, the run's newest awaiting act is used.
Reject a run that is paused (`awaiting`) at an approval interrupt. The API queues a resume command; the worker resumes the act and informs the model the action was declined, so it completes gracefully without performing the action (a soft decline — the act is not failed). Pass `interruptId` (the awaiting act's ID) to target a specific gate, and an optional `reason`. Returns 202 `{ run: { id, status: "running" } }`; 409 if the run is not awaiting.
POST/v1/threads/:id/runs/:runId/reject
Path parameters
Name
Type
Required
Description
id
string
Required
The thread ID that owns the run.
runId
string
Required
The run ID to reject.
Request body
Name
Type
Required
Description
interruptId
string
Optional
Optional awaiting act ID to reject. If omitted, the run's newest awaiting act is used.
List run-generated artifacts for a thread. Artifacts are durable files produced by tools during a run. Each item includes a typed `type` field (`dashboard`, `image`, `presentation`, `spreadsheet`, `document`, `file`) inferred from MIME type and extension, plus optional source fields (`code`, `content`, `generator`, `generator_version`, `prompt`, `model`).
Get a single artifact by ID. Returns the same shape as the list endpoint. Returns 404 if the artifact does not exist or belongs to a different organization.
Generate a short-lived presigned GET URL for downloading an artifact's underlying file. The URL expires after 1 hour. Returns 404 if the artifact does not exist or belongs to a different organization.
Soft-delete an artifact by setting its status to `deleted`. The underlying blob is not immediately purged. Returns 204 on success. Returns 404 if the artifact does not exist or belongs to a different organization.