Skip to main content
On this page

API Reference

Run your LLMWeave workflows programmatically: trigger a run, feed it data, and read the result over a small REST API. Everything is JSON over HTTPS. The base URL is https://llmweave.com/api/v1.

Quickstart

1. Create an API key in Settings → API keys (paid plans). Copy it once; the full key is not shown again.

2. (Optional) Upload a file to feed into the run. 3. Trigger the run. 4. Poll until it completes.

Before you start

A first API run usually takes 10 to 15 minutes. Add another 10 to 20 minutes if you are setting up signed webhooks.

You needWhyTime
Paid LLMWeave planAPI keys, uploads, paid models, and webhooks are paid-plan features.1 min to check
API keyUse workflows:read and workflow_runs:create. Add webhooks:manage only for standing webhook subscriptions.2 min
Saved workflow idThe API runs workflows from The Loom. Use GET /workflows or copy the id from the workflow URL.1 to 2 min
HTTP clientcurl is enough for the quickstart. The language snippets below use standard HTTP clients, not an SDK.1 min
Webhook endpoint, optionalUse this when another system should receive the result without polling.10 to 20 min
# 1. (optional) upload a CSV. Returns an attachment id
curl -X POST https://llmweave.com/api/v1/attachments \
  -H "Authorization: Bearer $LLMWEAVE_API_KEY" \
  -F "file=@timesheets.csv"
# → { "data": { "id": "7f6f0b6e-4b6e-4e57-9d1b-2f5b7f7e8a11",
#                "kind": "csv", "extraction_status": "extracted" } }
# save that data.id if you want the workflow to read the file:
ATTACHMENT_ID=7f6f0b6e-4b6e-4e57-9d1b-2f5b7f7e8a11

# 2. trigger a workflow run (attachment_ids optional)
curl -X POST https://llmweave.com/api/v1/workflows/$WORKFLOW_ID/runs \
  -H "Authorization: Bearer $LLMWEAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{ \"input_text\": \"Summarize hours by project\",
        \"attachment_ids\": [\"$ATTACHMENT_ID\"] }"
# → 201 { "data": { "id": "<run_id>", "status": "pending", "links": { ... } } }

# 3. poll until status is "completed" (or "failed" / "awaiting_input")
curl https://llmweave.com/api/v1/workflow-runs/<run_id> \
  -H "Authorization: Bearer $LLMWEAVE_API_KEY"

In your language

The same trigger-then-poll flow, reading LLMWEAVE_API_KEY and WORKFLOW_ID from the environment. Anything that can make an HTTPS request works. These are starting points, not an SDK.

import os, time, uuid, requests

BASE = "https://llmweave.com/api/v1"
KEY = os.environ["LLMWEAVE_API_KEY"]
WORKFLOW_ID = os.environ["WORKFLOW_ID"]
auth = {"Authorization": f"Bearer {KEY}"}

# 1. Trigger a run
resp = requests.post(
    f"{BASE}/workflows/{WORKFLOW_ID}/runs",
    headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
    json={"input_text": "Summarize hours by project"},
)
resp.raise_for_status()
run_id = resp.json()["data"]["id"]

# 2. Poll until the run reaches a terminal state
while True:
    time.sleep(3)
    run = requests.get(f"{BASE}/workflow-runs/{run_id}", headers=auth).json()["data"]
    if run["status"] in ("completed", "failed", "awaiting_input"):
        break

print(run["status"], run.get("result"))

Authentication

Send your key as a bearer token on every request. Keys look like llmw_live_… and are issued in Settings → API keys. API access requires a paid plan; team members are covered by their team’s plan.

Authorization: Bearer llmw_live_xxxxxxxxxxxxxxxxxxxx

Each key carries scopes: workflows:read (list/read workflows and runs), workflow_runs:create (create runs, upload files), and the opt-in webhooks:manage(create/delete webhook subscriptions, not granted by default; request it explicitly when creating the key). A key may also be restricted to an allowlist of specific workflow ids; calls outside the allowlist return 404 (the workflow is treated as non-existent for that key). Keys are personal-scope in v1. Individual keys bill the key owner; team-member keys bill through that member’s team allocation or owner pool. Store keys as secrets and revoke any that leak.

Conventions

Errors

Errors use a JSON envelope. Most include a request_id; quote it in support when it is present:

{ "error": { "code": "INSUFFICIENT_CREDITS", "message": "Insufficient credits. Estimated cost: $0.12.", "request_id": "req_..." } }

