Generation
Generation turns the sources in a notebook into a finished artifact — a Word document, a deck, an audio overview, a spreadsheet, an interactive HTML page. Every kind of output goes through one endpoint; only the template_id changes.
Generation is asynchronous. You submit a job, get a job_id back immediately, then poll until it reaches done.
Templates
A template is a prompt pair plus an output type. There are two sources:
- System templates — shipped with the platform, listed by short string ids like
research_summary. They are enumerated from YAML files at request time, so new ones can appear without a client release. - User templates — authored by you or shared into your workspace. Their ids are always prefixed
usr:followed by a UUID.
GET /api/notebooks/{notebook_id}/templates
List system templates available for a notebook.
- Auth required: JWT. Notebook viewer role is enough.
curl https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/templates \
-H "Authorization: Bearer $TOKEN"
[
{
"id": "research_summary",
"name": "Research Summary",
"output_type": "docx",
"description": "Structured summary of the key findings across your sources"
},
{
"id": "podcast",
"name": "Podcast",
"output_type": "podcast",
"description": "Two-speaker conversational podcast"
}
]
GET /api/templates/all
List system and user templates together, with usr: prefixes already applied. This is the endpoint to build a template picker against — it is one call instead of two, and it tells you which templates the caller owns.
- Auth required: JWT
- Workspace-aware: honours
X-Workspace-Id
curl https://research.onfire.so/api/templates/all \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"
[
{
"id": "research_summary",
"source": "system",
"name": "Research Summary",
"output_type": "docx",
"description": "Structured summary of the key findings across your sources",
"owned": false,
"visibility": ""
},
{
"id": "usr:f8ade5dd-90af-4bf0-8e35-6593377fa4b6",
"source": "user",
"name": "Source of Funds Declaration",
"output_type": "docx",
"description": "A formal declaration document covering identification details and source of funds",
"owned": true,
"visibility": "private"
}
]
source distinguishes system from user. owned tells you whether the caller may edit it. visibility is private or workspace for user templates — use it to group "My templates" separately from "Shared with workspace".
visibilitySystem entries return visibility: "", not "system". Branch on source to tell the two kinds apart; treat visibility as meaningful only when source is user.
You see your own templates (private and workspace) plus any workspace-visibility template shared into a workspace you belong to.
GET /api/templates/
List only your user templates, including the full system_prompt and user_prompt bodies. Use this when editing a template, not when listing choices.
- Auth required: JWT
Unlike /templates/all, this endpoint returns bare UUIDs with no usr: prefix. You must prepend usr: yourself before passing one to the generate endpoint, or it will be treated as an unknown system template id and 404.
[
{
"id": "df060807-df67-456f-b166-9e901681f53e",
"name": "TVN Benchmarking Report Generator",
"output_type": "docx",
"description": "Biannual competitive benchmarking report",
"system_prompt": "You are a competitive analyst…",
"user_prompt": "Produce a benchmarking report on {{user_topic}}…",
"created_at": "2026-06-14T09:12:44.001Z",
"blocks": null,
"composite_mode": null,
"visibility": "private",
"workspace_id": null
}
]
Output types
Fifteen output types exist. A template declares exactly one, and it determines the file you get back.
output_type | File | Notes |
|---|---|---|
docx | .docx | Word document — the most common |
pptx | .pptx | PowerPoint deck |
podcast | .mp3 | Two-speaker conversation, uses both voices |
audio_overview | .mp3 | Single narrator, uses voice1_id only |
video | .html | A self-contained Reveal.js slideshow — not an mp4 |
infographic | .html | Interactive HTML infographic |
mindmap | .html | Interactive mind map |
quiz | .html | Interactive quiz |
flashcard | .html | Interactive flashcards |
datatable | .xlsx | Excel spreadsheet |
html | .html | Standalone styled HTML document |
image | .png | HTML rendered and screenshotted |
infographic_image | .png | Rasterized infographic |
pdf | .pdf | Rendered and printed to PDF |
composite | .zip | A bundle of several children — see Composites |
video does not produce a videooutput_type: video renders an HTML slideshow. There is no encoder in the pipeline and no mp4 is produced. Label it accordingly in your UI — "Animated Slides (HTML)" is what the platform calls it.
Do not infer the output type from the template id. Ids like usr:<uuid> tell you nothing, and string-matching system ids breaks the moment a user template targets the same output type. Read output_type from the template listing.
Creating a generation job
POST /api/notebooks/{notebook_id}/generate
- Auth required: JWT. Notebook member role or above — a viewer gets 403.
- Status:
202 Accepted
Request fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
template_id | string | yes | — | System id, or usr:<uuid> for a user template |
topic | string | yes | — | See below — this does double duty |
extra_context | string | no | "" | Injected into the prompt at {{extra_context}} |
model | string | null | no | null → registry default | A key from the models endpoint. Affects credit cost |
voice1_id | string | no | 21m00Tcm4TlvDq8ikWAM | Host voice. Used by audio_overview and podcast |
voice2_id | string | no | pNInz6obpgDQGcFmaJgB | Guest voice. podcast only |
podcast_style | string | no | interview | interview, debate, educational, deep_dive |
podcast_length | string | no | medium | short, medium, long |
source_file_ids | string[] | null | no | null | Records which sources this artifact was built from |
enrich | object | null | no | null | Pull fresh web content before generating — see Enrichment |
topic is both the prompt and the search querytopic fills {{user_topic}} in the prompt and is the retrieval query used to pull the top 20 relevant chunks from your sources. An empty or vague topic retrieves poorly and produces a generic document, so it is required — omitting it returns 422.
For a one-click button in a UI, supply a sensible canned topic like "Key findings across my sources" rather than an empty string.
Minimal request
curl -X POST https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/generate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"template_id": "research_summary",
"topic": "Key findings across my sources"
}'
Fuller request
curl -X POST https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/generate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"template_id": "podcast",
"topic": "How our Q2 results compare to the market",
"extra_context": "Audience is the board. Keep it under ten minutes.",
"model": "claude-sonnet",
"voice1_id": "EXAVITQu4vr4xnSDxMaL",
"voice2_id": "onwK4e9ZLuTAKqWW03F9",
"podcast_style": "debate",
"podcast_length": "short",
"source_file_ids": ["a3f1…", "b7c2…"]
}'
Example response
{
"job_id": "6d2f8a91-3c5e-4b7d-9a10-8f4e2c6b5d33",
"status": "queued",
"download_url": null,
"error": null,
"source_file_ids": ["a3f1…", "b7c2…"]
}
What happens, in order
- The template is resolved. Unknown id →
404. - The credit cost is computed.
- Credits are charged. Insufficient balance →
402, and no job is created. - The job row is written with
status: "queued". - Rendering starts in the background and the response returns.
The charge happens at submission, not on success. If generation subsequently fails — a model outage, a bad prompt — the job lands in error and the credits are already spent. Budget for this, and prefer a model you know is healthy.
Errors
| Status | When |
|---|---|
403 | You have viewer role on the notebook; generation needs member or above |
404 | Unknown template_id, or the notebook does not exist / is not yours |
402 | Not enough credits for the computed cost |
422 | template_id or topic missing, or a field has the wrong type |
Models and credit cost
GET /api/notebooks/{notebook_id}/generate/models
List the models you may pass as model, with their credit multipliers.
curl https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/generate/models \
-H "Authorization: Bearer $TOKEN"
[
{
"key": "claude-opus",
"label": "Claude Opus 4.8",
"provider": "anthropic",
"credit_multiplier": 1.8,
"is_default": true
},
{
"key": "claude-sonnet",
"label": "Claude Sonnet 4.6",
"provider": "anthropic",
"credit_multiplier": 1.1,
"is_default": false
}
]
| Model key | Label | Multiplier | Notes |
|---|---|---|---|
claude-opus | Claude Opus 4.8 | 1.8 | Default when model is omitted. Highest quality |
claude-sonnet | Claude Sonnet 4.6 | 1.1 | Noticeably cheaper, strong for most documents |
Pass the key value as model. Read the list at runtime rather than hardcoding it — the registry is overridable from configuration, so models can be added or withdrawn without a release.
Earlier builds advertised gpt-5.5 and gpt-5.4. They have been removed from the registry because the platform's OpenAI credential is revoked — selecting one produced a 401 inside the render step, failing the job after credits had been charged. Only Anthropic models are offered now.
Retrieval and ingestion were never affected: embeddings run locally and need no third-party key.
How cost is calculated
base = the template's declared credit_cost
↳ if absent, a per-output-type fallback
↳ if that is absent too, 25
charge = max(1, ceil(base × model_multiplier))
The per-output-type fallback is: podcast 100, audio_overview 50, video 25, datatable 12, everything else 25. A template that declares its own credit_cost always wins over the fallback.
Credit costs by template
At the default model (claude-opus, 1.8×) — the number a caller actually pays unless they pick another model.
| Template | template_id | Output | Base | Cost at 1.8× |
|---|---|---|---|---|
| Podcast (2-speaker) | podcast | mp3 | 100 | 180 |
| Audio Overview | audio_overview | mp3 | 50 | 90 |
| Video Overview (HTML slides) | video_overview | html | 50 | 90 |
| Presentation | investor_deck | pptx | 25 | 45 |
| Research Summary | research_summary | docx | 25 | 45 |
| Executive Report | executive_report | docx | 25 | 45 |
| Article / Blog Post | article | docx | 25 | 45 |
| Business Letter | business_letter | docx | 25 | 45 |
| Email Communication | email_comms | docx | 25 | 45 |
| Product Description | product_description | docx | 25 | 45 |
| Quarterly Forecast | quarterly_forecast | docx | 25 | 45 |
| SOP | sop | docx | 25 | 45 |
| Meeting Minutes | meeting_minutes | docx | 25 | 45 |
| Infographic | infographic | html | 25 | 45 |
| Mind Map | mindmap | html | 12 | 22 |
| Flashcards | flashcard | html | 12 | 22 |
| Quiz | quiz | html | 12 | 22 |
| Data Table | datatable | xlsx | 12 | 22 |
| Executive Briefing (bundle) | executive_briefing | zip | 112 | 202 |
Multiply by the ratio to price another model — research_summary on claude-sonnet is ceil(25 × 1.1) = 28 credits, against 45 on Opus.
investor_deck is a pitch deck, not a generic presentationIt is the only pptx system template, and its prompt produces a seven-slide investor pitch — problem, solution, market, traction, team, ask — regardless of the topic you give it. If you need a neutral deck, author a user template.
Prompt placeholders
Exactly five placeholders are substituted. Anything else you write in a template prompt stays literal.
| Placeholder | Filled with |
|---|---|
{{retrieved_context}} | The top 20 source chunks matching topic, joined as File: <name> then the text. When the notebook has no sources: (No relevant source documents available) |
{{user_topic}} | Your topic |
{{extra_context}} | Your extra_context |
{{podcast_style}} | Your podcast_style |
{{podcast_length}} | Your podcast_length |
A notebook's brain_prompt and its brand kit are prepended to the system prompt rather than substituted, so you do not need a placeholder for them.
Composite templates
A composite template fans out into several children and packages the results.
composite_mode: "bundle"— each child is rendered and stored separately; the parent artifact is a zip, and per-child status is reported individually.composite_mode: "embedded"—docxchildren are stitched into one combined document.
A composite ignores its own declared credit_cost and charges the sum of its children. executive_briefing fans out to research_summary + datatable + infographic + audio_overview, which is why it costs 112 base (202 at the default model).
Partial success is delivered, not discarded: the job only fails if every child failed. Inspect the children array on the artifact to see which succeeded.
Inline enrichment
enrich pulls fresh content off the web immediately before retrieval, so a generation can reference material that is not yet in the notebook.
| Field | Type | Required | Notes |
|---|---|---|---|
mode | string | yes | search, crawl, or url |
query | string | for search | The search query |
url | string | for crawl and url | Page or site to fetch |
max_pages | int | null | no | Crawl breadth limit |
ingest | bool | null | no | true (default) makes the content a permanent notebook source; false uses it for this generation only |
{
"template_id": "research_summary",
"topic": "Competitor pricing changes this quarter",
"enrich": {
"mode": "search",
"query": "competitor pricing announcement Q2 2026",
"ingest": false
}
}
Tracking a job
GET /api/notebooks/{notebook_id}/jobs/{job_id}
Poll a job's status. This is the endpoint to build against.
curl https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/jobs/$JOB_ID \
-H "Authorization: Bearer $TOKEN"
{
"job_id": "6d2f8a91-3c5e-4b7d-9a10-8f4e2c6b5d33",
"status": "done",
"download_url": "https://storage1.onfire.so/generated/…?X-Amz-Signature=…",
"error": null,
"source_file_ids": []
}
GET /api/generate-jobs/{job_id}
The same payload without needing the notebook id. Convenient when you have only tracked the job.
Job statuses
Exactly four values are ever written:
| Status | Meaning |
|---|---|
queued | Accepted, credits charged, not started |
running | Rendering in progress |
done | Finished. download_url is populated |
error | Failed. error carries the reason. Credits are not refunded |
running is a single opaque phase — there is no retrieving, generating, rendering, or synthesizing. If you need a progress bar, drive it on elapsed time; the API will not report intermediate stages.
Polling
Poll every 3 seconds and stop when status is done or error. Typical durations: 60–120 seconds for a document on Opus, 1–3 minutes for a podcast.
while true; do
STATUS=$(curl -s "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/jobs/$JOB_ID" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['status'])")
echo "status: $STATUS"
[ "$STATUS" = "done" ] && break
[ "$STATUS" = "error" ] && exit 1
sleep 3
done
GET /api/notebooks/{notebook_id}/jobs/{job_id}/stream
A Server-Sent Events stream of the same job status. Accepts ?token= for EventSource.
It polls the job row every 2 seconds and hard-stops after 90 iterations. Podcasts and long documents routinely take longer than that, so the stream can end before the job does — on exactly the jobs you would most want to stream. Prefer polling.
Notifications
On completion the platform writes a notification row — generate_done or generate_error — readable at GET /api/notifications/. This is best-effort and written inside a try/except, so treat it as a nice-to-have for a bell icon after the user has navigated away, not as your primary completion signal.
Next
Once a job reaches done, fetch the file — see Artifacts.