Quickstart — your first generation, end to end
This is the whole lifecycle in one sitting: authenticate → discover → upload a file → submit → poll → read the output. Every step here is real and runnable; it mirrors a flow we execute against a live Madoo instance, so if you follow along with your own credentials and a workflow ID, it works.
We show each step twice — once with curl (to see the raw HTTP) and once with
JavaScript/TypeScript (fetch, runnable in Node 18+ or Deno). Pick whichever you prefer.
Before you start
- You have a
client_idandclient_secret(01-authentication.md).- You know your environment’s base URL (README §5).
- You have the ID of a published workflow to run (a
wf_…string). You can list available workflows withGET /api/v1/workflows— see 03-workflows.md.
Set up the shared variables:
export BASE_URL="https://testing-api.madoo.ai" # your environmentexport CLIENT_ID="…"export CLIENT_SECRET="…"export WF="wf_53fb2f6576fa4bc9a6d3c91a7e84de47" # the workflow you want to runStep 1 — Get an access token
Section titled “Step 1 — Get an access token”TOKEN=$(curl -s -X POST "$BASE_URL/api/v1/auth/token" \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=client_credentials" | jq -r .access_token)
echo "${TOKEN:0:12}…" # sanity check: should print a token prefix, not 'null'const token = await getToken(BASE_URL, CLIENT_ID, CLIENT_SECRET); // helper from 01-authentication §4Step 2 — Discover the workflow’s interface
Section titled “Step 2 — Discover the workflow’s interface”Always read the interface before sending inputs. It tells you the exact input keys, their types, and which are required.
curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/v1/workflows/$WF" | jq '.interface'Example response (trimmed):
{ "inputs": [ { "name": "brand_url", "type": "text", "required": true }, { "name": "audience", "type": "text", "required": true }, { "name": "tone", "type": "text", "required": true }, { "name": "block_title", "type": "text", "required": true }, { "name": "block_brief", "type": "text", "required": true }, { "name": "product_image_0", "type": "image", "required": true }, { "name": "product_image_1", "type": "image", "required": false }, { "name": "product_image_2", "type": "image", "required": false } ], "outputs": [ { "name": "hero_image", "type": "image" }, { "name": "copy_json", "type": "text" } ]}So this workflow needs five text inputs, at least one product image (product_image_0), and
optionally more. It produces an image (hero_image) and a text blob (copy_json).
Step 3 — Provide your file inputs
Section titled “Step 3 — Provide your file inputs”For a durable or private file, upload it first, get back a storage path, and reference that path in your inputs. This is the recommended choice when the same asset will be reused.
For a one-run image, video, audio, document, or model3d input, you may instead pass a
public HTTPS URL as a JSON-encoded value. Madoo downloads it through its security policy, verifies
the declared type and stores an execution-temporary copy before any downstream node runs. URLs that
require credentials, cookies or custom headers are not supported.
Structured data inputs (CSV, JSON, XLSX) use an uploaded/imported asset_path, not a public URL
in value. You can profile them first with POST /api/v1/structured-data/inspect. If row fan-out
reaches paid nodes, pass the same asset_path to the workflow estimate endpoint: Madoo can count the
selected rows before the run and return the full projected reservation instead of a deferred subtotal.
ASSET_PATH=$(curl -s -H "Authorization: Bearer $TOKEN" \ -F "file=@./product-hero.jpg" \ "$BASE_URL/api/v1/assets" | jq -r .path)
echo "$ASSET_PATH"const form = new FormData();form.append("file", new File([await Deno.readFile("./product-hero.jpg")], "product-hero.jpg", { type: "image/jpeg" }));
const uploadRes = await fetch(`${BASE_URL}/api/v1/assets`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form,});const { path: assetPath } = await uploadRes.json();More on uploads (formats, size limits, listing/deleting): 05-assets.md.
Step 4 — Submit the execution
Section titled “Step 4 — Submit the execution”POST your inputs. Each input is keyed by its interface name and is either a value (for
text/number/boolean/JSON inputs, or a public HTTPS media URL) or an asset_path (for an already
stored file).
⚠️ The one rule everyone trips on:
Section titled “⚠️ The one rule everyone trips on: value is a JSON-encoded string”valueis a JSON-encoded stringThe
valueof a text/number/boolean input is itself a JSON document encoded as a string. So:
- the text
Spring in Bloom→"value": "\"Spring in Bloom\""- the number
42→"value": "42"- the boolean true →
"value": "true"- the media URL
https://media.example/clip.mp4→"value": "\"https://media.example/clip.mp4\""In code you produce this by running
JSON.stringify()on your value and putting the result in thevaluefield. This is explained in full in 04-executions.md; for now, just notice the escaped quotes around text below.
# Build the body in a file to keep the quoting sane.cat > body.json <<JSON{ "workflow": "$WF", "inputs": { "brand_url": { "value": "\"https://www.maisonlumiere.com\"" }, "audience": { "value": "\"Design-conscious women aged 28-45\"" }, "tone": { "value": "\"Warm, sensorial, premium, concise.\"" }, "block_title": { "value": "\"Spring in Bloom\"" }, "block_brief": { "value": "\"Hero block for the spring newsletter.\"" }, "product_image_0": { "asset_path": "$ASSET_PATH" } }}JSON
RUN_ID=$(curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ --data @body.json "$BASE_URL/api/v1/executions" | jq -r .id)
echo "$RUN_ID" # e.g. run_b4c8d3e2…// In code, JSON.stringify() produces the inner JSON string for you.const text = (v: unknown) => ({ value: JSON.stringify(v) });
const createRes = await fetch(`${BASE_URL}/api/v1/executions`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ workflow: WF, inputs: { brand_url: text("https://www.maisonlumiere.com"), audience: text("Design-conscious women aged 28-45"), tone: text("Warm, sensorial, premium, concise."), block_title: text("Spring in Bloom"), block_brief: text("Hero block for the spring newsletter."), product_image_0: { asset_path: assetPath }, // product_image_1..N are optional → simply omit them }, }),});const { id: runId } = await createRes.json();You get HTTP 202 Accepted back immediately with the execution object — the generation runs in
the background. Optional inputs (here product_image_1, product_image_2) can simply be omitted.
Step 5 — Poll until it finishes
Section titled “Step 5 — Poll until it finishes”There are no inbound webhooks in v1, so you poll the execution until it reaches a terminal
status: completed, partial_success, failed, or cancelled. A poll interval of 3–5
seconds is reasonable.
while :; do RESP=$(curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/v1/executions/$RUN_ID") STATUS=$(echo "$RESP" | jq -r .status) echo "status=$STATUS progress=$(echo "$RESP" | jq -r .progress)" case "$STATUS" in completed|partial_success|failed|cancelled) break ;; esac sleep 4doneasync function waitForExecution(baseUrl: string, token: string, runId: string, timeoutMs = 120_000) { const terminal = ["completed", "partial_success", "failed", "cancelled"]; const start = Date.now(); while (Date.now() - start < timeoutMs) { const res = await fetch(`${baseUrl}/api/v1/executions/${runId}`, { headers: { Authorization: `Bearer ${token}` }, }); const exec = await res.json(); if (terminal.includes(exec.status)) return exec; await new Promise((r) => setTimeout(r, 4000)); } throw new Error(`Timed out waiting for ${runId}`);}const execution = await waitForExecution(BASE_URL, token, runId);A typical single-image run completes in ~15–20 seconds. The progress (0–100) and
progress_message fields let you show a progress bar while you wait.
Step 6 — Read the outputs
Section titled “Step 6 — Read the outputs”Use GET /api/v1/executions/{id}/result. It returns an indexed by_key map: look up each
output by its key and read .value (text/JSON — already parsed) or .url (binary). No find(), no
JSON.parse, no type === switch.
curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/v1/executions/$RUN_ID/result" | jq '.result.by_key'{ "hero_image": { "key": "hero_image", "state": "present", "url": "https://cdn-testing.madoo.ai/…/hero.png", // binary → fetch the url "value": null }, "copy_json": { "key": "copy_json", "state": "present", "value": { "headline": "Spring in Bloom", "body": "…", "cta": "Shop now" } // ← already an object }}const result = await fetch(`${BASE_URL}/api/v1/executions/${runId}/result`, { headers: { Authorization: `Bearer ${token}` },}).then((r) => r.json());
const heroImageUrl = result.result.by_key.hero_image.url; // "https://…/hero.png"const copy = result.result.by_key.copy_json.value; // { headline, body, cta } — use directlyconsole.log("Hero image URL:", heroImageUrl);console.log("Copy:", copy);Why this beats reading the raw output list yourself:
- Indexed by key —
result.by_key.hero_image, no scanning and no matching on the GUID-suffixedname. - Text/JSON is pre-parsed — including JSON that was produced by a
textnode and stored on a blob (the classic “I readvalueand gotnull” bug).valueis an object when the content is JSON, a string for prose. Branch onparse.formatif you need to be sure. - Skips are explicit — an optional branch that didn’t run comes back as
state: "absent"(not a silent empty field).
You can call
/resultwhile the run is still going too — it returns200withis_terminal: falseand each expected key instate: "not_ready". The raw, ungroupedGET /api/v1/executions/{id}/outputsendpoint still exists for advanced/debug use; full details in 04-executions.md §6.
📦 Or skip the boilerplate: the official TypeScript helper
examples/madoo-runtime.tsdoes submit → poll → read in one call (runWorkflowAndGetResult), returningresult.by_keydirectly.
You did it 🎉
Section titled “You did it 🎉”You authenticated, discovered a contract, uploaded an asset, ran a workflow, and read structured outputs. That is the entire core loop — everything else in this guide is depth on each step:
- Reading interfaces deeply (custom interfaces, defaults, versioning) → 03-workflows.md
- Executions in detail (input encoding rules, statuses, cancel, ZIP export) → 04-executions.md
- Assets (formats, limits) → 05-assets.md
- Running many at once → 06-advanced-batch.md
- Embedding a widget → 07-advanced-embed.md
- Status codes, errors, limits → 08-reference.md