Idempotency

For POST requests that create a run, send an Idempotency-Key header (any unique string ≤256 chars, e.g. a UUID). A retry with the same key returns the original response instead of starting a second run. Reusing a key with a different body, or while the first request is still in flight, returns 409. The key is bound to the exact method + path, so the same value can’t replay against a different endpoint.

Rate limits

Run creation is rate-limited per hour, scaled to your plan and shared with the web app. Over the limit returns 429with limit and remaining fields. Back off and retry.

Pagination

GET /workflow-runs takes ?limit (1 to 100, default 20) and ?cursor=, then returns newest-first rows plus next_cursor. Pass that cursor back to fetch the next page; null means you’ve reached the end. GET /workflows supports ?limit only, and webhooks list all matching subscriptions.

Endpoints

Who am I

GET/api/v1/me

Returns the account and key behind the token. This is handy for verifying a key works and which scopes it has.

{ "data": { "user_id": "...", "email": "you@example.com", "plan": "team",
           "key_id": "...", "key_name": "n8n integration",
           "key_scopes": ["workflows:read", "workflow_runs:create"] } }

List workflows

GET/api/v1/workflows

Lists workflows the key can run (its allowlist, if set). Requires workflows:read. Optional ?limit; no cursor is returned.

{ "data": [ { "id": "...", "name": "Timesheet Insights", "description": "...", "updated_at": "..." } ] }

Get a workflow

GET/api/v1/workflows/:id

Reads one workflow, including its declared run parameters (params): name, type, options, and defaults. Integrations can render input fields for Create a run without guessing. Requires workflows:read.

{ "data": { "id": "...", "name": "Timesheet Insights", "description": "...",
           "params": [
             { "name": "model", "type": "select", "label": "Model",
               "options": [
                 { "value": "anthropic/claude-sonnet-4.6", "label": "Claude Sonnet 4.6" },
                 { "value": "openai/gpt-5.4", "label": "GPT-5.4" }
               ],
               "default": "anthropic/claude-sonnet-4.6" },
             { "name": "include_summary", "type": "boolean", "label": "Include summary", "default": true },
             { "name": "max_rows", "type": "number", "label": "Max rows", "default": 500, "min": 1, "max": 5000 },
             { "name": "department", "type": "text", "label": "Department", "default": "", "maxLength": 80 }
           ] } }

Parameter type is one of select, number, text, or boolean. For select, each option is an object with value and label.

Upload a file

POST/api/v1/attachments

Multipart upload of a single file field. Returns an attachment id to pass as attachment_ids on a run. Requires workflow_runs:create + a paid plan. Attachment ids are UUIDs. Supported types include CSV, PDF, DOCX, JSON, and images (see the picker in-app for the full list and size cap). CSV/XLSX text extraction reads up to the first 5,000 rows (20MB max) and notes any truncation in the extracted text.

curl -X POST https://llmweave.com/api/v1/attachments \
  -H "Authorization: Bearer $LLMWEAVE_API_KEY" \
  -F "file=@data.csv"

→ 201 { "data": { "id": "7f6f0b6e-4b6e-4e57-9d1b-2f5b7f7e8a11", "original_filename": "data.csv",
                  "kind": "csv", "byte_size": 20480, "extraction_status": "extracted", "row_count": 500 } }

Upload a file, then feed it to a run, in your language:

import os, uuid, requests

BASE = "https://llmweave.com/api/v1"
auth = {"Authorization": f"Bearer {os.environ['LLMWEAVE_API_KEY']}"}

# 1. Upload a file (multipart). Returns an attachment id.
with open("data.csv", "rb") as f:
    up = requests.post(f"{BASE}/attachments", headers=auth, files={"file": f})
up.raise_for_status()
attachment_id = up.json()["data"]["id"]

# 2. Create a run that reads the file
run = requests.post(
    f"{BASE}/workflows/{os.environ['WORKFLOW_ID']}/runs",
    headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
    json={"input_text": "Summarize hours by project", "attachment_ids": [attachment_id]},
)
run.raise_for_status()
print(run.json()["data"]["id"])

Create a run

POST/api/v1/workflows/:id/runs

Starts a workflow run and returns immediately with a run id. Requires workflow_runs:create. Body fields:

