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_idstringyes—System id, or usr:<uuid> for a user template
topicstringyes—See 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.