Stateless document conversion API
Genflows API
Convert procedure PDFs and images to V1 - VLM JSON or V2 - CaptureGraph DAG. Provider keys are request-only and are not stored.
Interactive Request
Choose a version and provider. Upload a PDF or image, or use the bundled sample.
Request Examples
Response
No request sent yet.
Empirical evaluation pricing
Cost Estimator
Estimate model API spend from the observed August 2026 evaluation. One repetition processes every document once with each selected provider.
Rates include the retry and regeneration behavior observed across the 10-document pilot and 79-document full study.
- Document-runs
- 79
- Selected unit cost
- $0.3957
Model API charges only. Hosting, storage, taxes, local evaluation compute, and unrelated account activity are excluded.
API Reference
Shared request fields apply to both conversion versions.
JSON Request Shape
{
"provider": "openai",
"api_key": "$OPENAI_API_KEY",
"filename": "document.pdf",
"mime_type": "application/pdf",
"document_base64": "<base64 file bytes>",
"prompt": "Extract procedural steps."
}
Shared Fields
| provider | openai, gemini, anthropic |
| api_key | request-only provider key |
| file | multipart PDF, PNG, JPG, or JPEG |
| document_base64 | JSON request file bytes |
| prompt | optional extraction instructions |
| model | optional provider model override |
Errors return JSON with error and docs_url. Typical requests take 10 seconds to 4 minutes depending on file size and provider.
Conversion Endpoints
POST /api/convert V1 - VLM JSON
Returns procedures, sections, and steps. Accepts multipart uploads or JSON with base64 file bytes.
curl -X POST https://genflows-api.vercel.app/api/convert \
-F provider=openai \
-F api_key="$OPENAI_API_KEY" \
-F file=@/path/to/document.pdf \
-o result.json
POST /api/v2/convert V2 - CaptureGraph DAG
Returns a CaptureGraph procedure DAG. Same fields as V1.
curl -X POST https://genflows-api.vercel.app/api/v2/convert \
-F provider=openai \
-F api_key="$OPENAI_API_KEY" \
-F file=@/path/to/document.pdf \
-o procedure.json
Node Types
Nodes use kind, return_type, optional label, settings, inputs, and substeps.
| Kind | Returns | Role |
|---|---|---|
| ProcedureSequence | PVoid | Ordered substeps |
| ShowInstructions | PBool | Instruction text |
| RequireProcedureCompleted | PVoid | Required wrapped step |
| UserInputBool | PBool | Yes / No prompt |
| IfThenElse | PVoid | Boolean branch |
| SwitchString | PVoid | One branch per selected option |
| Stopwatch | PNumber | Timer in seconds |
| PaceKeeper | PVoid | Metronome |
| UserInputString | PString | Free-text field |
| UserInputNumber | PNumber | Numeric input |
| UserInputSelectString | PString | Fixed options |
| DoNothing | PVoid | No-op |
| NullProcedure | any | Typed null |
Output Shape
capturegraph.root points to the first node. capturegraph.nodes stores the DAG.
{
"procedure_name": "Adult Cardiac Arrest",
"format": "capturegraph",
"api_version": "2",
"metadata": {
"node_count": 17,
"expects_cycles": true,
"contains_cycles": true,
"model_used": "OpenAI gpt-4o",
...
},
"capturegraph": {
"root": "10",
"nodes": {
"00": {"kind": "NullProcedure", "return_type": "PImage"},
"01": {"kind": "ShowInstructions", "return_type": "PBool",
"label": "Start CPR",
"settings": {"text": "Give oxygen. Attach monitor."},
"inputs": {"image": "00"}},
"02": {"kind": "RequireProcedureCompleted", "return_type": "PVoid",
"inputs": {"procedure": "01"}},
"06": {"kind": "UserInputBool", "return_type": "PBool",
"label": "Shockable Rhythm?",
"settings": {"true_text": "Yes", "false_text": "No"}},
"0F": {"kind": "IfThenElse", "return_type": "PVoid",
"inputs": {"condition": "06",
"true_branch": "0B",
"false_branch": "0E"}},
"10": {"kind": "ProcedureSequence", "return_type": "PVoid",
"label": "Adult Cardiac Arrest",
"substeps": ["02", "03", "05", "0F"]}
}
}
}
Traversal Rules
Walk from root:
- ProcedureSequence: run
substepsin order. - IfThenElse: evaluate
inputs.condition, then followinputs.true_branchorinputs.false_branch. - RequireProcedureCompleted: run
inputs.procedure. - ShowInstructions: show
settings.text. - Stopwatch / PaceKeeper / UserInput*: render the matching control and store the result.
- NullProcedure / DoNothing: skip.
Nodes may be reused. Unlabeled return arrows are emitted as unconditional cycle edges. The procedure viewer supports cycles that complete at least one interactive step before re-entering the loop.
Custom Viewer
A viewer only needs capturegraph.root, capturegraph.nodes, and a value map for completed leaf nodes.
- Load
rootandnodesfrom the V2 response. - Find the next active leaf node from
root. - Render that node from its
kind,label, andsettings. - On completion, save
values[activeId]and recompute the next active node. - For Back, remove the last saved value and recompute from
root.
Open Viewer sends the latest V2 response to /viewer.
function resolve(id, nodes) {
const n = nodes[id];
if (!n) return null;
if (n.kind === "RequireProcedureCompleted")
return resolve(n.inputs?.procedure, nodes);
if (n.kind === "NullProcedure" || n.kind === "DoNothing") return null;
return id;
}
function branchId(node, takeTrue) {
return takeTrue ? node.inputs?.true_branch : node.inputs?.false_branch;
}
function switchBranchId(node, selectedValue) {
const index = (node.settings?.options ?? []).indexOf(selectedValue);
return index >= 0 ? node.substeps?.[index] : null;
}
function nextStep(id, nodes, values) {
const rid = resolve(id, nodes);
if (!rid) return null;
const node = nodes[rid];
if (node.kind === "ProcedureSequence") {
for (const child of node.substeps ?? []) {
const active = nextStep(child, nodes, values);
if (active) return active;
}
return null;
} else if (node.kind === "IfThenElse") {
const condition = resolve(node.inputs?.condition, nodes);
if (values[condition] === undefined) return nextStep(condition, nodes, values);
return nextStep(branchId(node, values[condition]), nodes, values);
} else if (node.kind === "SwitchString") {
const selection = resolve(node.inputs?.selection, nodes);
if (values[selection] === undefined) return nextStep(selection, nodes, values);
return nextStep(switchBranchId(node, values[selection]), nodes, values);
}
return values[rid] === undefined ? rid : null;
}
function complete(activeId, value, state) {
state.values[activeId] = value;
state.history.push(activeId);
state.activeId = nextStep(state.root, state.nodes, state.values);
}
function back(state) {
const id = state.history.pop();
if (id) delete state.values[id];
state.activeId = nextStep(state.root, state.nodes, state.values);
}
Node UI
Use label for titles. Use settings and completion values by node kind.
| Kind | UI element |
|---|---|
| ProcedureSequence | Section / step list |
| ShowInstructions | Info card: settings.text; button uses optional settings.button_text (default Next); save true |
| UserInputBool | Yes / No: true_text, false_text; save boolean |
| SwitchString | Match the saved selection to settings.options, then follow the same-index substeps branch |
| UserInputString | Text field; save string |
| UserInputNumber | Number field: min_value, max_value, step; save number |
| UserInputSelectString | Select / radio: options[]; save selected string |
| IfThenElse | Branch from saved condition value |
| Stopwatch | Timer; save elapsed seconds |
| PaceKeeper | Metronome at settings.bpm; save true |
| RequireProcedureCompleted | Resolve and run inputs.procedure |
| NullProcedure / DoNothing | Skip |
Release history
Changelog
Newest first. Expand an update for changes and JSON DAG migration guidance.
Evaluation cost estimator
What changed
- The API webpage now estimates evaluation spend from document count, repetitions, selected providers, and a configurable safety margin.
- Provider unit costs are based on the archived 10-document pilot and 79-document full evaluation, including their observed regeneration overhead.
Compatibility
- No API request or response formats changed.
- The estimates cover model API charges only and are planning figures rather than live billing quotes.
Deterministic V2 navigation repair and reduced regeneration
What changed
- Dangling V2
gotomarkers are removed deterministically while preserving all surrounding clinical branch content. - Every structural repair is disclosed in
metadata.deterministic_repairs. - Duplicate wording and one-option choices no longer trigger a second full-document model generation.
- Broken loop targets and other clinically meaningful structural failures still receive the existing focused repair attempt.
Existing JSON DAG compatibility
- No migration is required; the new metadata field is additive.
- Regenerated workflows may use fewer provider tokens when the first extraction contains only repairable navigation defects.
Unconditional loop extraction and V2 cycle generation
What changed
- The V2 extraction format now identifies return targets and represents unlabeled return arrows as explicit unconditional loop connectors.
- Loop connectors are compiled into real CaptureGraph back-edges, including loops reached from decision branches.
- V2 metadata now reports
expects_cyclesandcontains_cyclesso the viewer can validate the generated graph accurately.
Existing JSON DAG compatibility
- Existing V2 JSON files remain compatible and continue to load normally.
- Regenerate files containing unconditional return arrows to receive the new cycle edges and metadata.
Viewer graph integrity and cycle validation
What changed
- The viewer now detects missing node references, nodes disconnected from the root, and cyclic workflows that contain no cycle edge.
- Structurally incomplete workflows display
Reachable Path Completeinstead of incorrectly reporting the entire procedure as complete.
Existing JSON DAG compatibility
- No migration is required; existing graphs continue to load and now receive additional integrity checks.
Local Python test environment configuration
What changed
- Added workspace Python settings that select
.venv/bin/pythonand enable pytest discovery for thetestsdirectory. - The local virtual environment now installs both production and development requirements so editor analysis can resolve
pytestand application imports.
Existing JSON DAG compatibility
- No JSON DAG changes are required for this development-environment update.
Vercel production routing recovery
What changed
- Removed the catch-all Vercel rewrite that replaced every incoming path with
/api/index.pyand caused the homepage, viewer, and API routes to return JSON 404 responses. - Vercel now uses its native FastAPI routing so the application receives the original requested path.
- Added regression tests for the Vercel entrypoint, homepage, viewer, health endpoint, content types, and deployment configuration.
Existing JSON DAG compatibility
- No JSON DAG changes are required for this deployment-only fix.
- Existing v2 files, including
SwitchStringbranches and optionalbutton_textsettings, remain compatible.
Multiple-choice routing and instruction action labels
What changed
- Multiple-choice steps now support any number of options, with every option routed to its own next step or nested sequence through
SwitchString. - Instruction buttons accept optional
settings.button_text, such asStartorContinue. - The instruction button default changed from
Got ittoNext.
Existing JSON DAG compatibility
- No change is required for existing DAGs that use
IfThenElse, non-branchingUserInputSelectString, orShowInstructions. Missingbutton_textautomatically displaysNext. - To retain a different instruction label, add
"button_text": "Your label"to thatShowInstructions.settingsobject. - To make an existing select branch, add a
SwitchStringnode whoseinputs.selectionpoints to theUserInputSelectString. Copy the ordered choices toSwitchString.settings.options, place the matching branch node IDs inSwitchString.substepsin the same order, and have the parent reference the switch node. - The option and branch arrays must remain the same length and order: option at index
0follows branch at index0, and so on.