REST API reference
Start runs, read workflows and results, and manage outbound webhooks from your own systems.
Overview
The Fluxo Flow REST API lives at https://flow.fluxosuite.com/api/v1. It speaks JSON over HTTPS. With it you can list and read workflows, start a run with an input payload (and optionally wait for the result), list runs and read each step's status and output, cancel queued or paused runs, and manage outbound webhook endpoints. Building and editing workflows still happens in the app.
- Machine-readable spec: OpenAPI 3.1 at https://flow.fluxosuite.com/api/v1/openapi.json (public, no key needed) — import it into Postman, Insomnia or a code generator.
- Every response carries an X-Request-Id header. Quote it when you contact support.
- Ids are UUIDs. Times are ISO 8601 in UTC. Fields are camelCase. New fields can appear at any time — ignore ones you don't know.
Authentication and scopes
Workspace owners and admins create API keys in Settings → API & webhooks. A key looks like fxk_live_<key id>_<secret>; it is shown once, and we keep only a salted, peppered hash. The fxk_live_<key id> part is the key's public prefix, shown in the key list so you can tell keys apart. Send the key on every request as Authorization: Bearer fxk_live_… — from your server only. Requests from web browsers are refused (no CORS), and your Fluxo Flow sign-in does not work on /api/v1, just as API keys don't work anywhere else in the app.
| Scope | Allows |
|---|---|
| workflows:read | Read workflows. List workflows and read their settings (never secrets). |
| runs:read | Read runs. List runs and read their status, step results and output. |
| runs:write | Start and cancel runs. Start a workflow run with an input payload, and cancel queued or paused runs. |
| webhooks:read | Read webhooks. List outbound webhook endpoints and their delivery log. |
| webhooks:write | Manage webhooks. Add and remove outbound webhook endpoints. |
- New keys get workflows:read and runs:read unless you tick more. runs:write includes runs:read, and webhooks:write includes webhooks:read.
- Optional expiry: 30 days, 90 days, 1 year or a date you pick. Expired and revoked keys stop working on their next request (401).
- The key list shows who created each key, when it was last used and from which country.
- Keys created before the API launched (fluxo_…) keep working with read-only scopes. Create a new key for write access.
- Every key belongs to one workspace. Objects in other workspaces always answer 404, never 403, so their existence isn't revealed.
- If the workspace is suspended, or its owner's account is blocked, its keys get 403.
- Key creation, revocation, rejected requests and (at most hourly) key use are recorded in the workspace activity log; runs started by a key show the key's name.
Quick start
curl https://flow.fluxosuite.com/api/v1/me \ -H "Authorization: Bearer $FLUXO_API_KEY"
curl -X POST https://flow.fluxosuite.com/api/v1/workflows/WORKFLOW_ID/runs \
-H "Authorization: Bearer $FLUXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"input": {"customerEmail": "ana@example.com", "amount": 1999}, "wait": true}'const base = "https://flow.fluxosuite.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.FLUXO_API_KEY}`, "Content-Type": "application/json" };
let run = await fetch(`${base}/workflows/${workflowId}/runs`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ input: { customerEmail: "ana@example.com" } }),
}).then((r) => r.json());
while (["queued", "running"].includes(run.status)) {
await new Promise((r) => setTimeout(r, 2000));
run = await fetch(`${base}/runs/${run.id}`, { headers }).then((r) => r.json());
}
console.log(run.status, run.output);import os, uuid, requests
base = "https://flow.fluxosuite.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['FLUXO_API_KEY']}"}
run = requests.post(
f"{base}/workflows/{workflow_id}/runs",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={"input": {"customerEmail": "ana@example.com"}, "wait": True},
timeout=40,
).json()
print(run["status"], run.get("output"))Runs: lifecycle, waiting and cancelling
POST /workflows/{id}/runs queues a run on the same queue and runner as webhook runs (its source is "api"). The input object becomes the trigger's payload — the same shape a webhook body would have; GET /workflows/{id} lists the input fields the workflow reads. The response is 202 with the run's id and status "queued"; the run usually starts within seconds.
| Status | Meaning |
|---|---|
| queued | Accepted, waiting for the runner. |
| running | Executing now (or resuming after a review decision). |
| paused | Waiting at a Human review step (the run detail's review object says which step; it continues after someone decides in the Review Queue) or at a Wait step (the wait object gives the step and resumeAt; it continues on its own). |
| succeeded | Finished. output holds the last successful step's output. |
| failed | A step failed, the run hit the server time limit, or it couldn't start. error.message says why. |
| cancelled | Cancelled through the API before it ran, or while paused for review. |
- wait: true holds the request open for up to 25 seconds. If the run finishes in time you get 200 with the full run (steps included); otherwise 202 and the run keeps going — poll GET /runs/{id} or subscribe to run.* webhooks.
- metadata: up to 20 key/value tags (for example your order id) stored on the run and returned with it. They are not passed to the workflow.
- POST /runs/{id}/cancel cancels queued runs, runs paused for review (the review item is withdrawn) and runs waiting at a Wait step (they never resume). Steps that are executing can't be interrupted yet, so a running run answers 409.
- Run details include each step's status, duration, output, error, and token usage and cost when recorded. Outputs pass through the same secret scrubber as the workspace export and are capped at 16 KB per step.
- API runs count toward your plan like any other run, and each plan limits how many runs can be queued or running at once (429 concurrency_limit_exceeded above it).
Rate limits
| Plan | Per key | Per workspace (all keys) | Runs queued or running at once |
|---|---|---|---|
| Free | 60/min | 120/min | 2 |
| Basic | 120/min | 240/min | 5 |
| Pro | 600/min | 1,200/min | 20 |
| Business | 900/min | 1,800/min | 35 |
| Elite | 1,200/min | 2,400/min | 50 |
| Enterprise | 3,000/min (custom) | 6,000/min | 100 |
- Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets).
- Above a limit you get 429 with a Retry-After header (seconds). Back off and retry after it.
- Too many requests with invalid keys from one IP address are blocked for a while (429 too_many_auth_failures), even if a later key is valid.
- Enterprise limits are set per contract.
Pagination and filters
Lists return { object: "list", data, hasMore, nextCursor }, newest first. Pass limit (1–100, default 25) and, for the next page, cursor=<nextCursor>. Cursors are opaque; don't build them yourself. GET /runs filters by workflowId, status and since (an ISO 8601 date-time with an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+02:00; a time without a zone is rejected). GET /workflows filters by q (name contains).
curl "https://flow.fluxosuite.com/api/v1/runs?workflowId=WORKFLOW_ID&status=failed&since=2026-10-01T00:00:00Z&limit=50" \ -H "Authorization: Bearer $FLUXO_API_KEY"
Idempotency
Send an Idempotency-Key header (any unique string up to 255 characters; a UUID is ideal) when starting a run. For 24 hours, a retry with the same key and the same body returns the original run instead of starting another one (with the header Idempotent-Replayed: true). Reusing a key with a different body returns 409 idempotency_conflict; retrying while the first request is still being processed returns 409 idempotency_in_progress — wait a second and retry. Keys are shared across all API keys of the workspace.
Errors
Errors use one shape: { "error": { "code", "message", "requestId", "docs" } } (validation errors add details.fields). Branch on code, show message to people, and log requestId. 401 means the key is missing, wrong, expired or revoked; 403 means it lacks the scope, the workspace is suspended, or the request came from a browser; 404 covers objects that don't exist or belong to another workspace; 409 is an idempotency conflict or a run that can't be cancelled; 413 is a body over 1 MB; 415 is a body that isn't application/json; 422 is a field that fails validation; 429 is a rate or concurrency limit; 5xx is our side — retry with backoff.
| Code | HTTP | Meaning and what to do |
|---|---|---|
| authentication_required | 401 | Send your API key in the Authorization header: `Authorization: Bearer fxk_live_…`. |
| invalid_api_key | 401 | This API key isn't valid. Check you copied the whole key, or create a new one in Settings → API & webhooks. |
| api_key_expired | 401 | This API key has expired. Create a new key in Settings → API & webhooks. |
| api_key_revoked | 401 | This API key was revoked. Create a new key in Settings → API & webhooks. |
| insufficient_scope | 403 | This API key doesn't have the scope this endpoint needs. |
| workspace_suspended | 403 | This workspace is suspended, so its API keys can't be used. Contact support. |
| account_blocked | 403 | The workspace owner's account isn't eligible to use the API. Contact support. |
| browser_requests_not_allowed | 403 | API keys can't be used from a web browser. Call the API from your server instead. |
| not_found | 404 | Not found. |
| method_not_allowed | 405 | This endpoint doesn't support that HTTP method. |
| idempotency_conflict | 409 | This Idempotency-Key was already used with a different request body. Use a new key for a different request. |
| idempotency_in_progress | 409 | A request with this Idempotency-Key is still being processed. Retry in a moment. |
| run_not_cancellable | 409 | This run can't be cancelled in its current state. |
| conflict | 409 | The request conflicts with the current state of the resource. |
| payload_too_large | 413 | The request body is larger than 1 MB. |
| unsupported_media_type | 415 | Send the request body as JSON with `Content-Type: application/json`. |
| invalid_json | 400 | The request body isn't valid JSON. |
| validation_failed | 422 | Some fields are invalid. |
| rate_limited | 429 | Too many requests. Slow down and retry after the time in the Retry-After header. |
| upgrade_required_live_runs | 402 | Upgrade to run workflows live. The Free plan can build workflows and test them with Dry Run in the app; runs through the API need a paid plan. |
| concurrency_limit_exceeded | 429 | Too many runs are queued or running for this workspace. Wait for some to finish, then retry. |
| too_many_auth_failures | 429 | Too many requests with invalid API keys from this address. Wait before trying again. |
| internal_error | 500 | Something went wrong on our side. Retry, and contact support with the request id if it keeps happening. |
| service_unavailable | 503 | The API isn't available right now. Retry in a moment. |
Outbound webhooks
Instead of polling, add an endpoint in Settings → API & webhooks (or POST /webhooks) and we'll POST a signed JSON event to it. Endpoints must be https and publicly reachable; private, loopback and link-local addresses are refused when you add them and again before every delivery, and redirects are not followed.
| Event | When |
|---|---|
| run.succeeded | A workflow run finished successfully. |
| run.failed | A workflow run failed or timed out. |
| run.paused | A run reached a Human review step and is waiting for a decision. |
| review.decided | Someone approved or rejected a review step (or it timed out). |
| workflow.updated | A workflow's steps or settings were saved. |
| credential.rotated | A credential in the vault was saved. |
| credential.revoked | A credential was removed from the vault. |
| plan.changed | The workspace's subscription plan changed. |
{
"id": "evt_3f1c9a2b7d4e5f60718293ab",
"type": "run.failed",
"event": "run.failed",
"api_version": "v1",
"created_at": "2026-10-02T09:30:59.800Z",
"workspace_id": "0b6e2c1a-9d3f-4e7b-8a2c-1f0e9d8c7b6a",
"attempt": 1,
"data": {
"run": {
"id": "8f14e45f-ceea-4e6b-9a1c-5f0b4f6c2a10",
"workflowId": "3b9d6c1e-2f4a-4c55-8e0a-6a7b8c9d0e1f",
"workflowName": "Invoice approval",
"status": "failed",
"source": "api",
"durationMs": 1840,
"costUsd": 0.0012,
"error": "Slack: channel_not_found"
}
}
}- Headers: Fluxo-Signature, Fluxo-Event, Fluxo-Event-Id and Fluxo-Delivery. X-Fluxo-Signature and X-Fluxo-Event carry the same values for receivers built against the earlier names; prefer the new ones.
- Respond with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 15 h (7 attempts over about 24 hours), then marked as given up.
- Retries reuse the event id — de-duplicate on it. Events can arrive out of order; use created_at and the run status.
- After 20 failed deliveries in a row the endpoint is turned off automatically; owners and admins get an email and a notification, and the activity log records it. Turn it back on from the endpoint's Edit dialog once it's fixed.
- Send test event delivers a signed webhook.test event right away and shows the response. The delivery log (and GET /webhooks/{id}/deliveries) shows every attempt for 30 days: status code, duration, attempts and the first 1 KB of the response with secrets removed.
- Rotate secret issues a new signing secret. For 24 hours deliveries carry two v1 signatures (new and old) so you can switch your receiver without missing events.
Verifying webhook signatures
Fluxo-Signature looks like t=1790000000,v1=5257a8…. Compute HMAC-SHA256 with your endpoint's signing secret over the string "<t>.<raw request body>", hex-encode it, and compare it in constant time with each v1 value (there are two during a rotation). Reject timestamps more than 5 minutes old to block replays. Always use the raw body bytes — re-serialized JSON won't match.
import crypto from "node:crypto";
// Express: app.post("/fluxo", express.raw({ type: "application/json" }), handler)
export function verifyFluxoSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = header.split(",").map((p) => p.split("="));
const t = parts.find(([k]) => k === "t")?.[1];
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false; // replay guard
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some((s) =>
s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))
);
}
// In your handler (rawBody is the exact bytes received, as a string):
// if (!verifyFluxoSignature(rawBody, req.get("Fluxo-Signature"), process.env.FLUXO_WEBHOOK_SECRET)) return res.sendStatus(400);
// const event = JSON.parse(rawBody); // de-duplicate on event.id
import hashlib, hmac, time
def verify_fluxo_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
items = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in items if k == "t"), None)
sigs = [v for k, v in items if k == "v1"]
if not t or not sigs:
return False
if abs(time.time() - int(t)) > tolerance: # replay guard
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)
# Flask:
# if not verify_fluxo_signature(request.get_data(), request.headers["Fluxo-Signature"], os.environ["FLUXO_WEBHOOK_SECRET"]):
# abort(400)
Versioning and deprecation
- The version is in the path: /api/v1. Breaking changes only ever ship as a new version (/api/v2).
- Within v1 we may add endpoints, optional parameters, response fields, enum values and webhook events without notice. Build clients that ignore unknown fields.
- If a v1 endpoint is deprecated, we announce it in the changelog and keep it working for at least 12 months. Its responses carry Deprecation and Sunset headers in the meantime.
Deprecation and Sunset headers
- Deprecation: @<unix seconds> (RFC 9745) — the moment the endpoint (or the whole API version) was deprecated.
- Sunset: <HTTP date> (RFC 8594) — when it stops working. Absent until a date is set.
- Link: <https://flow.fluxosuite.com/docs/api#deprecations>; rel="deprecation" — where to read about the change (for a whole version: #version-<version>).
- The headers are added to every response of an affected endpoint, successful or not. Log them in your client so you notice before the sunset date; the changelog announces each deprecation too.
Building a third-party app (OAuth)
- Register your app in the developer portal (/developer-portal): name, redirect URIs and the scopes it needs. You get a client id and a client secret (shown once). Apps are reviewed before other workspaces can install them.
- Send the user to /oauth/authorize?response_type=code&client_id=…&redirect_uri=…&scope=…&state=…&code_challenge=…&code_challenge_method=S256 (PKCE with S256 is required). An owner or admin picks the workspace and approves.
- Exchange the code: POST /api/oauth/token with grant_type=authorization_code, code, redirect_uri and code_verifier, authenticating with your client id + secret (HTTP Basic or form fields). The access token works like an API key on /api/v1 with the approved scopes. It expires after an hour (expires_in: 3600); the response also carries a refresh_token.
- Renew: POST /api/oauth/token with grant_type=refresh_token and refresh_token (optionally a narrower scope). Each refresh token works once: store the new refresh_token from every response. Presenting a spent refresh token again revokes the connection (we treat it as stolen). Refresh tokens last 90 days from their last use.
- Disconnect: POST /api/oauth/revoke with token=<access token or refresh token>. Workspace admins can also revoke the app in Settings → API & webhooks; both show up in the workspace activity log.
Security checklist
- Keep keys in a secret manager or server environment variables — never in browser code, mobile apps or source control.
- Give each system its own key with only the scopes it needs, and set an expiry.
- Revoke a key the moment you suspect it leaked (Settings → API & webhooks, or Danger Zone → Revoke all API keys).
- Verify every webhook signature and timestamp before trusting the body.
- Run outputs can contain your business data; treat API responses like the data in the app.
Other ways to integrate
- Inbound without an API key: give a workflow a Webhook trigger, activate it, and POST JSON to its URL with the secret header (see Webhooks & triggers).
- Outbound from inside a workflow: add an HTTP Request, Webhook Call or Publish Output step to send data anywhere at any point.
Endpoints
Paths are relative to https://flow.fluxosuite.com/api/v1. Each endpoint lists the scope its key needs.
Account
Workflows
Runs
Webhooks
Related guides
Last updated . New to Fluxo Flow? See what it does and how pricing works.