Skip to main content

Webhook Module

Webhooks both ways: public endpoints that start a workflow when another system calls them, a Send webhook Task that notifies other systems with signed, retried deliveries, and a Wait for webhook step that pauses a flow until an external system calls back.

The Webhook Module adds one Start event on its own Channel, the Webhook start (Webhook), and two Tasks that work on any Channel. Typical uses: run a flow for every GitHub push or Stripe payment; let a partner's system start a process by posting JSON to a URL; tell a customer's system about an order with a signed webhook; pause a flow until a payment provider or a long job reports back.

Configuration​

The Webhook Module is configured per Environment: each Environment has its own endpoints (its own URLs and secrets) and its own outbound signing secret, so Development and Production never share them.

FieldTypeRequiredDescription
EndpointslistNoThe inbound endpoints (below). At most 20 per Environment.
Outbound signing secretpasswordNoSigns every webhook the Send webhook Task sends (Standard Webhooks headers) unless the Task brings its own secret. Use a whsec_ base64 secret for receivers that verify with a Standard Webhooks library; any other text works too.
Allowed internal hoststext (advanced)NoOne host name or IP per line that Send webhook may call although it is a private or local address. Every other private address is refused.

Endpoints​

Each endpoint gets its own URL: {engine}/api/WebhookModule/{environmentToken}/{key}. The Environment token is the Environment's short URL id (the same one the Form and chat endpoints use). Anything after the key (…/{key}/orders/created) reaches the flow as {webhook.path}.

FieldTypeDefaultDescription
Endpoint idstringStable name, exposed as {webhook.endpointId}. Lowercase letters, digits and _.
EnabledbooleanonA disabled endpoint answers 404.
URL keystringLast URL segment, 22 to 64 letters, digits, - or _. Treat it as a secret when the endpoint has no other authentication. Changing it retires the old URL within about 30 s.
AuthenticationchoiceShared secret headernone, secret, basic, hmac or standard (below).
Secret / Previous secretpasswordThe credential of the chosen mode. The previous secret is also accepted during a rotation.
Secret headerstringX-Webhook-Secretsecret mode: the header the sender puts the secret in. Authorization: Bearer <secret> is always accepted too.
Usernamestringbasic mode: the user name to require. Empty: any user name with the right password.
Signature header, prefix, encoding, field, signed content, timestamp header / field, tolerancehmac mode: how the provider signs (recipes below).
Timestamp tolerance (s)number300hmac with a timestamp and standard modes: a signed timestamp further than this from now is refused. 0: not checked.
Allowed methodsstringPOSTComma-separated: POST, PUT, PATCH, DELETE, GET. Others get a 405.
Event header / Event fieldstringWhere the event name comes from: a header (X-GitHub-Event), else a dotted path in the JSON body (type). Exposed as {webhook.event}.
Delivery id headerstringThe sender's unique delivery id (X-GitHub-Delivery, webhook-id). A delivery seen in the last 48 hours runs once. Empty: every request runs.
Allowed networkstextOne IP or CIDR range per line. Other callers get a 404. Empty: any address.
Requests per minutenumber600Beyond it the sender gets a 429 and retries later.
Response status, content type, body200, application/jsonThe answer to an accepted delivery. {deliveryId} in the body is replaced. 204 sends no body. The flow runs afterwards and never answers the sender itself.

Authentication modes​

ModeWhat the sender doesTypical use
noneNothing: the URL key is the only secret.Internal tools, a quick test. Restrict Allowed networks when you can.
secretPuts the secret in the Secret header or as Authorization: Bearer.Your own apps, Zapier/Make-style senders.
basicHTTP Basic with the user name and the secret as password.Providers that offer Basic auth (Aculab, some SaaS).
hmacSigns the raw body with HMAC-SHA256 and sends it in a header.GitHub, Shopify, Stripe, most providers.
standardStandard Webhooks headers webhook-id, webhook-timestamp, webhook-signature.Svix-powered providers, another EW-Flow Environment's Send webhook.

HMAC recipes

ProviderSignature headerPrefixEncodingFieldSigned contentTimestamp field
GitHubX-Hub-Signature-256sha256=hexbody
ShopifyX-Shopify-Hmac-Sha256base64body
StripeStripe-Signaturehexv1timestamp.bodyt
Generic HMAC(body) in hexthe provider's headerhexbody

Set the Secret to the provider's signing secret. GitHub: also set Event header X-GitHub-Event and Delivery id header X-GitHub-Delivery. Stripe: Event field type, Delivery id header empty (Stripe's event id is in the body: use {webhook.body.id} in the flow).

A mode that needs a secret with none configured refuses every delivery (503, logged), never lets them through.

Webhook start​

Starts the Process when a request reaches one of the Environment's endpoints and passes its checks. A Process may have other starts next to it (one per Channel); the Webhook Channel has nobody to answer, so an Input Task fails there.

FieldTypeDefaultDescription
Only these endpointsstring(all)Comma-separated endpoint ids.
Only these eventsstring(all)Comma-separated event names; a trailing * matches a prefix (order.*). With a filter, a delivery without an event never matches.
Webhook variableoutput{webhook}Receives the delivery (below).

A delivery the filters drop is accepted (the configured answer) and not run, so the provider does not retry it.

Testing. In the editor's chat, start the Process at the Webhook start with test data, e.g. { "event": "push", "body": { "ref": "refs/heads/main" }, "headers": { "X-GitHub-Delivery": "t-1" }, "endpointId": "github" }. body may be JSON text or an object; method, query, path, text and ip are accepted too. {webhook.isTest} is true.

The {webhook} variable​

