Skip to main content

Artifacts

An artifact is the output of a generation job — the rendered file plus its metadata. Rendered bytes live in Cloudflare R2; the API stores only the object key and signs a URL for you on read.


GET /api/notebooks/{notebook_id}/artifacts/

List a notebook's artifacts, newest first.

  • Auth required: JWT. Notebook viewer role is enough.
Keep the trailing slash

The route is registered as /artifacts/. Requesting /artifacts without it returns a 307 redirect. Most HTTP clients follow it transparently, so it works — at the cost of an extra round trip on every call, which adds up when you are polling every 3 seconds.

curl https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/artifacts/ \
-H "Authorization: Bearer $TOKEN"

Example response

[
{
"job_id": "6d2f8a91-3c5e-4b7d-9a10-8f4e2c6b5d33",
"template_id": "research_summary",
"topic": "Key findings across my sources",
"status": "done",
"download_url": "https://8c424e771e04328e2ef63fcc61f6cf8e.r2.cloudflarestorage.com/onfire1/document-ai/generated/4655aa7e…/6d2f8a91….docx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&X-Amz-Signature=…",
"created_at": "2026-07-30T13:24:27.486667+00:00",
"updated_at": "2026-07-30T13:26:02.113940+00:00",
"is_composite": false,
"children": [],
"bundle_url": null
},
{
"job_id": "1a4b7c90-2e5f-4a8b-b3d6-9c0e1f2a3b44",
"template_id": "podcast",
"topic": "How our Q2 results compare to the market",
"status": "running",
"download_url": null,
"created_at": "2026-07-30T13:20:11.002331+00:00",
"updated_at": "2026-07-30T13:20:44.887012+00:00",
"is_composite": false,
"children": [],
"bundle_url": null
}
]

Response fields

FieldTypeNotes
job_iduuidThe generation job that produced this
template_idstringSystem id or usr:<uuid>
topicstringThe topic the job was submitted with
statusstringqueued, running, done, or error
download_urlstring | nullSigned R2 URL. Present only when status is done
created_attimestampJob submission time
updated_attimestampLast status transition
is_compositebooltrue for bundle jobs
childrenarrayPer-child results; empty unless composite
bundle_urlstring | nullSigned URL for the zip; composite jobs only
Two limits to design around

The list is capped at 50 rows. There is no pagination parameter — older artifacts simply fall off the end. If you need history beyond that, record job_ids as you create them and fetch individually.

The list is notebook-scoped, not user-scoped. Everyone with access to a shared notebook sees everyone else's artifacts in it, not just their own.

The response carries no output_type

ArtifactOut tells you the template_id but not what kind of file you are getting. You cannot reliably derive it: usr:<uuid> ids reveal nothing, and matching against known system ids breaks for user templates.

Build a template_id → output_type map from GET /api/templates/all and join on it client-side. Be aware that this misses artifacts whose template has since been deleted — fall back to the file extension in the signed URL for those.


GET /api/notebooks/{notebook_id}/artifacts/{job_id}

Fetch one artifact, including composite children.

  • Auth required: JWT. Viewer role is enough.
curl https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/artifacts/$JOB_ID \
-H "Authorization: Bearer $TOKEN"

Example response — a composite bundle with a partial failure

