Webhook
The node that listens. It owns a URL you paste into Stripe, GitHub, Typeform or your own app, and everything that arrives there becomes an event that runs the rest of the flow. Most pipelines begin here, and the one thing you do first is copy an address off the screen.
This page is about using the node from the dashboard. The terminal side — the ingress endpoint and its status codes, the REST routes, the CLI — lives on Webhooks — API & CLI.
How it works
A request arrives at the webhook's URL. HostWebhook checks the protections you turned on, stores the event, and then does two independent things with it:
- Runs the flow. Every node wired to the webhook on the canvas receives the payload — a Filter, a Transform, an Email, an AI Node, whatever you connected. This is what the canvas is for.
- Delivers to the Target URL, if you set one. The event is sent to your own server with retries, health tracking and a signature, independently of what the flow does. This is the classic “webhook relay” use, and it is optional.
Both happen for every event. A webhook with no target URL is a perfectly normal webhook — it just feeds the flow and delivers nowhere. A webhook with a target URL and nothing on the canvas is a relay with retries.
Webhook URL and Webhook ID
Two rows at the top of the right-hand column, each with a copy button. Webhook URL is the whole address — that is what a provider needs. Webhook ID is the last segment of it on its own, which is what the rest of the product asks you for.
Webhook URL https://api.hostwebhook.com/api/in/a1b2c3d4e5f6…
Webhook ID a1b2c3d4e5f6…The internal document id — the one in the browser's address bar — is not pasted anywhere and does not travel in the URL or in any header. It is in the Info card at the bottom of the column if you ever need it.
isActive: true also starts paused.Public URL, on and off
Next to the URL there is a Public URL switch. On — the default — the address accepts requests from anywhere. Off, the webhook stops answering from outside: the URL row is replaced by a Public URL blocked notice, and the only things that can still send it events are the ones inside HostWebhook — a chain, a Scheduled Workflow, or an internal call.
The Webhook ID row stays visible either way: turning the public URL off does not delete the identifier, it only stops it being callable from outside, and the things inside HostWebhook keep using it.
Rotate
Rotate, beside the URL, issues a new Webhook ID. The old URL stops accepting deliveries the moment you confirm — there is no grace period — so every provider posting to it needs the new address before it will deliver again. Use it when a URL leaked, or when you want to be sure an old integration is really gone.
Target URL
General → Target URL is where HostWebhook delivers each event in addition to running the flow. Leave it empty for a webhook that only feeds the canvas. Fill it in and every event that passes the protections is POSTed there, with:
- Retries on failure, with the strategy and delay you pick — see Delivery settings.
- A signature, so your server can check the delivery really came from HostWebhook — see Signing and Receiving webhooks for the handler side.
- The payload transform, if you wrote one — see below.
- The payload filters, if you set any: an event that does not match is stored as filtered and not delivered — see Payload filters.
localhost or a private address. The save is refused. To deliver to a server on your own machine while you develop, use the CLI tunnel — the walkthrough is on Local development.Payload transform
The Payload Transform card rewrites what is sent to the Target URL. Write a template in the same syntax as the rest of the product — {{payload.customer.email}}, {{headers.x-github-event}}, {{meta.eventId}} — and the result becomes the body of the delivery. It does not change what the flow receives: nodes on the canvas get the event as it arrived. Saving an empty transform clears it. The syntax, the pipes and the context variables are on Payload transforms.
Protection
Four defences, each its own switch, any combination. When more than one is on, a request has to pass all of them. The full picture, with what each one does and does not stop, is on Ingress protection; this is what each card on the screen means.
Require API key
An API key says who is sending. Turn this on when the thing posting to you is your own code — a client app, a backend service, a script — rather than a third-party provider like Stripe. The sender includes a HostWebhook key in the Authorization header of every request, and the webhook checks three things: the key is valid and belongs to your organization, it is on this webhook's list of allowed keys, and it has the right scope.
The scope is ingress:write. It means “can send events to webhooks that ask for a key” and nothing else — a key with only that scope cannot list your events, replay them or touch any other part of the API, which is exactly what you want for a key you hand to someone else. Create it under Settings → API Keys, tick Ingress → Write, and pick it in the webhook's list. Keys made before that scope existed carry events:write, which also opens the ingress — it just opens more than that. Everything about keys and scopes is on API keys.
Third-party providers cannot send you an API key. For those, use signature verification instead.
Incoming signature verification
A signature says the body was not tampered with and comes from a provider that holds a shared secret. Pick the provider in the dropdown — Stripe, GitHub, Shopify, HostWebhook (for one of your own webhooks signing what it sends), or Static secret for a plain shared token — paste the secret the provider gave you, and every delivery is checked before it becomes an event. One that fails the check is kept, marked signature invalid, so you can see it happened; it does not run the flow. The secret is stored encrypted and never shown again. How each provider signs, and how to reference a Signing Secret instead of pasting one, is on Verifying signatures.
IP allowlist and rate limit
IP allowlist takes one address or CIDR range per line; anything else is refused. Empty means everyone. Rate limit caps how many requests per minute the webhook accepts — past it, the sender gets a “too many requests” answer and nothing is stored. 0 means no limit.
The “No protection” banner
An amber banner appears at the top of the node page when the public URL is on and none of the four defences is. It says “Anyone with the webhook URL can send events” and offers a Configure button that opens the form. It is not shown inside the onboarding modal, where a brand-new webhook would always trigger it.
Response mode
What the sender gets back. None — the default — answers “accepted” at once and the flow runs on its own; the sender never learns what happened next. Sync keeps the connection open until a Schema Validator in the flow has judged the payload, and answers with that verdict — accepted or rejected, with the status codes and the body you configured on the validator — or until an AI Node answers with its output, or a 500 with the reason if it fails. It waits 20 seconds at most, 55 when an AI Node can answer. You cannot turn Sync on without a Schema Validator or an AI Node in the flow, because there would be nothing to answer with. The full contract is on Sync responses.
Repeat deliveries
Providers retry. If the same delivery arrives twice, the second is dropped instead of running your flow again. Deduplication is always on and there is nothing to set up.
Idempotency-Key header, the provider's own delivery id (GitHub, Shopify, PayPal, Twilio and the Standard Webhooks header are recognised), or an id in the body. Anything without one of those goes through — two identical form submissions are two real events, not a duplicate.HostWebhook remembers each id for 5 minutes. Past that, the same id counts as a new event. A repeat is answered with a success status and a body that says deduplicated, so the provider stops retrying.
Delivery settings
These cards only appear once there is a Target URL — without one there is nothing to deliver — and they only concern that delivery, never the flow.
- Delivery & Retries — how many times to retry a failed delivery (0–20), how long to wait before the first retry, and whether the wait stays fixed (linear) or doubles each time (exponential; exponential-capped, the default, doubles but never waits more than 30 minutes). What a “failure” is and what each attempt looks like is on Events & retries.
- Health Monitoring — how many failures in a row turn the badge in the header amber (Degraded, default 2) and then red (Down, default 4).
- Auto-Recovery — while the target is down, probe it every so often and mark it healthy again when it answers; optionally replay the events that failed in the meantime. See Dead Letter Queue.
- Circuit Breaker — when the target keeps failing, stop hammering it for a while, then let a few deliveries through to see if it recovered. See Circuit Breaker.
- Advanced Delivery — Priority (0–10, lower is higher, 0 default) decides who goes first when the queue is busy; Fallback URLs, one per line, are tried in order when the primary fails. See Priority Queues.
The Configuration card opens collapsed with a read-only summary of all of this; Edit opens the form, Done closes it, and there is no Save button — it autosaves. The one thing that holds it open is a half-typed Target URL: an unfinished address is not sent, and Done stays disabled until you finish it or clear it.
Internal calls
By default a webhook is only reachable from outside: someone POSTs to its URL. Internal calls lets things inside HostWebhook run it directly — no HTTP, no URL, no signature, no API key — so a webhook becomes a building block that other flows and AI agents in your organization can invoke.
What can call it
- Another flow, through a flow link between two webhooks: the flow on the other side ends in a webhook, this one starts yours, and you join them from either one — Add a target on the first, or Add a source on the second. This is how flows in different workspaces talk to each other — see Linking two flows.
- An AI Node's answer, through a flow link — the one exception to “between webhooks”. Open the AI Node — in any workspace — and in its Tools tab, under Cross-workspace links, use Link to another flow to pick this webhook. Every time that AI Node answers, this webhook fires with its output. The AI Node has no Internal calls switch of its own; only this webhook's counts. A flow link fires after the AI Node answers and returns nothing to it; when the model needs this flow's result to answer, add the webhook as a tool instead (next item).
- An AI Node, as a tool: add the webhook to the AI Node's tools and the model decides at runtime whether to call it, passing the arguments it built as the payload. No link is created; the row in the Incoming list is read-only and says paused when the tool is disabled on the AI Node.
Below the toggle are two lists. Outgoing comes first: the webhooks this one hands its events to. Then Incoming: the webhooks, AI Nodes and tools that call this one. Flow links are persistent, and each row has a sync/async switch, a Last fired · N total line and a button to disconnect it.
Linking two flows
Flows in different workspaces are joined webhook to webhook. Flow A ends in a webhook — call it W_A: the last node of flow A is connected to a Webhook node. Flow B starts in another webhook, W_B. Turn on Internal calls on both, then join them from whichever page you are on: on W_A, Internal calls → Outgoing → Add a target and pick W_B; or on W_B, Internal calls → Incoming → Add a source and pick W_A. It is the same link either way. From then on, every event W_A receives also fires W_B — inside HostWebhook, with no HTTP and no signature — and flow B runs with the payload W_A got.
Workspace A Webhook ─▶ Transform ─▶ W_A (Internal calls on)
│
│ flow link — "Add a target" on W_A,
│ or "Add a source" on W_B
▼
Workspace B W_B ─▶ Filter ─▶ Email (Internal calls on)- Both webhooks consent, and both can withdraw it. W_A needs Internal calls on to be picked, and it is checked again every time the link fires: turning Internal calls off on W_A — or pausing W_A — stops its links, exactly as doing it on W_B does. Nothing is deleted; turn it back on and they fire again.
- Every event W_A receives means every one — those that reach it from the end of flow A, and any POSTed to W_A's own URL while its public URL is on. What hangs off W_A on its own canvas runs first, then its links.
- One link, two pages. The link shows up under Outgoing on W_A and under Incoming on W_B, whichever side you created it from. Disconnect it from either page and it is gone from both.
- The picker lists the webhooks of your organization that have Internal calls on, from every workspace you can see, the other workspaces first. It leaves out this webhook and the ones already linked in that direction: a webhook cannot be linked to itself, and there is one link per pair and direction.
- Sync or async is chosen when you create the link and switched on its row. Sync — the default — makes W_A's side wait until flow B has finished, up to the link's Sync timeout: 30 seconds unless you change it, and never more than your plan allows (10 s free, 60 s pro, 300 s enterprise). Async fires and moves on. Neither brings flow B's result back to flow A.
- Last fired · N total counts the times W_B actually ran. A link that was skipped — W_B paused, Internal calls off on either side, a loop — does not add to it. A sync link whose wait ran out does, because W_B ran anyway.
What crosses a link, and what does not
- The payload crosses. W_B receives what W_A received, with a
_metaenvelope added that says where it came from (see How it works). $()does not. From a node in flow B,{{$("Node name")}}sees the nodes of flow B — W_B included, with what it received — and nothing in workspace A. Anything flow B needs from flow A has to travel in the payload: add it with a Transform before W_A.- It is one run. Both flows share the run that started on side A, so Run History shows flow B's steps in the same run, with W_B as a step of its own.
- W_B's Target URL gets the event too. If W_B has one, an event that arrives through a link is delivered there just as one that arrived over HTTP would be.
- Links relay. If W_B is itself the source of a link to W_C, that one fires as well — W_A → W_B → W_C — under the same protections: a link that would reach a webhook that already ran in this run — the one that started the chain, or the webhook that received the original event, included — is skipped, so a loop is cut instead of running forever; and a chain stops at your plan's depth (2 links on free, 5 on pro, 10 on enterprise). Link calls are also capped per minute across the organization (60 free, 600 pro, no cap on enterprise).
- When a link does not fire, Logs says why — a warning carrying the run's correlation id, for a loop, the depth limit, W_A or W_B paused, Internal calls off on either webhook, a sync wait that ran out, or the per-minute cap. The one silent case is a link turned off through the API (
enabled: false), on purpose. - Deleting a webhook or an AI Node removes its links, the ones it is the source of and the ones it is the target of. Nothing is left pointing at a node that is gone.
How it works
An internal call does not go through the URL at all. The source writes the event straight into this webhook, the same way a real request would have been stored, and the flow runs from there. Two things are different from an HTTP event:
- The stored event says where it came from. Its type is
flow-linkorai-tool; its headers carryx-hw-flow-link(the link's id) orx-hw-ai-tool, plusx-hw-call-stack; and its payload carries a_metaenvelope —source: "flow-link"or"ai-tool", plus the type and id of the node that called. All of it is visible on the event, in the webhook's event list. - It skips the protections. Signature, API key, IP allowlist and rate limit are HTTP things; there is no request to check. What guards an internal call is the consent gate below, and the fact that the caller already had access to your organization.
{{meta.source}}, not on _meta. Every node receives the payload without the _meta envelope, so {{payload._meta.source}} resolves to nothing. What the flow does get is the run's own metadata: {{meta.source}} is flow-link or ai-tool when an internal call started the flow, and empty for an HTTP event; {{meta.sourceNodeType}}, {{meta.sourceNodeId}} and, for a link, {{meta.flowLinkId}} say who called. They are set by the server, so an HTTP request cannot fake them.A flow link can be sync or async, a switch on each row. Sync — the default — makes the calling flow wait until this webhook's flow has finished, up to the link's timeout and never more than your plan allows (10 s free, 60 s pro, 300 s enterprise), so the caller knows the work was done before it carries on. Async fires and moves on. An event that arrives through a flow link is also delivered to this webhook's Target URL, if it has one, and fires this webhook's own outgoing links — see What crosses a link.
The toggle is a consent gate
Internal calls is off by default, and only someone with access to this webhook's workspace can turn it on. That is what stops a user in workspace A from reaching a webhook in workspace B just by adding it as an AI tool: the owner of B has to opt in first. Once it is on, any flow or AI Node in the organization can reference it.Turning it off does not delete the links and tools that point here — it stops them firing. An amber count tells you how many links are now waiting: “N links to or from this webhook exist, but the toggle is OFF — none will fire until you turn it back on”. Turn it back on and they resume; disconnect a link from the row if you mean it to be gone. On a webhook that is the source of links — W_A above — the same switch holds its outgoing links: off, they stop firing; on again, they resume.
Internal calls vs. Public URL
They are independent switches that answer different questions. Public URL is about the outside world: off means nobody can POST to the address. Internal calls is about the inside: on means other flows and AI agents may run this webhook. A webhook with the public URL off and internal calls on is a private step that only your own flows can trigger — a common shape for a “send the welcome email” or “sync this customer” routine that several flows share.
Send Test Event
At the bottom of the page — and inside the onboarding modal — is Send Test Event. It pushes a payload through the real pipeline and shows you the status code, the response body and the latency. Custom payload opens the JSON editor; Include signature signs the test request the way a real delivery would be signed, so you can check your verification before a provider does.
The example payloads
Above the editor, under “What the object you will receive looks like”, a dropdown offers 22 example payloads — Stripe and Stripe Checkout, GitHub, Vercel, Sentry, Shopify, Slack, Twilio, Telegram, Linear, Jira, Notion, Typeform, Calendly, Cal.com, Zoom, SendGrid, Clerk, Supabase, a generic form and a minimal object — each with a one-line description of what it represents. They are representative shapes, checked against each provider's documentation and trimmed to what you would write a template against, not full copies. Two are worth knowing: SendGrid sends an array of events, and Twilio really posts form-encoded data, shown here as JSON.
When the node above this one has already produced data, a separate button — Input from <that node> — appears above the dropdown. That is the exact payload the connected node produced, _meta included, and it beats any example.
Inside the first flow, and on the canvas
During onboarding the node opens in a modal headed Set up your Webhook. It is the same screen in a compact form: same fields, same autosave. The URL and Webhook ID sit at the very top with their copy buttons — copying that address is the next thing you are going to do — Configuration opens collapsed, and Send Test Event is kept at the bottom so you can copy the URL, fire it, and watch the event arrive without leaving the modal. Rotate and the Public URL switch are hidden there, both being destructive gestures on a webhook that is a minute old; the switch does appear if the URL is already blocked, so there is always a way out of the notice.
On the canvas, clicking the node opens a side panel with the health badge, the target URL, the Webhook URL and Webhook ID rows with Copy links, the signature type if one is set, retries, rate limit, and the payload transform if there is one. It links through to the full node page. A webhook can also be chained to other webhooks — on a successful delivery, its response is forwarded to the next one — which is its own topic: Webhook Chains.
For developers
Sending an event from a terminal, every status code the ingress can answer, where the delivery id and the signature are read from, the REST routes that manage webhooks and events, flow links as documents, and the hostwh CLI — all of it is on Webhooks — API & CLI. Building the server that receives deliveries is on Receiving webhooks, and running it on your own machine on Local development.
Gotchas
- The address has an
/api/in/in it. Copy the URL from the screen rather than typing it: a missing segment fails exactly like a wrong id. - A paused webhook looks like it does not exist to whoever posts to it — some providers disable an endpoint after enough of those. Turning the Public URL off is the safer way to stop traffic for a while.
- A duplicate is answered with success, and the body says so. The provider stops retrying; your flow does not run twice.
- A delivery with no id is never a duplicate. No
Idempotency-Key, no provider header, noidin the body means it runs, however many times it arrives. - The target URL cannot be local. Use the CLI tunnel while developing.
- The target URL is optional. Without one the webhook still receives events and still drives the flow; the delivery cards simply disappear.
- Rotating is immediate. The old URL stops accepting deliveries the moment you confirm.
- An API key is for your own senders. Stripe, GitHub and the rest cannot send one; use signature verification for them.
- Turning Internal calls off does not remove the references. They stay in the list and stop firing, with an amber count of how many are waiting. On a source webhook it stops its outgoing links the same way.
$()stops at the workspace. A flow on the far side of a link sees its own nodes by name, not the calling flow's; only the payload crosses.- The flow does not see
_meta.{{payload._meta.source}}is empty in a template; use{{meta.source}}instead. - Priority is inverted — 0 is the highest and 10 the lowest.
- Sync mode needs a Schema Validator or an AI Node in the flow. The save is refused otherwise, with a message that says which node to add. See Sync needs something that can answer.