FieldTypeNotes
input_textstringRequired. The prompt/instruction. ≤ 50,000 characters.
paramsobjectOptional. Values for the workflow’s declared parameters.
attachment_idsstring[]Optional. Ids from POST /attachments. Paid only.
webhook_urlstringOptional. HTTPS URL that receives this run’s terminal event. It is a one-shot webhook, no subscription needed. See Receiving webhooks.
→ 201 { "data": { "id": "<run_id>", "workflow_id": "<id>", "status": "pending",
                  "created_at": "...",
                  "webhook_secret": "whsec_...",   // only when webhook_url was sent; shown once
                  "links": { "self": "https://llmweave.com/api/v1/workflow-runs/<run_id>",
                             "app":  "https://llmweave.com/weave/workflows/<id>/runs/<run_id>" } } }

Common errors: 400 INVALID_PARAM, 400 INVALID_STATE, 402 PAID_PLAN_REQUIRED,402 INSUFFICIENT_CREDITS, 403 NO_ALLOCATION, 403 MODEL_ACCESS_DENIED,403 MODEL_DENIED_BY_TEAM, 403 MAX_RUN_COST_EXCEEDED, 403 PLAN_REQUIRED(video workflow on a non-Pro plan), 404 (workflow not found or not allowed for this key),429 RATE_LIMITED, and 503 BILLING_UNAVAILABLE.

Get a run

GET/api/v1/workflow-runs/:id

Poll this until status is terminal. Statuses: pendingrunningcompleted | failed | cancelled | awaiting_input (a human-review step is waiting). A poll every 2 to 5 seconds is plenty. Each response carries the final result, the cost, and a nodes array with the same node-by-node progress our own run viewer shows, so you can mirror it in your own UI.

If a run returns awaiting_input, v1 does not expose a resume endpoint yet. Open links.app and respond in LLMWeave before human_input_expires_at.

{ "data": {
    "id": "...", "workflow_id": "...", "status": "running",
    "current_layer_index": 1,
    "cost_cents": 12, "cost_breakdown": [ ... ],
    "human_input_request": null, "started_at": "...", "completed_at": null,
    "result": { ... },
    "nodes": [
      { "id": "input",   "type": "user_input", "label": "Your input", "status": "complete" },
      { "id": "research","type": "llm_call",   "label": "Research",
        "model_id": "anthropic/claude-opus-4-8", "status": "complete", "output": "..." },
      { "id": "writer",  "type": "llm_call",   "label": "Writer",
        "model_id": "openai/gpt-5.4", "status": "running" },
      { "id": "output",  "type": "output",     "label": "Result", "status": "pending" }
    ]
  } }

Each nodes[] entry has a status of pending / running /complete / error / awaiting, plus the node’s label, type, resolved model_id, and (when present) its output text, error reason, retry_count, and loop iterations. current_layer_index is the graph layer the engine is currently on.

Images & videos in the result

When a run produces media, result carries structured, signed, directly-downloadable URLs. No session or extra auth is needed, just GET the URL. Images use a 30-day signature; videos likewise (and support HTTP range requests for streaming).

"result": {
    "output": "...markdown...",
    "images": [ { "url": "https://llmweave.com/api/workflow-images/<run>/<node>-000-<nonce>.png?sig=...&exp=...",
                  "mime_type": "image/png" } ],
    "videos": [ { "url": "https://llmweave.com/api/workflow-videos/<run>/<node>-000-<nonce>.mp4?sig=...&exp=...",
                  "mime_type": "video/mp4", "duration": 0, "model_id": "google/veo-3.1-fast",
                  "thumbnail_url": "https://llmweave.com/api/workflow-videos/<run>/<thumb>.jpg?sig=...&exp=..." } ]
  }

Some providers return duration: 0 even for completed videos. Treat it as unknown unless your workflow stores an explicit duration parameter.

Download everything a finished run produced, in your language:

import os, requests

BASE = "https://llmweave.com/api/v1"
auth = {"Authorization": f"Bearer {os.environ['LLMWEAVE_API_KEY']}"}

run = requests.get(f"{BASE}/workflow-runs/{os.environ['RUN_ID']}", headers=auth).json()["data"]
result = run.get("result") or {}

# Images: result["images"] holds signed, directly-downloadable URLs.
for i, img in enumerate(result.get("images", [])):
    ext = img["mime_type"].split("/")[-1]
    data = requests.get(img["url"]).content  # signed URL, no auth header needed
    open(f"image_{i}.{ext}", "wb").write(data)

# Videos: result["videos"] holds signed URLs (valid 30 days).
for i, vid in enumerate(result.get("videos", [])):
    data = requests.get(vid["url"]).content
    open(f"video_{i}.mp4", "wb").write(data)