{
"job_id": "8c3e5f21-9b0d-4a76-8e12-4f7a6c9d2b18",
"template_id": "executive_briefing",
"topic": "Q2 board briefing",
"status": "done",
"download_url": null,
"created_at": "2026-07-30T11:02:19.441280+00:00",
"updated_at": "2026-07-30T11:08:53.907714+00:00",
"is_composite": true,
"bundle_url": "https://8c424e771e04328e2ef63fcc61f6cf8e.r2.cloudflarestorage.com/onfire1/document-ai/generated/4655aa7e…/8c3e5f21….zip?X-Amz-Expires=86400&X-Amz-Signature=…",
"children": [
{
"child_index": 0,
"child_template_id": "research_summary",
"output_type": "docx",
"title": "Research Summary",
"status": "done",
"download_url": "https://8c424e771e04328e2ef63fcc61f6cf8e.r2.cloudflarestorage.com/onfire1/document-ai/generated/4655aa7e…/child-docx.docx?X-Amz-Expires=86400&X-Amz-Signature=…",
"error": null
},
{
"child_index": 1,
"child_template_id": "datatable",
"output_type": "datatable",
"title": "Key Metrics",
"status": "done",
"download_url": "https://8c424e771e04328e2ef63fcc61f6cf8e.r2.cloudflarestorage.com/onfire1/document-ai/generated/4655aa7e…/child-xlsx.xlsx?X-Amz-Expires=86400&X-Amz-Signature=…",
"error": null
},
{
"child_index": 2,
"child_template_id": "infographic",
"output_type": "infographic",
"title": "At a Glance",
"status": "done",
"download_url": "https://8c424e771e04328e2ef63fcc61f6cf8e.r2.cloudflarestorage.com/onfire1/document-ai/generated/4655aa7e…/child-html.html?X-Amz-Expires=86400&X-Amz-Signature=…",
"error": null
},
{
"child_index": 3,
"child_template_id": "audio_overview",
"output_type": "audio_overview",
"title": "Audio Overview",
"status": "error",
"download_url": null,
"error": "Text-to-speech provider returned 429"
}
]
}

Child fields

FieldTypeNotes
child_indexintPosition in the composite, zero-based
child_template_idstring | nullThe template that produced this child
output_typestringPresent here, unlike on the parent
titlestring | nullDisplay name
statusstringdone or error per child
download_urlstring | nullSigned URL for this child alone
errorstring | nullReason, when the child failed

Note that the parent reports status: "done" even though one child failed. A composite job only fails outright if every child fails — partial results are still delivered. Always inspect children before telling a user their bundle is complete.


Downloading

download_url and bundle_url are pre-signed R2 URLs. They carry their own authorization in the query string, so fetch them directly — do not attach your bearer token or API key.

DOWNLOAD_URL=$(curl -s "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/artifacts/$JOB_ID" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['download_url'])")

curl -L -o report.docx "$DOWNLOAD_URL"

The URL points at the R2 S3 endpoint directly (…r2.cloudflarestorage.com/onfire1/document-ai/generated/<user_id>/<job_id>.<ext>), not at the storage1.onfire.so CDN.

Signed URLs expire after 24 hours

The signature carries X-Amz-Expires=86400. Persisting a URL in your own database or a client-side store produces links that break the next day. Store the job_id instead and re-request the artifact when you need the file again — a fresh signature is minted on every read.


Notifications

GET /api/notifications/

Read notification rows, including the generate_done and generate_error events written when a job finishes.

  • Auth required: JWT only — this endpoint does not accept an API key
curl https://research.onfire.so/api/notifications/ \
-H "Authorization: Bearer $TOKEN"
[
{
"id": "f2a1c8d3-5b7e-4901-a2c6-3d8e9f0a1b22",
"user_id": "4655aa7e-98e2-42d5-a1c3-9cc3c589a510",
"notebook_id": "86091cd8-6235-40be-8203-984af0798e02",
"type": "generate_done",
"message": "Your Research Summary is ready",
"read": false,
"created_at": "2026-07-30T13:26:02.113940+00:00"
}
]

These rows are written best-effort, inside a try/except around the completion handler. A missing notification does not mean the job failed. Use them to drive a bell icon or a toast after the user has navigated away — never as the authoritative completion signal. Poll the job or the artifacts list for that.

POST /api/notifications/read-all

Mark every one of your notifications as read.

  • Auth required: JWT only
  • Status: 204 No Content, with an empty body
curl -X POST https://research.onfire.so/api/notifications/read-all \
-H "Authorization: Bearer $TOKEN"

There is no endpoint to mark a single notification read — it is all or nothing.