HuluFlow · REST reference
API reference
Base path /api/v1. Authenticate with Authorization: Bearer hulu_…. Errors use {"detail": …}.
This page is a reference for developers. If you have never used HuluFlow, complete Getting started in the console first, then read the Workflows & nodes guide. Come here when another system must create workflows or read rows with a Bearer key.
Create an API key under Console → API keys. The raw token starts with hulu_ and is shown once — store it like a password. Website login cookies do not work on /api/v1.
- Auth
- Workflows
- Run
- Datasets
Authentication
Every /api/v1 call needs the Bearer header. Session cookies are for the console only. Invalid or missing keys return 401. Prefer testing the same graph in the console before automating it.
Console → API keys. Header: Authorization: Bearer hulu_…
Authorization: Bearer hulu_…
curl https://huluflow.com/api/v1/workflows \ -H "Authorization: Bearer $HULU_KEY"
Workflows
DAG of scrape, store, and notify nodes. Runs consume credits by webpage requests.
List workflows
All workflows for the key’s account, newest first. Response items use graph_json (not graph).
GET /api/v1/workflows
Create workflow
graph defaults to empty nodes/edges. status is active or paused. interval_minutes defaults to 1440. Runs need remaining credits (saving the graph does not).
POST /api/v1/workflows
Content-Type: application/json
{
"name": "Product pipeline",
"status": "paused",
"interval_minutes": 1440,
"graph": { "nodes": [], "edges": [] }
}
Get workflow
404 if the id is not yours.
GET /api/v1/workflows/{id}
Update workflow
Partial update. Omit a field to leave it unchanged. Sending graph replaces the whole DAG. Credits are checked when you run, not on save.
PATCH /api/v1/workflows/{id}
Content-Type: application/json
{
"name": "Renamed",
"status": "active",
"interval_minutes": 60,
"graph": { "nodes": [], "edges": [] }
}
Delete workflow
Removes the workflow. Returns {"ok": true}. Datasets already written are kept.
DELETE /api/v1/workflows/{id}
{"ok": true}
Run workflow
Runs the graph now and waits until it finishes. Success is a WorkflowRun plus node_runs. Engine or node failures return 502 with detail (string or {error, run, node_runs}).
POST /api/v1/workflows/{id}/run
{
"id": 1,
"workflow_id": 12,
"status": "ok",
"error": null,
"started_at": "2026-08-24T12:00:00",
"finished_at": "2026-08-24T12:00:08",
"node_runs": [
{
"id": 10,
"node_id": "s1",
"node_type": "scrape",
"status": "ok",
"input_json": {},
"output_json": { "items": [{ "title": "Mug", "url": "https://shop.example/p/mug" }] },
"error": null
}
]
}
Run history
Newest first. limit 1–100, default 20. Detail includes each node’s input_json, output_json, status, and error.
GET /api/v1/workflows/{id}/runs?limit=20
GET /api/v1/workflows/{id}/runs/{run_id}
Scrape preview (field discovery)
Field discovery only. Does not write datasets. Requires an existing workflow id (any of yours) so the call is authenticated in that account. mode list or detail.
POST /api/v1/workflows/{id}/scrape-preview
Content-Type: application/json
{
"url": "https://example.com/list",
"mode": "list",
"requirement": "title, price, url"
}
{
"ok": true,
"url": "https://example.com/list",
"fields": [{ "name": "title", "label": "title", "type": "text" }],
"sample": [{ "title": "Mug", "url": "https://shop.example/p/mug" }]
}
Minimal graph
Put these objects in graph.nodes[].config. edges use from and to (node ids), not source/target. If you are brand new, read the Workflows & nodes guide first, then copy JSON here.
{
"nodes": [
{
"id": "g1",
"type": "url_gen",
"config": {
"mode": "range",
"template": "https://example.com/list?page={page}",
"param": "page",
"start": 1,
"end": 3,
"step": 1
}
},
{
"id": "s1",
"type": "scrape",
"config": {
"url": "https://example.com/list",
"mode": "list",
"requirement": "title, price, url",
"limit": 20,
"fields": [
{ "name": "title", "label": "title", "type": "text" },
{ "name": "url", "label": "url", "type": "url" }
]
}
},
{
"id": "st1",
"type": "store",
"config": { "dataset_name": "products", "key_fields": ["url"] }
}
],
"edges": [
{ "from": "g1", "to": "s1" },
{ "from": "s1", "to": "st1" }
]
}
Node config
Put these objects in graph.nodes[].config. edges use from and to (node ids), not source/target. If you are brand new, read the Workflows & nodes guide first, then copy JSON here.
scrape
Needs config.url and/or upstream items. mode list returns many rows from a listing page; detail returns one row per URL and can merge the parent list row. After discovery, keep fields. limit caps URLs for that node this run (default 20). input_field picks the upstream link column for list→detail. For plain-language walkthroughs see the Workflows & nodes guide.
{
"url": "https://example.com/item/1",
"mode": "list | detail",
"requirement": "title, price, url",
"limit": 20,
"input_field": "url",
"fields": [{ "name": "title", "label": "title", "type": "text" }]
}
url_gen
No inputs. range: template must contain {param} (default page). list: urls_text one URL per line. Max 500. Connect only to scrape. Beginners should preview the generated count in the console first.
{
"mode": "range",
"template": "https://example.com/list?page={page}",
"param": "page",
"start": 1,
"end": 10,
"step": 1
}
{
"mode": "list",
"urls_text": "https://example.com/a\nhttps://example.com/b"
}
store
Reads upstream items. dataset_id pins an existing table; otherwise dataset_name creates or reuses by name. key_fields hash for upsert identity (default url, link) — prefer a stable product URL. store_fields optionally limits written columns.
{
"dataset_id": 4,
"dataset_name": "products",
"key_fields": ["url"],
"store_fields": ["title", "price", "url"]
}
notify
when=new emails new row keys; when=field_change compares watch_fields to the previous run of this node; other values hash the whole items list. email defaults to the account. When testing alerts, run twice to establish a baseline.
{
"email": "ops@example.com",
"when": "new | field_change",
"watch_fields": ["price"],
"subject": "[HuluFlow] price change"
}
Datasets
Tables you own, with row_count, column_count, and columns [{key, label, type}].
GET /api/v1/datasets
Create dataset
Body {"name": "…"}. Empty name becomes New table. Store nodes can also create tables by dataset_name.
POST /api/v1/datasets
Content-Type: application/json
{ "name": "products" }
Get dataset
GET /api/v1/datasets/{id}
List rows
page ≥ 1, limit 1–200 (default 50). Response: page, page_size, total, total_pages, columns, rows [{id, key_hash, data, created_at, updated_at}]. data is the stored object.
GET /api/v1/datasets/{id}/rows?page=1&limit=50
Export table
GET /api/v1/datasets/{id}/export?format=csv|json. Streams CSV (UTF-8 BOM) or returns JSON {id, name, columns, total, rows}. Max 100,000 rows (413 if over). Same download is available in the console.
GET /api/v1/datasets/{id}/export?format=csv
GET /api/v1/datasets/{id}/export?format=json
curl -L "https://huluflow.com/api/v1/datasets/$DATASET_ID/export?format=csv" \
-H "Authorization: Bearer $HULU_KEY" \
-o table.csv
Node presets
Presets: scrape, notify, url_gen only.
Body {name, type, config}. Scrape must include selected fields. url_gen config is validated (template placeholder, max 500).
GET /api/v1/node-presets
POST /api/v1/node-presets
GET /api/v1/node-presets/{id}
DELETE /api/v1/node-presets/{id}
POST /api/v1/node-presets
Content-Type: application/json
{
"name": "List scrape",
"type": "scrape",
"config": {
"mode": "list",
"url": "https://example.com/list",
"fields": [{ "name": "title", "label": "title", "type": "text" }]
}
}
Errors
Errors use {"detail": …}. Quota on create/update is HTTP 402. Run failures are 502.
401 invalid key. 402 out of credits. 404 missing workflow, run, dataset, or preset. 400 bad body or scrape-preview URL. 502 crawl engine or run failure. FastAPI errors are {"detail": … } — detail may be a string or an object.
Success shape
{ "ok": true }
Error shape
{ "detail": "Invalid API key" }
Quota denial
POST …/run (or scheduled runs) with insufficient credits returns 402. Upgrade or wait for the next period, then retry.
{ "detail": "Credit quota exceeded" }
HTTP 402
Limits
Credits = webpage requests by scrape nodes this period. URL generator max 500 URLs per node. Scrape default limit 20 URLs per node per run. Dataset rows: max 200 per page; export max 100,000 rows. Run list: max 100. Preview is discovery, not bulk extract.
Models
Shared headers, response bodies, and errors across endpoints.
What next
Need field-level config? Read Workflows & nodes. Want a curl walkthrough? API-first guide.