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.
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
| Field | Type | Notes |
|---|---|---|
job_id | uuid | The generation job that produced this |
template_id | string | System id or usr:<uuid> |
topic | string | The topic the job was submitted with |
status | string | queued, running, done, or error |
download_url | string | null | Signed R2 URL. Present only when status is done |
created_at | timestamp | Job submission time |
updated_at | timestamp | Last status transition |
is_composite | bool | true for bundle jobs |
children | array | Per-child results; empty unless composite |
bundle_url | string | null | Signed URL for the zip; composite jobs only |
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.
output_typeArtifactOut 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
| Field | Type | Notes |
|---|---|---|
child_index | int | Position in the composite, zero-based |
child_template_id | string | null | The template that produced this child |
output_type | string | Present here, unlike on the parent |
title | string | null | Display name |
status | string | done or error per child |
download_url | string | null | Signed URL for this child alone |
error | string | null | Reason, 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.
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.