Skip to main content

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".

System templates have an empty visibility

System 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
Ids here are raw UUIDs

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_typeFileNotes
docx.docxWord document — the most common
pptx.pptxPowerPoint deck
podcast.mp3Two-speaker conversation, uses both voices
audio_overview.mp3Single narrator, uses voice1_id only
video.htmlA self-contained Reveal.js slideshow — not an mp4
infographic.htmlInteractive HTML infographic
mindmap.htmlInteractive mind map
quiz.htmlInteractive quiz
flashcard.htmlInteractive flashcards
datatable.xlsxExcel spreadsheet
html.htmlStandalone styled HTML document
image.pngHTML rendered and screenshotted
infographic_image.pngRasterized infographic
pdf.pdfRendered and printed to PDF
composite.zipA bundle of several children — see Composites
video does not produce a video

output_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

FieldTypeRequiredDefaultNotes
template_idstringyesSystem id, or usr:<uuid> for a user template
topicstringyesSee below — this does double duty
extra_contextstringno""Injected into the prompt at {{extra_context}}
modelstring | nullnonull → registry defaultA key from the models endpoint. Affects credit cost
voice1_idstringno21m00Tcm4TlvDq8ikWAMHost voice. Used by audio_overview and podcast
voice2_idstringnopNInz6obpgDQGcFmaJgBGuest voice. podcast only
podcast_stylestringnointerviewinterview, debate, educational, deep_dive
podcast_lengthstringnomediumshort, medium, long
source_file_idsstring[] | nullnonullRecords which sources this artifact was built from
enrichobject | nullnonullPull fresh web content before generating — see Enrichment
topic is both the prompt and the search query

topic 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

  1. The template is resolved. Unknown id → 404.
  2. The credit cost is computed.
  3. Credits are charged. Insufficient balance → 402, and no job is created.
  4. The job row is written with status: "queued".
  5. Rendering starts in the background and the response returns.
Credits are charged before the work runs

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

StatusWhen
403You have viewer role on the notebook; generation needs member or above
404Unknown template_id, or the notebook does not exist / is not yours
402Not enough credits for the computed cost
422template_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 keyLabelMultiplierNotes
claude-opusClaude Opus 4.81.8Default when model is omitted. Highest quality
claude-sonnetClaude Sonnet 4.61.1Noticeably 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.

OpenAI models have been withdrawn

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.

Templatetemplate_idOutputBaseCost at 1.8×
Podcast (2-speaker)podcastmp3100180
Audio Overviewaudio_overviewmp35090
Video Overview (HTML slides)video_overviewhtml5090
Presentationinvestor_deckpptx2545
Research Summaryresearch_summarydocx2545
Executive Reportexecutive_reportdocx2545
Article / Blog Postarticledocx2545
Business Letterbusiness_letterdocx2545
Email Communicationemail_commsdocx2545
Product Descriptionproduct_descriptiondocx2545
Quarterly Forecastquarterly_forecastdocx2545
SOPsopdocx2545
Meeting Minutesmeeting_minutesdocx2545
Infographicinfographichtml2545
Mind Mapmindmaphtml1222
Flashcardsflashcardhtml1222
Quizquizhtml1222
Data Tabledatatablexlsx1222
Executive Briefing (bundle)executive_briefingzip112202

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 presentation

It 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.

PlaceholderFilled 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"docx children 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.

FieldTypeRequiredNotes
modestringyessearch, crawl, or url
querystringfor searchThe search query
urlstringfor crawl and urlPage or site to fetch
max_pagesint | nullnoCrawl breadth limit
ingestbool | nullnotrue (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:

StatusMeaning
queuedAccepted, credits charged, not started
runningRendering in progress
doneFinished. download_url is populated
errorFailed. error carries the reason. Credits are not refunded
There are no sub-statuses

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.

The stream stops after 3 minutes

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.