Webhook actions
Receive an HTTP call from a workflow and feed its response back into the next node.
actions.http_webhook calls your API from inside a workflow. The response is
parsed and exposed as node outputs, so the next node can branch on your own
service's answer.
Here is the endpoint that receives it:
// app/api/buildbase/provision/route.ts
export async function POST(request: Request) {
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.WORKFLOW_WEBHOOK_SECRET) {
return Response.json({ error: 'forbidden' }, { status: 403 });
}
const { workspaceId, plan } = await request.json();
const account = await provisionAccount(workspaceId, plan);
// Everything returned here is available to later nodes as
// {{responseBody.<key>}}
return Response.json({
accountId: account.id,
region: account.region,
seatsRemaining: account.seatsRemaining,
});
}Before you start
Add the HTTP Webhook action in the console's workflow editor, set the URL, and add your shared secret under Headers. Header values accept merge tags.
Configuration
| Input | Type | Default | Notes |
|---|---|---|---|
url | text | Required | Merge tags allowed, so the path can include a workspace ID |
method | select | POST | POST, PUT, PATCH only |
headers | key-value | — | Merge tags allowed in values |
body | JSON | — | Merge tags allowed with {{key}} |
timeout | number | 30,000 ms | Range 1,000 – 120,000 ms |
The console's method dropdown offers only POST, PUT, and PATCH. The action
is built for sending data, so a read-only lookup against your API needs a POST
endpoint on your side.
Content-Type: application/json is set for you. A string body that parses as
JSON is sent as JSON; one that does not is sent as-is.
Constraints you cannot configure away
| Constraint | Value |
|---|---|
| Request and response body cap | 5 MB each |
| Response stored on the node | Truncated to 4,096 characters |
| Blocked destinations | localhost, 127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x, 0.0.0.0, ::1 |
| Stripped request headers | host, authorization, cookie, x-forwarded-for, x-real-ip, proxy-authorization |
Warning
authorization is silently stripped. Putting your credential in an
Authorization header will not work — it is removed before the request is
sent, with no error. Use a custom header name such as x-webhook-secret, as
the example above does.
Destinations are checked twice: the hostname is matched against the blocked list, then resolved and its IP rejected if it lands in a private range. That second check defeats DNS rebinding, and it also means a workflow cannot call a service on your private network — the endpoint must be publicly reachable.
Outputs
| Output | Type | Description |
|---|---|---|
statusCode | number | HTTP status returned |
responseBody | object | Parsed response body |
responseHeaders | object | Response headers |
success | boolean | Whether the status was 2xx |
duration | number | Round-trip time in ms |
requestedAt | string | ISO timestamp of the request |
Reference them downstream as {{responseBody.accountId}}, {{statusCode}}, and
so on.
Branch on success rather than on statusCode unless you care about a specific
code — success is already the 2xx test.
Outputs
| Output | Type | Description |
|---|---|---|
statusCode | number | HTTP status returned |
responseBody | object | Parsed response body |
responseHeaders | object | Response headers |
success | boolean | Whether the status was 2xx |
duration | number | Round-trip time in ms |
requestedAt | string | ISO timestamp of the request |
Reference them downstream as {{responseBody.accountId}}, {{statusCode}}, and
so on.
Branch on success rather than on statusCode unless you care about a specific
code — success is already the 2xx test.
What counts as a failure
This is the part worth reading twice, because it is not what most people assume.
| Outcome | Node result | Retried? |
|---|---|---|
2xx | Completes, success: true | — |
4xx or 5xx | Completes, success: false, statusCode set | No |
| Timeout | Throws | Yes |
| DNS failure, connection refused, TLS error | Throws | Yes |
| Blocked host or private IP | Throws | Yes |
A non-2xx response does not fail the node. The request is treated as having
happened — the status is simply recorded and the flow continues down the normal
path with success: false. Retries exist for requests that never got an
answer, not for answers you did not like.
Two consequences:
- Returning
409for "already processed" is fine. It will not be retried and it will not fail the instance. Branch onsuccessorstatusCodeinstead. - If you want a bad status to stop the flow, add an
if_elsecondition on{{success}}after the webhook node. Nothing does this for you.
Retries
The retry policy applies only to the throwing cases above. This action overrides the workflow defaults, because outbound HTTP is the least reliable thing a workflow does:
| Setting | HTTP webhook | Workflow default |
|---|---|---|
maxAttempts | 5 | 3 |
backoffType | exponential | exponential |
backoffDelay | 10,000 ms | 5,000 ms |
timeoutMs | 60,000 ms | 30,000 ms |
Note the two timeouts are different things: the timeout input (default
30s) bounds a single request, while timeoutMs in the retry policy (60s)
bounds the attempt from the queue's point of view.
Warning
Five attempts with exponential backoff means your endpoint must be idempotent. A request that times out after doing its work is retried, and the workflow has no way to know the first attempt succeeded. Key your writes on something stable from the payload.
Testing
Running the workflow with /test in dry-run mode does not call your
endpoint. The node logs what it would have sent and returns placeholder
outputs. Use hot-run to exercise the real request.
Verifying the request
The action sends no signature — there is no HMAC header and no shared signing secret, unlike webhooks, which do sign their deliveries.
Authenticate the call yourself:
- Put a long random secret in a custom header, as the example above does, and compare it in constant time.
- Or embed an unguessable token in the URL path.
Do not rely on source IP. Workflow execution runs from the BuildBase workers and the address is not contractual.
Sending workflow context
The body is JSON with merge tags, so pass whatever the receiving service needs:
{
"workspaceId": "{{workspaceId}}",
"plan": "{{subscription.planName}}",
"triggeredBy": "{{trigger.id}}"
}Merge tag references are validated at publish time. A tag pointing at a node
that no longer exists blocks the publish rather than silently sending null.
When the request never lands
After the fifth failed attempt — timeout, DNS, refused connection — the node fails. The instance then follows its error path if one is wired, or fails outright if not. Failed instances stay visible in the console and can be retried per node, so a fix on your side does not mean re-running the whole flow. See monitoring.
Next Steps
- Monitoring — retry a failed node.
- Triggers and actions — the full catalog.
- Webhooks & events — signed outbound events, no workflow needed.