Skip to content

HTTP endpoints

Every flow you build is automatically a live HTTP endpoint. This page covers the URL it lives at, how the method is chosen, how inputs are validated, how to protect it, and how to see inside a run.

The URL

A flow is reachable at:

/api/run/<project>/<file-slug>[/param1/param2/…]
  • <project> is your project's ID.
  • <file-slug> is the flow file's name, lowercased with every run of non-alphanumeric characters turned into a single hyphen. A file named Create Order answers at create-order.
  • If the flow lives inside a named folder, that folder contributes its own slug segment first: orders/create-order. Folders without a name are transparent — they add no segment.
  • Any extra segments after the slug are positional path parameters (see below).

If the project, the file, or a method-matching Start node can't be found, the endpoint returns 404 — the same answer whether the flow doesn't exist or simply doesn't accept that method, so you never leak which flows exist.

Stable webhook URL

The slug URL above is readable, but it changes if you rename the file. For a webhook you hand to a third party — Stripe, GitHub, a partner — you want a link that never moves. Open the webhook button (🪝) on the trigger bar above the canvas and Generate one:

/api/run/<project>/_wh/<token>
  • The token is opaque, unguessable, and independent of the file name — rename the flow freely and the URL keeps working.
  • Rotate issues a fresh token (invalidating the old URL); Remove deletes it.
  • It's an alternate address for the same flow — the Start node's method and auth still apply exactly as below. Pair it with signature auth for a Stripe/GitHub-style verified webhook.

NOTE

The token sits in the URL, so treat the whole URL like a secret — anyone who has it can address the flow (they still have to satisfy its auth). URLs can end up in access logs and browser history, so if one leaks, hit Rotate. For a webhook where the URL alone must never be enough, use signature auth so an unsigned request is rejected regardless.

The method comes from the Start node

A flow's Start node has a methodGET, POST, PUT, PATCH, or DELETE (default GET). An incoming request only runs the flow if its method matches a Start node's method.

A single flow file can hold more than one Start node, each with a different method, so one URL can behave differently for a GET versus a POST. The request is routed to the Start node whose method matches.

Input validation

The Start node declares what inputs it accepts, sorted into four buckets. Each is validated independently, and a caller that violates any of them gets a 400 with a list of exactly what failed.

BucketRead fromNotes
Query?key=value in the URL
BodyThe JSON request bodyOnly read for methods that allow a body (not GET/HEAD), and only when the body schema has fields. Requires Content-Type: application/json.
HeadersRequest headersLooked up case-insensitively.
PathPositional trailing URL segmentsFilled in the order the path params are declared.

Sending more trailing path segments than the flow declares is a 404, not a 400 — an undeclared path isn't a real endpoint.

Once every bucket validates, the values are merged into a single input object and handed to the flow's Start node. From there, downstream nodes read them by name.

NOTE

