OnFire Research API
OnFire Research is a retrieval-augmented document platform. You collect source material into a notebook — PDFs, web pages, audio, video, pasted text — and then generate finished work from it: reports, decks, spreadsheets, audio overviews, mind maps, quizzes.
Everything the web app at research.onfire.so does is available over this API.
| Base URL | https://research.onfire.so/api |
| Auth | JWT bearer token, or an API key on three specific routes |
| Format | JSON, except file upload which is multipart/form-data |
| Interactive reference | /api/docs — live OpenAPI/Swagger UI |
| Health check | /api/healthz |
The shape of the API
Three concepts, in the order you use them:
- Notebook — a container. Sources go in; everything you generate is grounded in them.
- Source — one ingested document. Ingestion is asynchronous: a source is
ingestinguntil it has been chunked and embedded, thenready. - Generation job — a template applied to a notebook. Also asynchronous: you submit, get a
job_id, and poll untildone. The result is an artifact with a signed download URL.
The two asynchronous steps are the thing to design around. Nothing useful comes out of generating against a notebook whose sources are still ingesting.
Quickstart
This walks the full path — log in, create a notebook, add three kinds of source, generate a report, download it. Every command is runnable as written.
1. Get a token
export TOKEN=$(curl -s -X POST https://research.onfire.so/auth/login_user \
-H "Content-Type: application/json" \
| python3 -c "import sys,json; print(json.load(sys.stdin)[0]['access_token'])")
echo "${TOKEN:0:24}…"
The token is valid for 24 hours.
Bad credentials come back as 200 with success: false and access_token: null. The command above will print None rather than erroring. Check the success field — see Authentication.
2. Create a notebook
export NOTEBOOK_ID=$(curl -s -X POST https://research.onfire.so/api/notebooks/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "API quickstart"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "notebook: $NOTEBOOK_ID"
3. Add sources
Three routes, three kinds of input. Run any or all.
Upload a file — multipart/form-data, not JSON:
curl -s -X POST "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/files/" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./quarterly-report.pdf"
Paste raw text:
curl -s -X POST "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/files/ingest-text" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"text": "Revenue grew 24% year over year, driven by enterprise renewals.",
"title": "Q2 headline numbers"
}'
Scrape a web page:
curl -s -X POST "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/files/ingest-url" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/2026-market-report"}'
Each returns a file record with "status": "ingesting".
4. Wait for ingestion
Sources must reach ready before they can be retrieved against.
until curl -s "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/files/" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "
import sys, json
files = json.load(sys.stdin)
pending = [f for f in files if f['status'] == 'ingesting']
print(f\"{len(files) - len(pending)}/{len(files)} ready\", file=sys.stderr)
sys.exit(1 if pending else 0)
"; do sleep 3; done
echo "all sources ready"
5. Pick a template
curl -s "https://research.onfire.so/api/notebooks/$NOTEBOOK_ID/templates" \
-H "Authorization: Bearer $TOKEN" \
| python3 -m json.tool | head -30
Nineteen system templates are available. research_summary produces a Word document and is a good default. See the full list with credit costs.
6. Generate
export JOB_ID=$(curl -s -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"
}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo "job: $JOB_ID"
Returns 202 Accepted immediately. topic is required — it fills the prompt and is the retrieval query against your sources, so make it specific.
The charge happens when the job is accepted. A job that later fails does not refund. See Errors & Limits.
7. Poll the job
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" ] && { echo "generation failed"; exit 1; }
sleep 3
done
Statuses are queued, running, done, error — those four and no others. Expect 60–120 seconds for a document, longer for audio.
8. Download the artifact
DOWNLOAD_URL=$(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)['download_url'])")
curl -L -o research-summary.docx "$DOWNLOAD_URL"
open research-summary.docx # or xdg-open on Linux
The URL is pre-signed and carries its own authorization — do not attach your bearer token to it. It expires after 24 hours; re-request the artifact for a fresh one rather than caching the link.
Where to go next
| If you want to | Read |
|---|---|
| Set up a machine client, or understand the JWT and API-key split | Authentication |
| Ingest files, URLs, YouTube or raw text; organise with folders | Notebooks & Sources |
| Understand templates, models, credit costs and job lifecycle | Generation |
| List, inspect and download results, including composite bundles | Artifacts |
| Handle failures, retries and rate limits correctly | Errors & Limits |
| Turn a file URL into a document, with a webhook callback | Conversion API — rolling out |
Three things that surprise people
API keys work on almost nothing — and never for file upload. Only ingest-text, enrich-search, the Sonix routes and the workflow routes accept X-API-Key. Everywhere else the header is ignored and you get 401 Missing Bearer token, which looks like a bad key but means the route only speaks JWT. In particular, uploading a file is JWT-only: automations that push documents must log in. See Authentication and Automating ingestion.
topic is doing two jobs. It is not just a title — it is the retrieval query that decides which of your sources reach the model. A vague topic produces a vague document even when your sources are excellent.
Trailing slashes matter. Collection routes are registered with one (/notebooks/, /files/, /artifacts/). Without it you get a 307 redirect — harmless, but it doubles the request count on anything you poll.