Skip to main content
When an HQ agent produces a deliverable — a chart, a PDF, a report — it publishes an artifact: the bytes live in object storage and a row records the metadata. Each artifact has a stable id and a download URL of the shape /v1/api/artifacts/{id}/download. This guide covers listing artifacts (workspace-wide and per-conversation), downloading an artifact’s raw bytes, downloading a whole conversation as a ZIP archive, and deleting an artifact. All requests go to https://api.hq.zone and authenticate with a personal access token:
export HQ_TOKEN="hq_pat_..."
The endpoints in this guide carry these scopes: listing and downloading require conversations:read; deleting requires conversations:write. The single-artifact byte download (/v1/api/artifacts/{id}/download) requires no specific scope — any authenticated caller in the artifact’s workspace may use it.

List artifacts

There are two ways to list, depending on whether you want everything in the workspace or just one conversation’s files.
1

List the whole workspace

List artifacts returns the delivered artifacts visible to you across the workspace, newest first and capped at 500.
curl https://api.hq.zone/v1/api/artifacts \
  -H "Authorization: Bearer $HQ_TOKEN"
Each entry carries id, name, content_type, size_bytes, caption (nullable), sensitive, created_at, conversation_id (null once detached), conversation_title (null once detached), and download_url:
// → 200
// {
//   "artifacts": [
//     {
//       "id": "<ARTIFACT_ID>",
//       "name": "q3-revenue.pdf",
//       "content_type": "application/pdf",
//       "size_bytes": 81234,
//       "caption": null,
//       "sensitive": false,
//       "created_at": "2026-06-10T09:14:00Z",
//       "conversation_id": "<CONVERSATION_ID>",
//       "conversation_title": "Q3 numbers",
//       "download_url": "/v1/api/artifacts/<ARTIFACT_ID>/download"
//     }
//   ]
// }
2

List one conversation's artifacts

List conversation artifacts returns just the files for a single conversation, in chronological order (oldest first).
curl https://api.hq.zone/v1/api/conversations/<CONVERSATION_ID>/artifacts \
  -H "Authorization: Bearer $HQ_TOKEN"
Each entry is a trimmer shape — id, name, content_type, size_bytes, sensitive, created_at, and download_url:
// → 200
// {
//   "artifacts": [
//     {
//       "id": "<ARTIFACT_ID>",
//       "name": "chart.png",
//       "content_type": "image/png",
//       "size_bytes": 20481,
//       "sensitive": false,
//       "created_at": "2026-06-10T09:12:00Z",
//       "download_url": "/v1/api/artifacts/<ARTIFACT_ID>/download"
//     }
//   ]
// }
If you are a workspace admin inspecting another member’s conversation, add ?view=admin to the per-conversation listing to read their artifacts. This admin read is audited.

Download an artifact’s bytes

Both list responses give you a download_url. Resolve it against the base URL and call Download an artifact. The response is raw binary — the artifact bytes streamed back with the original Content-Type and a Content-Disposition: attachment header carrying the filename — not JSON. Save it to a file rather than printing it.
1

Save the bytes with curl

Use -o to write the response body straight to a file:
curl https://api.hq.zone/v1/api/artifacts/<ARTIFACT_ID>/download \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -o q3-revenue.pdf
A 200 streams the bytes; a 404 means there is no such artifact in your workspace.
2

Save the bytes from JavaScript

Read the response as an ArrayBuffer (the body is binary, not JSON) and write it to disk:
import { writeFile } from "node:fs/promises";

const base = "https://api.hq.zone";
const headers = { Authorization: `Bearer ${process.env.HQ_TOKEN}` };
const artifactId = "<ARTIFACT_ID>";

const res = await fetch(`${base}/v1/api/artifacts/${artifactId}/download`, { headers });
if (!res.ok) throw new Error(`download failed: ${res.status}`);

const bytes = Buffer.from(await res.arrayBuffer());
await writeFile("q3-revenue.pdf", bytes);
The download is streamed, so large artifacts don’t have to fit in memory on the server. The Content-Type you receive is whatever the artifact was published as (e.g. application/pdf, image/png).

Download a conversation as a ZIP archive

To grab every artifact in a conversation at once, call Download conversation artifacts. The response is a raw application/zip stream, not JSON — save it to a file. Filenames that would collide inside the archive are de-duplicated automatically.
curl https://api.hq.zone/v1/api/conversations/<CONVERSATION_ID>/artifacts/archive \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -o conversation-artifacts.zip
A 200 returns the ZIP; a 404 means the conversation has no artifacts to download.
import { writeFile } from "node:fs/promises";

const base = "https://api.hq.zone";
const headers = { Authorization: `Bearer ${process.env.HQ_TOKEN}` };
const convId = "<CONVERSATION_ID>";

const res = await fetch(`${base}/v1/api/conversations/${convId}/artifacts/archive`, { headers });
if (res.status === 404) throw new Error("no artifacts to download");
if (!res.ok) throw new Error(`archive failed: ${res.status}`);

await writeFile("conversation-artifacts.zip", Buffer.from(await res.arrayBuffer()));
The archive is a pure storage read — it works even when the conversation’s workspace is asleep, since no agent VM has to be running.

Delete an artifact

Delete an artifact permanently removes an artifact’s row and its stored bytes.
curl -X DELETE https://api.hq.zone/v1/api/artifacts/<ARTIFACT_ID> \
  -H "Authorization: Bearer $HQ_TOKEN"
Note the path: deletion targets /v1/api/artifacts/{id} (DELETE), while the byte download targets /v1/api/artifacts/{id}/download (GET). The delete response is JSON with the artifact id and a deleted flag:
// → 200
// { "id": "<ARTIFACT_ID>", "deleted": true }
You may delete an artifact if you participate in its backing conversation, or — once it has been detached from a deleted conversation — if you were its original producer. Otherwise the call returns 403. A 404 means the artifact was not found in your workspace.
Deleting an artifact that is still attached to a conversation also removes it from that conversation’s transcript. The bytes are gone and the existing download link will then 404.