List runs

GET/api/v1/workflow-runs

Lists your runs, newest first. These are light rows only, with no result blob; fetch a single run for that. Requires workflows:read. Filters: ?status= (completed, failed,cancelled, awaiting_input, running, pending), ?workflow_id=, plus limit/cursor pagination.

GET https://llmweave.com/api/v1/workflow-runs?status=completed&limit=3

→ { "data": [ { "id": "...", "workflow_id": "...", "status": "completed",
                "input_text": "...", "cost_cents": 12,
                "started_at": "...", "completed_at": "...", "created_at": "..." } ],
    "next_cursor": "2026-07-05T12:00:00.000Z" }

Manage webhooks

POST/api/v1/webhooks
GET/api/v1/webhooks
GET/api/v1/webhooks/:id
DELETE/api/v1/webhooks/:id
POST/api/v1/webhooks/:id/rotate
GET/api/v1/webhooks/:id/deliveries

Subscription CRUD for push delivery. All webhook endpoints require the opt-in webhooks:manage scope. Full walkthrough in Receiving webhooks below.

Receiving webhooks

Instead of polling, LLMWeave can push run events to your server: subscribe an HTTPS URL and we POST a signed JSON payload when a run finishes, fails, or pauses for human review. Polling keeps working; webhooks are strictly additive. Use push when a run feeds the next step of an automation (Zapier, Make, n8n, your own queue); keep polling for one-off scripts.

Events

EventFires whenKey payload fields
workflow_run.completedA run finishes successfully.result.output, result.images/videos, cost_cents
workflow_run.failedA run fails (including human-review expiry).result.error, result.failure_code
workflow_run.awaiting_inputA human-review step pauses the run.human_input_request, human_input_expires_at, links.app

For workflow_run.awaiting_input, send the human reviewer to data.run.links.app. The run expires after 24 hours if nobody responds. There is no v1 approve/resume endpoint yet.

The payload’s data.run object is an allowlisted run summary. It includes the same core fields as a poll response, but not the poll-only progress fields such as nodes, current_layer_index, or param_defs. Fetch data.run.links.self when you need the full poll shape.

{
  "id": "evt_9c2f...",                     // stable per event, dedupe on this
  "type": "workflow_run.completed",
  "api_version": "2026-07-05",
  "created_at": "2026-07-05T12:00:00.000Z",
  "data": { "run": { "id": "…", "workflow_id": "…", "status": "completed",
                     "input_text": "…", "params": {},
                     "result": { "output": "…", "images": [], "videos": [] },
                     "cost_cents": 12, "cost_breakdown": [],
                     "started_at": "…", "completed_at": "…", "created_at": "…",
                     "links": { "self": "https://llmweave.com/api/v1/workflow-runs/…", "app": "https://llmweave.com/…" } } }
}

Subscribe

Create a subscription with a key that has the webhooks:manage scope. The response includes the signing secret. It is shown exactly once, so store it like a password.

curl -X POST https://llmweave.com/api/v1/webhooks \
  -H "Authorization: Bearer $LLMWEAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "target_url": "https://example.com/llmweave-webhook",
        "events": ["workflow_run.completed", "workflow_run.failed"],
        "workflow_id": null }'
# → 201 { "data": { "id": "…", "target_url": "…", "events": [...],
#                   "secret": "whsec_…" } }        ← shown once

# List (no secrets) / inspect / unsubscribe:
curl https://llmweave.com/api/v1/webhooks -H "Authorization: Bearer $LLMWEAVE_API_KEY"
curl -X DELETE https://llmweave.com/api/v1/webhooks/<id> -H "Authorization: Bearer $LLMWEAVE_API_KEY"

events defaults to completed + failed; add workflow_run.awaiting_input to get pinged when a human-review step needs a decision. Set workflow_id to scope the subscription to one workflow; omit it for all runs. The list endpoint returns active subscriptions by default. Add ?include_disabled=true to see subscriptions that were auto-disabled after repeated failures, including disabled_reason and consecutive_failures.

Verify signatures

Every delivery is signed so you can prove it came from LLMWeave. Headers on each POST: llmweave-event (type), llmweave-event-id (stable across retries, dedupe on it), llmweave-delivery-id (unique per attempt), llmweave-timestamp (unix seconds), and llmweave-signature: v1=<hex> where the signature is HMAC-SHA256(secret, timestamp + "." + raw_body). Verify like this:

