Skip to main content
An API Call node runs an HTTP request from FirstFlow’s backend while a flow executes, then drops the response into the run’s step bag so later nodes a decision branch, a message, a guide’s widget composition can read live data like account status, plan limits, or order state. The request, the credentials, and the response never touch the browser. The node type is api_call. It belongs to the same family of server-side action nodes as Slack messages and webhooks, but unlike those it produces an output you consume downstream rather than a fire-and-forget side effect.

How it works

A flow run carries a step bag a map keyed by node id that accumulates each node’s output as the run advances. When the engine reaches an api_call node, it first interpolates the node’s config against everything the run knows so far, then performs the fetch, then writes the result under the node’s id. Downstream nodes interpolate their own config against that same bag, which is how a later node “sees” what the API returned. The sequence inside one node execution: Everything here happens in ApiCallExecutor on the backend. The browser SDK is never involved it only ever receives the finished widget that downstream composition produces.

Config is interpolated before the fetch

Config values may contain {{...}} tokens, and the engine resolves them against the run before the request is built. Two roots are available:
  • {{trigger.<path>}} the payload that started the run. For a message-driven guide that includes {{trigger.content}} (also aliased as {{content}}) and {{userId}}.
  • {{steps.<nodeId>.<path>}} the output of any node that already ran, including earlier API Call nodes.
A token that is the entire string resolves to the raw typed value, so objects, numbers, and booleans survive untouched. A token embedded in surrounding text is stringified. That distinction matters: "{{steps.lookup.body.plan}}" yields the string "pro", while "id={{userId}}" yields "id=usr_123".

The response shape

On a successful (2xx) call the node writes this object to the step bag:
The response is parsed as JSON. If the body is not valid JSON, body falls back to { text: "<raw response>" } so downstream interpolation never breaks. Read any of it by path from a later node:
Use the same paths in a decision or classify node to branch for example route to one guide when {{steps.account_lookup.body.plan}} equals trial and another when it equals pro. Branch evaluation runs server-side, so the data the branch reads is the data the backend fetched.

Configure a node

The flow editor writes two things: the request config travels with the flow graph (apiCall* keys on the node), and the secret API key is stored separately and encrypted, never in the graph. Saving the key uses PUT /agents/:id/api-calls/:nodeId.
apiCallUrl
string
required
The endpoint to call. Must be https:// (plain http://localhost is allowed only in development). The URL may contain {{...}} tokens. URL-embedded credentials (https://user:pass@host) are rejected.
apiCallMethod
string
default:"GET"
HTTP method, upper-cased. GET is typical for reads.
apiCallQueryParams
{ key, value }[]
Query-string parameters appended to the URL. Keys and values are URL-encoded; rows with a blank key are dropped. Values may use {{...}}.
apiCallHeaders
{ key, value }[]
Extra request headers. FirstFlow always sends Accept: application/json and User-Agent: FirstflowBot/1.0; your headers are merged on top. Values may use {{...}}.
apiCallAuthHeader
string
default:"Authorization"
The header the stored secret key is injected into, as Bearer <key>. Set this to a custom name (e.g. X-Api-Key) if the target expects something other than Authorization.
apiCallTimeoutMs
number
default:"5000"
Request timeout in milliseconds, clamped to 100010000. The request is aborted when it elapses.
apiCallMaxResponseBytes
number
default:"51200"
Maximum response size, clamped to 1024102400 (50 KB by default). A larger body fails the node rather than truncating, so downstream paths never read a half-parsed object.

Saving the secret key

The key never lives in the flow graph. It is encrypted at rest (the same credential vault used for other integrations) and only decrypted in memory at call time, server-side, to build the Bearer header.
The save endpoint validates url (https only, no embedded credentials) and clamps timeoutMs (100010000) and maxResponseBytes (1024102400). The matching GET returns config metadata plus a boolean hasApiKey never the key itself:
Send "apiKey": null to clear a stored key, or omit apiKey on a later PUT to leave the existing key in place while changing other fields.
Most teams configure all of this in the flow editor and never call these endpoints directly. The contract is documented here because the key handling — encrypted at rest, decrypted only server-side, surfaced to clients only as hasApiKey is the reason an API Call node is safe to point at an authenticated endpoint.

Test before you ship

The editor can run a node’s request once, on demand, via POST /flow-node-test/api-call, so you capture the real { status, headers, body } as the step’s sample shape while authoring. The test applies the same SSRF guard as a live run and attaches the stored key only for an agent in your own organization, so a key can never be exfiltrated by testing someone else’s node.
A captured sample lets you reference {{steps.<id>.body.plan}} in downstream nodes with confidence that the path exists.

Failures, retries, and limits

The node distinguishes definitive failures from transient ones, because only transient failures are worth retrying. A thrown error is handed to the engine’s retry policy. The default policy is maxAttempts: 1, which means no retry the run fails unless the node is configured with a higher attempt count, in which case the delay grows exponentially from a base. A definitive failed is never retried; the flow takes its failure path immediately. The SSRF guard is not optional. At runtime the URL is re-validated to be https:// pointing at a public host private, loopback, link-local, unique-local, and carrier-grade-NAT IP ranges are rejected, as are localhost and .local names (except localhost in development). Redirects are re-validated on every hop, so an endpoint cannot 302 you onto an internal address. This is why you cannot point an API Call node at internal infrastructure; expose a public HTTPS endpoint instead. A few smaller constraints worth knowing: the response must fit in apiCallMaxResponseBytes (50 KB default, 100 KB max), the body is interpreted as JSON with a { text } fallback, and the timeout caps at 10 seconds. If you need to push data outward rather than pull it in, reach for a webhook instead it is the outbound counterpart and runs through a retrying outbox.

Next

Branch on what you fetched with a decision or classify node, or wire the value into a guide built from widgets and blocks. For pushing data out instead of pulling it in, see webhooks.