Webhook triggers
A webhook trigger lets an external system start the workflow with an HTTP call. It is the right choice when something happens in a system you do not control and you want to react as it happens: an order confirmed in a marketplace, a file registered by a supplier portal, a status pushed by a payment provider.
The address is issued per trigger
The endpoint address is issued per trigger and shown on the trigger in Cockpit. Copy it from there and hand it to the calling system.
There is no address pattern to construct. You cannot derive the address from the workflow's name, the tenant, or the trigger's id, and creating a second webhook trigger gives you a second, different address — even on the same workflow. Installing a template that carries a webhook trigger issues a fresh address for your copy, so an address from someone else's screenshot is not yours.
Practically, that means the address is a piece of configuration you have to move by hand, once, from Cockpit into the calling system. Treat it as one: keep it wherever that system's configuration lives, and expect to re-copy it if the trigger is ever recreated.
Public endpoint and protection
A webhook trigger has two ways in, and which one you have decides who can call it:
- Authenticated endpoint (the default). The caller needs a platform API token and the tenant it acts on, like any other call to the Integration Studio API. Use it for your own systems.
- Public endpoint. Switch on Public endpoint in the trigger's settings and save. Where public endpoints are available, the trigger then gets its own public URL and its own endpoint secret, for a third-party system that holds no platform token. Everything below about protection, the raw body and the response applies to this endpoint.
Saving does not always give you a URL. Where public endpoints are not available on the platform, switching a webhook trigger to public is refused (as of 2026-11): the workflow save answers 422 with a message that starts "Public exposure is not available" and names the error at triggers.N.config.public (N is the trigger's position in the triggers array, counting from 0), and nothing of the workflow is saved. No call tells you beforehand whether public endpoints are available; this refusal is how you find out. Changing the Protection of a webhook that already has a public endpoint is refused too: 422, a message that starts "The auth profile cannot be changed", and the error at triggers.N.config.authProfile. A save that leaves an already public trigger and its protection as they were is not refused, and neither is a revision restore or a template install.
Switching Public endpoint off, removing the trigger, or changing it to another trigger type tries to take its public URL down when you save, as a best effort: if that step fails, the save still stands and the URL keeps answering. Nothing retries it on its own. For a trigger the workflow still has, the next save tries again, so save again if the old URL still answers; for a removed trigger, nothing tries again. To be sure a URL is gone before you remove its trigger, switch Public endpoint off, save, and check that the old URL no longer answers. Deleting the whole workflow (in the Studio, through the API or over MCP) takes the public URLs of its webhook triggers down the same way, as a best effort (as of 2026-11): if that step fails, the workflow is still deleted, the URL keeps answering, and nothing tries again. To be sure, do the same before you delete a workflow with a public trigger.
For API users: only the platform writes a trigger's public registration (config.inbound). A workflow save that sends config.inbound has it dropped, and the stored one is kept. The save does not return the endpoint secret either, so get it by rotating the endpoint secret (Rotate endpoint secret in the editor).
Choose the protection before you hand the URL to anyone. The Protection setting decides how the platform checks the caller before the workflow runs:
| Protection | The caller sends |
|---|---|
| API key / token header (the default) | The endpoint secret in a header. You can name the header; left empty it is X-Webhook-Token. For that default header only, ?token= in the query string is also accepted |
| Bearer token | Authorization: Bearer <secret> |
| Basic auth | The endpoint secret as the password. The username is any value unless you pin one |
| None (capture everything) | Nothing. Anyone who has the URL can start the workflow |
A call that fails the check is answered with 401 and does not start a run.
The secret is generated by the platform, and the editor shows it only when you choose Rotate endpoint secret, once, so copy it into the calling system straight away (as of 2026-10). Rotating issues a new secret and keeps the previous one working until the next rotation, so you can switch the caller over without a gap. There is no way to enter a secret the sender gave you.
The editor offers only the four protections above (as of 2026-10). The Integration Studio API also accepts three signature profiles on a webhook trigger's authProfile: hmac-generic (a configurable header, SHA-256 or SHA-512, hex or base64, over the raw body), stripe and standard-webhooks. For these the platform verifies the signature against the endpoint secret, and a signature older than five minutes is refused. Because the secret is platform-generated, a provider that signs with a secret of its own, as Stripe does, cannot be verified this way. Setting mode to annotate in authConfig captures a failed check instead of refusing it, and the run then sees verified as false.
Method matching
A webhook trigger declares the HTTP method it accepts — GET, POST, PUT, PATCH or DELETE, defaulting to POST. A call using any other method is rejected, with a response naming the method the endpoint expects. It does not start a run.
If a partner's system reports a 405, the method on the trigger and the method they are sending do not match; nothing else is wrong. Check the trigger first — it is a one-field fix, and it is the single most common false alarm on a new integration.
What the workflow sees
The request is turned into a payload with four parts:
| Expression | Value |
|---|---|
${{ trigger.payload.body }} | The request body — parsed when it is JSON or a form post, raw otherwise |
${{ trigger.payload.query }} | Query-string parameters |
${{ trigger.payload.headers }} | The request headers, with authentication headers removed |
${{ trigger.payload.method }} | The method the call used |
Authentication headers are stripped before the payload is built, so a workflow — and its run history — never carries the caller's credential. Do not design a flow that reads a token out of headers and forwards it; it will not be there.
A call to the public endpoint arrives wrapped. The platform has already checked the caller, and it hands the workflow a JSON envelope rather than the bare request. trigger.payload.body is that envelope:
| Expression | Value |
|---|---|
${{ trigger.payload.body.verified }} | true when the protection check passed. Always false for None, and for a failed check in annotate mode |
${{ trigger.payload.body.request.body }} | The sender's body, parsed. null when it is not JSON |
${{ trigger.payload.body.request.body_b64 }} | The sender's exact bytes, base64-encoded. This is the raw body, for a signature check of your own or a payload that is not JSON |
${{ trigger.payload.body.request.headers }} | The sender's headers, names in lower case. authorization, cookie, proxy-authorization and x-csrf-token are removed |
${{ trigger.payload.body.request.query }} | The sender's query parameters, the first value of each |
${{ trigger.payload.body.id }} | The platform's id for this delivery |
${{ trigger.payload.body.received_at }} | When the call arrived, as an RFC 3339 timestamp |
The envelope is posted to the workflow with POST, so set a public trigger's method to POST: any other method on the trigger rejects every delivery.
Whether the caller waits
This section is about the authenticated endpoint. A webhook interacts with the workflow's execution_mode, and the default is not the same as it is for a run you start yourself:
async_only— the call is acknowledged immediately and the run continues in the background.sync_only— the caller waits for the result.caller_decides— the caller waits by default. This is the opposite of creating a run directly, which defaults to asynchronous.?mode=asyncor?mode=syncon the call overrides it.
A call that arrives while the workflow's build is not ready is refused with a 409 that names the build status. Nothing is kept for a replay, so the caller has to send it again (as of 2026-10).
On both the public and the authenticated endpoint, a call to a webhook trigger that is inactive, or whose workflow is inactive, is answered with 404 exactly like an address nobody has, and starts no run (as of 2026-10). See Triggers.
A synchronous webhook is capped at 30 seconds. See Execution modes for what that means for a caller that retries.
Design the calling side for the mode you chose: a fire-and-forget caller against an asynchronous webhook never sees a workflow failure, so the failure has to be visible on your side — an error branch that notifies, or the run history.
The public endpoint always answers first
A public endpoint does not run the workflow while the caller waits. It checks the caller, stores the request, and answers 200 with {"id": "<delivery id>"} straight away. The workflow cannot shape that response: its result, its status and its body never reach the sender, whatever the workflow does. A sender that needs the workflow's answer cannot get it from the public endpoint.
The workflow then starts in the background, always asynchronously. That means a workflow whose execution_mode is sync_only cannot sit behind a public endpoint: the platform asks for an asynchronous run, the workflow refuses it, and the delivery is parked. Use async_only or caller_decides.
If starting the run fails for a passing reason, such as the worker being unavailable, the platform retries up to seven times, from 30 seconds apart to six hours. A refusal that retrying cannot fix, such as a method mismatch, is not retried.
Duplicates
The platform removes a duplicate only when the request carries a provider event id it can read, which today means the stripe and standard-webhooks profiles. A repeat of such an event is answered 200 with {"duplicate": true} and starts no second run. Under any of the four protections the editor offers, a sender's retry starts a second run. Make the workflow safe to run twice: take a stable id from the sender's body, such as an order number, and check it before you act on it.
Size and rate limits
A body larger than the endpoint's cap is refused with 413 and starts no run. The default cap is 1 MiB (as of 2026-10). A sender that exceeds its rate limit gets 429. For anything bigger than a small event, have the sender post a reference, such as a file name or an order number, and let the workflow fetch the content, rather than posting the content itself.
The Integration Studio API itself accepts request bodies up to 10 MB by default, and answers 413 beyond that. That is the limit for starting a run through the API with a payload; it does not raise the public endpoint's cap.
Where to go next
- Execution modes — the synchronous cap, and the build gate a call passes through.
- Triggers — what all four types share.
- Expressions — reading the four payload parts into a node's config.
- Webhooks — the other direction: the platform calling your endpoint.