Skip to content

Call an API from a Flow

Flows are great at collecting answers (Ask / Send / Buttons). The Call API node adds the missing piece: reading and writing your own backend during the chat. With it you can build order lookups, live pricing, and real transactions directly in the Flow builder instead of a separate bot.

In the Flow builder, open the palette → Data category → Call API. It has two output handles:

  • success — the request returned 2xx
  • error — non-2xx, a timeout, or any failure

Connect a fallback message to the error handle so the customer always gets a reply.

FieldNotes
MethodGET / POST / PUT / PATCH / DELETE
URLSupports {{variable}} — e.g. https://api.example.com/orders?phone={{phone}}
HeadersKey/value list. Each value is either a literal (with {{variable}}) or a 🔒 secret referenced by name (see below)
BodyJSON with {{variable}} interpolation (shown for POST/PUT/PATCH/DELETE)
Sign requests (HMAC)Off by default. Pick a 🔒 Flow Secret to sign every request this node sends (see Request signing below)
Response mappingresponse path → variable. Dot/JSON-path (e.g. data.orders.0.id). Lists become newline-joined text — handy for a following Send message
TimeoutMilliseconds (default 10000, capped at 30000)

After the call, two variables are always set:

  • {{_http_status}} — the HTTP status code
  • {{_http_error}} — a short reason on the error branch (e.g. HTTP 404, request failed: ConnectTimeout)

Store credentials once at Settings → Flow Secrets (a name + value, e.g. infracart_api). In a header, switch the value to 🔒 secret and pick the name.

  • The raw value is write-only — it’s never returned by the API and never appears in the exported flow graph JSON.
  • The engine resolves the secret to its value only at run time, server-side.

Some partner APIs require each write to be signed, not just carry an API key — a shared key alone can’t prove who sent a request or stop replays. The Call API node supports this natively: set Sign requests (HMAC) to a Flow Secret and every request the node sends carries two extra headers:

HeaderValue
X-WA-Flow-TimestampUnix time in seconds when the request was sent
X-WA-Flow-Signaturesha256= + lowercase hex of HMAC-SHA256(secret, "<timestamp>." + raw_body_bytes)

How the receiving side verifies (reference):

message = "<timestamp>" + "." + <raw request body bytes> # GETs sign an empty body: "<timestamp>."
expected = "sha256=" + hex(HMAC-SHA256(secret, message))
accept if expected == X-WA-Flow-Signature (constant-time) and |now - timestamp| <= 300s

Guarantees the node makes:

  • Byte-exact. A signed request sends the request body verbatim — the exact bytes the signature covers. Keys are never reordered or re-serialized after signing.
  • Same timestamp in the header and the signed message, generated at send time.
  • Fails closed. If the signing secret is unset (or can’t be decrypted), the node takes the error branch with {{_http_error}} = flow secret '<name>' is not set — it never emits an unsigned write that would be rejected opaquely downstream.
  • Signing composes with normal headers: keep your existing API-key header (literal or 🔒 secret) — the two signature headers are added alongside it.

In the graph JSON the setting is one field on the node’s data:

{ "signingSecretRef": "WHATBOT_FLOW_SIGNING_SECRET" }

Flows are authored by PATCH-ing draft_graph. A Call API node looks like:

http_request node
{
"id": "fetch_orders",
"type": "http_request",
"position": { "x": 600, "y": 140 },
"data": {
"method": "GET",
"url": "https://api.example.com/orders?phone={{phone}}",
"headers": [{ "key": "X-Api-Key", "secretRef": "infracart_api" }],
"body": null,
"responseMap": { "orders": "orders_text" },
"timeoutMs": 10000,
"signingSecretRef": "WHATBOT_FLOW_SIGNING_SECRET"
}
}

Edges branch on sourceHandle: "success" and sourceHandle: "error":

edges
[
{ "id": "e1", "source": "fetch_orders", "sourceHandle": "success", "target": "show_orders" },
{ "id": "e2", "source": "fetch_orders", "sourceHandle": "error", "target": "fallback_msg" }
]

A header with an inline value instead of a secret:

{ "key": "Content-Type", "value": "application/json" }

Order status — buyer taps My orders:

Ask Buttons ("My orders")
→ Call API GET /orders?phone={{phone}} map: orders → {{orders_text}}
success → Send "Your orders:\n{{orders_text}}"
error → Send "Couldn't load your orders right now — try again."

Live rate:

Ask Text ("Which material?", save as material)
→ Call API GET /rate?product={{material}} map: price → {{rate}}
success → Send "Today's {{material}} rate: ₹{{rate}}"
error → Send "Rates are unavailable at the moment."

Submit a real bid:

… collect rfq + amount …
→ Call API POST /bids
body: {"rfq":"{{rfq}}","amt":{{amount}}}
header X-Api-Key = 🔒 infracart_api
map: rfq_no → {{rfq_no}}
success → Send "Quote submitted for {{rfq_no}} ✅"
error → Send "We couldn't submit your quote — please retry."
  • Never breaks the flow. Any failure/timeout/non-2xx routes to the error handle — the run never crashes.
  • Interpolation applies to URL, header values, and body via {{variable}}. Unknown variables are left as-is.
  • Body must be valid JSON after interpolation. If a variable’s value breaks the JSON (a stray quote or newline), the node takes the error branch with a clear {{_http_error}} instead of sending an empty body.
  • Response mapping reads up to ~200 KB of JSON. A mapped list is joined into newline-separated text; an object is stored as compact JSON; scalars pass through.
  • Timeout is capped at 30 s so a slow API can’t hang the conversation.
  • Secret headers are stored at the workspace level and referenced by name — never the raw value in the graph.
  • Only fields you map end up in flow variables; nothing else from the response is stored.