Body parsing is strict-ish but forgiving: a non-JSON content type is treated as an empty body, and malformed JSON is reported as body must be valid JSON. If the schema declares no body fields, the body is never read at all. Every request body is capped at 256 KB regardless of auth — an oversized normal body is rejected with a 400 (request body too large), and an oversized signed body with a 413 (it can't be verified without buffering it).

Authentication

The Start node can require callers to authenticate. Auth is checked before any input is parsed, so an unauthenticated request never reaches your flow.

TypeCaller sendsConfigured with
NoneNothingThe endpoint is public.
BearerAuthorization: Bearer <token>An expected token.
BasicAuthorization: Basic <base64 user:pass>Expected username + password.
API keyA header (default X-API-Key)Expected key + optional header name.
Webhook SignatureA signature over the raw body (HMAC, or Stripe's t=,v1=)A signing secret + a scheme — see below.

All comparisons are constant-time. A failed check returns 401, and for Bearer/Basic a WWW-Authenticate header is included.

Webhook signatures

Webhook Signature is how you securely receive events from Stripe, GitHub, Shopify, and the like. Those services sign each request with a shared secret; FlowRunner recomputes the signature and rejects anything that doesn't match — so a public endpoint can't be spoofed with fake events. Pick a scheme on the Start node's Authorization tab:

HMAC (GitHub / Shopify / generic)

The sender computes an HMAC of the raw request body and puts the digest in a header.

FieldWhat it isExample
Signing secretThe shared secret — store it in the vault and reference it.${secrets.WEBHOOK_SECRET}
Signature headerThe header the sender puts the signature in.X-Hub-Signature-256 (GitHub), X-Shopify-Hmac-Sha256 (Shopify)
Algorithmsha256 (default) or sha1.sha256
Digest encodinghex (default) or base64.hex (GitHub), base64 (Shopify)
Header prefixAn optional prefix to strip before comparing.sha256= (GitHub)

Stripe

Stripe signs "{timestamp}.{raw body}" and sends a structured Stripe-Signature: t=…,v1=… header. Choose the Stripe scheme and set:

FieldWhat it isExample
Signing secretThe webhook endpoint's signing secret from the Stripe dashboard.${secrets.STRIPE_WEBHOOK_SECRET}
Signature headerDefaults to Stripe-Signature.Stripe-Signature
Timestamp toleranceMax age of the signed timestamp, in seconds — rejects replays. 0 disables the check.300 (5 min)

FlowRunner recomputes HMAC-SHA256("{t}.{body}"), accepts the request if it matches any v1 in the header (Stripe sends several during a secret rotation), and rejects a timestamp outside the tolerance window. Once verified, the flow reads the event from the body as usual — e.g. ${start.type} and ${start.data} — so a checkout.session.completed event can fulfil the order.

In both schemes the signature is checked over the raw body exactly as received, before any parsing. Signed payloads are capped at 256 KB — a larger body is rejected with a 413 (it can't be verified without buffering it), which is ample for typical webhook events.

WARNING

A required body field is not enforced on GET. The designer disables the Body tab for GET, but a body schema authored while the method was POST survives switching back — and since a GET carries no body, the requirement is silently skipped rather than failing the request. If a required field isn't being enforced, check that it's declared under Params or Headers for a GET endpoint. Query, header, path and (on methods that carry one) body requirements are all enforced.

WARNING

Auth is fail-closed. If you configure an auth type but the expected secret is empty — for example an unresolved ${secrets.TOKEN} because the secret was never set — every request is rejected. This is deliberate: an empty expected value can never accidentally authorize a caller.

Put the actual credentials in the secrets vault and reference them as ${secrets.NAME} in the auth config. The value is resolved per-workspace at run time and never stored in the flow.

Seeing inside a run: ?_trace

Add ?_trace=1 to any request and the response includes a _trace array — one entry per node that ran, with its timing, whether it succeeded, and which output port it took. The same trace is also returned in an X-Flow-Trace header (URL-encoded).

bash
curl 'https://your-app/api/run/proj/orders/create-order?_trace=1' \
  -H 'Authorization: Bearer '"$TOKEN"

This is the fastest way to debug a live endpoint without opening the editor. The same trace powers run history and the in-editor testing panel.

NOTE

The trace is gated. The _trace array, the X-Flow-Trace header, and the X-Flow-Refs header are returned only to an authorized caller: when FLOW_TRACE=1 is set on the deploy, when the flow uses Basic or Bearer auth (so a ?_trace caller has proven credentials), or to a signed-in project member (which is how the editor's test panel gets them). An anonymous caller of a public (auth:none) flow gets no trace, even with ?_trace=1 on the URL.

The response

The flow's End node shapes what the caller receives: its configured status code (any valid 100599, default 200) and a response body composed from the flow's data. If the flow throws, the endpoint returns 500 — but the failing node's ID, the error message, and any _trace are added only for an authorized caller (the same gate as above); otherwise the body is just { ok: false, errors: ['flow execution failed'] }. A flow with no End node returns a default { ok: true, inputs } echo.

Every endpoint is CORS-enabled (OPTIONS preflight returns 204) and rate-limited per client IP to 60 requests per minute; exceeding that returns 429 with a Retry-After header.

See also

FlowRunner — the no-code platform for small businesses.