// Node 20+ HTTP handler (Express shown; any framework works).
// IMPORTANT: verify against the RAW body bytes. Do not JSON.parse first.
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.LLMWEAVE_WEBHOOK_SECRET!; // whsec_..., shown once at subscribe

app.post("/llmweave-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.header("llmweave-timestamp") ?? "";
  const signature = (req.header("llmweave-signature") ?? "").replace(/^v1=/, "");

  // 1. Reject stale timestamps (replay protection)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(400).end();

  // 2. Recompute the HMAC over "<timestamp>.<raw body>"
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.${req.body.toString("utf8")}`)
    .digest("hex");

  // 3. Constant-time compare
  const valid =
    /^[0-9a-f]{64}$/i.test(signature) &&
    crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString("utf8"));
  // Dedupe on event.id (llmweave-event-id header). Retries reuse the same id.
  console.log(event.type, event.data.run.id, event.data.run.status);

  res.status(200).end(); // respond 2xx fast; do slow work async
});

app.listen(3000);

Respond with a 2xx within 10 seconds. Do slow work asynchronously. Redirects are not followed; a 3xx counts as a failure.

Retries & auto-disable

Delivery is at-least-once with no ordering guarantee. A failed delivery retries with backoff: roughly +1m, +5m, +30m, +2h, +6h. It is abandoned after 6 attempts. Sends are dispatched by a sweep that runs every 5 minutes, so expect up to ~5 minutes of latency on top of the backoff. After 20 consecutive failures the subscription is disabled (disabled_reason: "too_many_failures"); re-create it once your endpoint is healthy. Use GET /webhooks?include_disabled=true to find disabled subscriptions because the default list shows active ones only. Inspect delivery history at GET /webhooks/:id/deliveries (status, attempts, last error, paginated).

One-shot webhooks (run & wait)

Don’t want a standing subscription? Pass webhook_url when creating a run and that single run’s events (completed or failed, plus awaiting_input if it pauses) are POSTed there, signed with the webhook_secret returned once in the create response. Works with any key that can create runs, no webhooks:manage scope needed.

Rotate a secret

curl -X POST https://llmweave.com/api/v1/webhooks/<id>/rotate -H "Authorization: Bearer $LLMWEAVE_API_KEY"
# → { "data": { "secret": "whsec_NEW…", "previous_secret_expires_at": "…" } }

We sign with the new secret immediately; accept either secret on your side for the 24-hour grace window, then drop the old one.

Good to know

Media URLs expire. result.images / result.videos carry 30-day signed URLs. If you store payloads and fetch media later, re-fetch the run via links.self for fresh URLs instead of trusting stored ones.
Local testing: point target_url at a tunnel (ngrok, cloudflared) or an inspector like webhook.site, run a workflow, and watch the delivery arrive. The deliveries log shows every attempt and its response code.

No-code & automation tools

Any tool that can make an authenticated HTTP request works: n8n (HTTP Request node), Zapier (Webhooks → Custom Request), and Make (HTTP module). Configure them the same way: method POST, URL https://llmweave.com/api/v1/workflows/<id>/runs, an Authorization: Bearer … header, JSON body. To get results back, either poll https://llmweave.com/api/v1/workflow-runs/<id> until status is completed, or skip polling entirely: give the tool’s catch-hook URL as the run’s webhook_url (or a standing webhook subscription) and let LLMWeave push the finished run to it.

Status codes

CodeMeaning
200 / 201 / 204Success. DELETE /webhooks/:id returns 204 with no body.
400Invalid request or invalid run state. See error.code, for example INVALID_PARAM or INVALID_STATE.
401Missing, malformed, revoked, or expired API key.
402Payment required. Common codes: PAID_PLAN_REQUIRED, INSUFFICIENT_CREDITS.
403Forbidden. Common codes: FORBIDDEN, NO_ALLOCATION, MODEL_ACCESS_DENIED, MODEL_DENIED_BY_TEAM, MAX_RUN_COST_EXCEEDED, PLAN_REQUIRED, LIMIT_REACHED.
404Run/workflow not found, or not permitted for this key.
409Idempotency-Key conflict or a prior request still in flight.
429Rate limited. Run creation includes limit / remaining; uploads also have their own per-hour cap.
500 / 503Server or billing service error. Retry with backoff; 503 commonly uses BILLING_UNAVAILABLE.

Ready to start? Create an API key →