Pipeline API — one call, many outputs
One request takes a recording (or any supported source file), adds it to a project's sources, transcribes it, and runs several templates on it in parallel. When every output is ready, the platform POSTs the download links to your callback_url. You can also poll for them.
POST /api/v1/notebooks/{notebook_id}/pipeline {file | file_url, template_ids, callback_url}
→ 202 {run_id, jobs[], credits_required}
→ … upload → transcribe → file appears in the project's Sources …
→ … every template runs on the FULL transcript (chained ones on their parent's output) …
→ POST callback_url {status: "done", jobs: [{template_name, filename, download_url}, …]}
research-api.onfire.soresearch.onfire.so sits behind Cloudflare Bot Fight Mode, which challenges automation platforms that call from cloud data centres (Zapier's Code steps get a 403 that never reaches the API). The same API is served for machines at https://research-api.onfire.so/api/v1/…, covering both endpoints on this page. That host serves only /api/v1/; authentication and rate limits are identical.
It is the endpoint to call from Zapier, Make or n8n: it accepts an X-API-Key, so there is no login step and no token expiry. It also takes a form, multipart or JSON body.
POST /api/v1/notebooks/{notebook_id}/pipeline
- Auth:
X-API-Key: rsk_…(create one withPOST /api/api-keys). A JWT also works. - Permission: member or higher on the notebook.
- Status:
202 Accepted. - Rate limit: 10 runs/hour per caller.
Fields
Send them as multipart/form-data (required when uploading the file itself), application/x-www-form-urlencoded, or application/json.
| Field | Required | Notes |
|---|---|---|
file | one of | The file itself (multipart). Max ~100 MB through Cloudflare. |
file_url | one of | A public http(s) URL the platform downloads, e.g. a Dropbox or Google Drive direct link. Up to 300 MB. Internal and private addresses are refused. |
filename | no | Overrides the name taken from the upload or the URL. Set it when the URL has no file extension. |
template_ids | yes | The templates to run, up to 10. Use a list, or a comma-separated string. A custom template's id can be sent bare (9261e63e-…) or prefixed (usr:9261e63e-…). System templates use their id (email_html). Template IDs are shown in the template editor. |
inputs_from | no | Chains templates: child=parent, comma-separated, or a JSON object {"child": "parent"}. The child runs after the parent and receives the parent's text output as additional context. |
topic | no | Defaults to the filename with _ turned into spaces, e.g. SIHCX2025 1015 Playbook General Counsel Expectations Review. Templates that read the audience from the first word of the input see it first. |
extra_context | no | Free text appended to every template's prompt. |
model | no | A key from GET /api/notebooks/{id}/generate/models. Defaults to Claude Opus. |
callback_url | no | A public URL that receives the final payload as a JSON POST. Zapier: use the URL of a Catch Hook trigger. |
What each template sees
- The full transcript of this file. Templates do not get a search over the whole project. Older sources in the project do not leak into an output about this recording.
- The project's instructions, brand kit and privacy settings, exactly as in normal generation.
- Custom templates whose prompts were written for another tool, with no
{{retrieved_context}}placeholder (the whole brief in the system prompt, for example), still receive the topic, the transcript and the JSON shape their renderer needs.
Response — 202 Accepted
{
"run_id": "a82cc7cd-ffbd-44f0-84d7-0158926d59cf",
"status": "queued",
"notebook_id": "cda482a4-f401-4059-9160-dbd7efe05e04",
"file": {"id": "a82cc7cd-…", "filename": "SIHCX2025_1015_Playbook_General Counsel Expectations Review.mp3", "status": "ingesting"},
"topic": "SIHCX2025 1015 Playbook General Counsel Expectations Review",
"credits_required": 315,
"status_url": "/api/v1/pipeline-runs/a82cc7cd-ffbd-44f0-84d7-0158926d59cf",
"jobs": [
{"job_id": "f4240533-…", "template_id": "usr:9261e63e-…", "template_name": "Mailchimp Post Exchange Email", "credits": 45, "input_from": null, "status": "queued"},
{"job_id": "58c8fa58-…", "template_id": "usr:fd90aacd-…", "template_name": "Mighty Networks Article", "credits": 45, "input_from": "usr:9261e63e-…", "status": "queued"}
]
}
run_id is also the id of the new source file in the project.
Errors
| Status | When |
|---|---|
401 | Missing or invalid API key |
402 | The balance does not cover every template. Nothing is stored. |
404 | Notebook not found / not yours, or an unknown template_id |
413 | Upload over 300 MB |
422 | Neither or both of file/file_url; unsupported file type; no template_ids; more than 10; inputs_from naming a template not in the list, or forming a cycle; a refused URL |
429 | More than 10 runs in the hour |
GET /api/v1/pipeline-runs/{run_id}
Returns the run. Every poll re-signs the links.
Add ?include_content=true to also get each finished output's words. It adds a content object to every job: title, subtitle, preheader, text, body_html (line-break HTML for Mighty Networks or Gmail), and html (the full file, for HTML outputs). It also adds an outputs object keyed by template name, e.g. outputs.mighty_networks_article.content.body_html. The callback always includes content.
{
"run_id": "a82cc7cd-…",
"status": "done",
"file": {"id": "a82cc7cd-…", "filename": "…mp3", "status": "ready", "error": null},
"jobs": [
{
"job_id": "541d64d2-…",
"template_name": "Exchange Newsletter (HTML)",
"status": "done",
"title": "Senior In-House Counsel Exchange: October Notes",
"filename": "Senior In-House Counsel Exchange- October Notes.html",
"download_url": "https://…r2.cloudflarestorage.com/…",
"preview_url": "https://…",
"error": null
}
],
"completed": 7, "failed": 0, "total": 7
}
status | Meaning |
|---|---|
transcribing | The file is being ingested / transcribed |
generating | Templates are running |
done | Every output is ready |
partial | Some outputs failed (see each job's error); the rest are ready |
error | Nothing was produced (e.g. the file could not be downloaded or had no speech) |
download_url forces a save. preview_url opens inline. Both expire after 24 hours. Poll again for fresh links, or fetch the file in the next Zap step.
The outputs also appear in the project's Studio panel like any other generation.
POST /api/v1/pipeline-runs/{run_id}/retry
Re-runs only the failed outputs of a finished run, on the transcript the run already has. It does no new download, no new transcription, and charges no transcription credits. The retried jobs keep their ids, so a Zap just replays its "collect outputs" step afterwards. A chained template whose parent succeeded the first time receives that parent's saved output.
- Auth:
X-API-Key(or a JWT), member or higher on the notebook. - Body (optional):
{"inputs_from": {...}}. It defaults to the chain the run was submitted with. - Response
202:{run_id, status: "generating", credits_required, retrying: [{job_id, template_name, credits, input_from}]}. - Errors:
409if the run is still in progress, has no failed outputs, or its source never finished ingesting (submit a new run in that case).402if the balance cannot cover the retried templates.
Credits
- Each template is priced as in Generation, including the model multiplier. At submit time the balance must cover every template, or you get a
402and nothing happens. - Each template is charged when it starts running. A file that cannot be downloaded or has no speech costs nothing. A template that starts and then fails is not refunded, the same as single generation.
- Transcription is metered separately, as for any audio upload: 1 credit per 2 minutes of audio.
Zapier recipe
Zap 1 — submit
- Trigger: New File in Folder (Google Drive / Dropbox / OneDrive), or any step that yields an MP3.
- Action: Webhooks by Zapier → POST
- URL:
https://research.onfire.so/api/v1/notebooks/<NOTEBOOK_ID>/pipeline - Payload type:
form - Data:
file_url→ the file's direct download link from step 1. Alternatively, map the file itself to afilefield.filename→ the file name from step 1template_ids→a4ab7511-…,3306832b-…,80e827d0-…,8081e906-…,9261e63e-…,fd90aacd-…,efbb7d03-…inputs_from→fd90aacd-…=9261e63e-…callback_url→ the Catch Hook URL from Zap 2
- Headers:
X-API-Key→ yourrsk_…key
- URL:
Zap 2 — receive
- Trigger: Webhooks by Zapier → Catch Hook. Its URL is the
callback_urlabove. - Actions: use
jobs[].download_url/jobs[].filenameto save each output to Drive, email it, or post it to Slack. Use Zapier's Looping step to handle every job.
file_url over uploading the bytesZapier waits for the 202. With file_url the response is immediate, and the platform downloads the file afterwards. Uploading a large file directly makes Zapier hold the connection for as long as the upload takes.
A platform deploy during a run can interrupt it. The run then shows its jobs stuck in running. Submit it again.