PathExampleDescription
{webhook.endpointId}githubThe endpoint that received it.
{webhook.deliveryId}72d3162e-…The sender's delivery id when the endpoint names its header, else an id of this delivery.
{webhook.event}pushFrom the event header or field; empty when neither is set.
{webhook.method}, {webhook.path}, {webhook.url}POST, orders/createdThe request.
{webhook.contentType}application/json
{webhook.ip}192.30.252.10The caller's address (also {webhook.from}).
{webhook.headers.<name>}{webhook.headers.x_github_event}Headers by lowercase name with - as _. Authorization, cookies and the secret header read [redacted].
{webhook.query.<name>}{webhook.query.ref}Query string values.
{webhook.body…}{webhook.body.order.id}, {webhook.body.items.0.sku}The parsed JSON body (objects, lists, numbers, booleans), or a form body by field name. Empty for other content types.
{webhook.text}The raw body (up to 256 KB).
{webhook.receivedAt}, {webhook.channel}, {webhook.isTest}, {webhook.to}When it arrived, the run's Channel, test mode, the endpoint id.

Bodies over 1 MB are refused (413). A delivery runs on its own session; the sender's answer never waits for the flow.

Send webhook​

Sends a body to a URL the way webhook providers do, and stores the outcome. Prefer the Standard Module's HTTP request for calls whose answer the flow needs; use Send webhook to notify.

FieldTypeDefaultDescription
URLtemplateAbsolute http(s) URL. Redirects are not followed; private and local addresses are refused unless listed in Allowed internal hosts.
MethodchoicePOSTPOST, PUT or PATCH.
EventtemplateSent as the webhook-event header.
HeaderslistExtra headers (an API key). Content-Type, Content-Length and the webhook-* headers cannot be listed.
BodycodeSent as typed with {variables} filled in as they are (build JSON values in a script when they may hold quotes).
Content typestringapplication/json
Sign the webhookbooleanonAdds webhook-signature; off sends only webhook-id and webhook-timestamp.
Signing secrettemplateOverrides the Environment's outbound secret for this step (a per-customer secret from a record).
Attemptsnumber3The first attempt plus retries on no connection, timeout, 408, 425, 429 and 5xx answers (1 to 5). Other answers are final.
Retry delayseconds (advanced)2Doubles on each further attempt, up to 30 s.
Timeoutseconds (advanced)30Per attempt, at most 120.
Max responseKB (advanced)256Longer answers are cut in {delivery.body}.
Treat non-2xx as errorbooleanonOn: an answer other than 2xx after the last attempt takes the 'error' flow (a warning, the answer still stored). Off: branch on {delivery.status}. No answer at all is an error either way.
Store result inoutput{delivery}The outcome (below).

Headers sent (Standard Webhooks): webhook-id (the same on every attempt, so receivers can deduplicate), webhook-timestamp (Unix seconds), webhook-signature = v1,<base64 HMAC-SHA256(secret, "{id}.{timestamp}.{body}")>, and webhook-event when set. A receiver on another EW-Flow Environment verifies them with an endpoint in standard mode and the same secret.

The {delivery} variable​

PathExampleDescription
{delivery.ok}trueThe receiver answered 2xx.
{delivery.status}200The last HTTP status; 0 when no answer came back.
{delivery.attempts}1Attempts made.
{delivery.deliveryId}9c1f…The webhook-id sent.
{delivery.body}, {delivery.headers.<name>}The receiver's answer.
{delivery.durationMs}412Whole delivery, retries included.
{delivery.error}timed out after 30sWhy it failed; empty otherwise.

Wait for webhook​

Pauses the flow until an external system calls a one-time URL made for this step, then continues with what it sent. The URL is unique to the run and the step (an unforgeable key bound to the execution plus a one-time nonce), accepts any method, answers 202 at once and works once; a second call gets 410.

FieldTypeDefaultDescription
Timeoutseconds0How long to wait before giving up on the 'error' flow with {callback.timedOut} = true. 0: as long as the session may last. At most 14 days.
Notify URLtemplateBefore waiting, post the callback URL there so the other system learns it (signed with the Environment's outbound secret when set, event callback.registered). Empty: nothing is sent.
Notify bodycode{"callbackUrl": "…"}{callbackUrl} is replaced by this step's URL; other {variables} are filled in.
Notify headerslistExtra headers for the notification.
Callback URL variableoutput{callbackUrl}Receives the URL when the step starts.
Callback variableoutput{callback}Receives what the callback sent (below).

The URL exists only once the step runs, so the usual way to hand it out is Notify URL. A failed notification (after the retries) stops the step with an error instead of waiting for nobody. The engine needs a public address (Engine:PublicBaseUrl) to build the URL.

The {callback} variable​

PathDescription
{callback.received}True when a request reached the URL.
{callback.timedOut}True when the timeout ended the wait.
{callback.method}, {callback.contentType}, {callback.ip}, {callback.receivedAt}The request.
{callback.headers.<name>}, {callback.query.<name>}As for {webhook}.
{callback.body…}, {callback.text}The parsed JSON or form body, and the raw text.

Limits and safety​

  • Inbound bodies: 1 MB; headers: 64, 4 KB each; the raw text kept in the variable: 256 KB.
  • Every endpoint is rate-limited (its own per-minute limit, and 600 requests per minute per caller network across all endpoints). Limits and delivery-id deduplication need Redis; without it every request runs.
  • A secret is compared in constant time; signed timestamps are checked against the tolerance; secrets never reach {webhook.headers}.
  • Outbound: http and https only, no redirects, private and local addresses refused (loopback, 10/8, 172.16/12, 192.168/16, 169.254/16, 100.64/10, IPv6 local), checked again after DNS resolution and on every attempt.
  • Callback URLs carry no secret of the engine: the key is an HMAC over the execution id with a key derived from WebhookModule:CallbackKey (falling back to InternalApi:Token); set one of them so every engine instance verifies the same keys.