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 need | Why | Time |
|---|---|---|
| Paid LLMWeave plan | API keys, uploads, paid models, and webhooks are paid-plan features. | 1 min to check |
| API key | Use workflows:read and workflow_runs:create. Add webhooks:manage only for standing webhook subscriptions. | 2 min |
| Saved workflow id | The API runs workflows from The Loom. Use GET /workflows or copy the id from the workflow URL. | 1 to 2 min |
| HTTP client | curl is enough for the quickstart. The language snippets below use standard HTTP clients, not an SDK. | 1 min |
| Webhook endpoint, optional | Use 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_xxxxxxxxxxxxxxxxxxxxEach 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
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
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
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
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
Starts a workflow run and returns immediately with a run id. Requires workflow_runs:create. Body fields:
| Field | Type | Notes |
|---|---|---|
input_text | string | Required. The prompt/instruction. ≤ 50,000 characters. |
params | object | Optional. Values for the workflow’s declared parameters. |
attachment_ids | string[] | Optional. Ids from POST /attachments. Paid only. |
webhook_url | string | Optional. 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
Poll this until status is terminal. Statuses: pending → running →completed | 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
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
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
| Event | Fires when | Key payload fields |
|---|---|---|
workflow_run.completed | A run finishes successfully. | result.output, result.images/videos, cost_cents |
workflow_run.failed | A run fails (including human-review expiry). | result.error, result.failure_code |
workflow_run.awaiting_input | A 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
| Code | Meaning |
|---|---|
| 200 / 201 / 204 | Success. DELETE /webhooks/:id returns 204 with no body. |
| 400 | Invalid request or invalid run state. See error.code, for example INVALID_PARAM or INVALID_STATE. |
| 401 | Missing, malformed, revoked, or expired API key. |
| 402 | Payment required. Common codes: PAID_PLAN_REQUIRED, INSUFFICIENT_CREDITS. |
| 403 | Forbidden. Common codes: FORBIDDEN, NO_ALLOCATION, MODEL_ACCESS_DENIED, MODEL_DENIED_BY_TEAM, MAX_RUN_COST_EXCEEDED, PLAN_REQUIRED, LIMIT_REACHED. |
| 404 | Run/workflow not found, or not permitted for this key. |
| 409 | Idempotency-Key conflict or a prior request still in flight. |
| 429 | Rate limited. Run creation includes limit / remaining; uploads also have their own per-hour cap. |
| 500 / 503 | Server or billing service error. Retry with backoff; 503 commonly uses BILLING_UNAVAILABLE. |
Ready to start? Create an API key →