Webhooks
The inbound webhook contract: URLs, bearer and HMAC verification, the three response modes with their status codes, and capture semantics.
The developer-facing contract behind the webhook trigger. Each webhook step gets its own endpoint:
POST https://<your-instance>/webhooks/<webhook-id>
Authentication
Two verification methods, set on the step:
- Bearer — MadMax mints the token; send it as
Authorization: Bearer …. Rotating the token on the step invalidates the old one immediately. - HMAC — for senders that sign payloads. Formats:
github(X-Hub-Signature-256),stripe(Stripe-Signature), orplain(hex digest of the body; custom header name supported).
Unauthenticated deliveries get 401 and never touch a workflow.
Response modes
| mode | behavior | caller sees |
|---|---|---|
| immediate | ack now, run async | 202 {"ok": true, "execution_id": …} |
| last step | block until the run ends | 200 + the terminal step's output |
| respond step | block until a respond to webhook step fires | that step's status/headers/body; the rest of the run continues in background |
Other codes worth handling: 404 — unknown id or the workflow isn't active (deliberately indistinguishable); 500 — the run failed before responding; 504 — the response timeout elapsed (the workflow keeps running; only the HTTP wait gave up).
Sample capture
While a webhook step is capturing, one
authenticated delivery is accepted even if the workflow isn't
active — it returns 202 {"ok": true, "captured": true} and
records the payload shape without running anything.
App events (platform-routed)
Each event arrives normalized: payload.text, user, channel,
channel_type, ts, thread_ts, mentioned (the bot was
@-mentioned or DM'd) and bot_thread (a reply in a thread the bot is
already part of — so a conversation continues without re-mentioning
it). Gate on mentioned or bot_thread for an assistant.
The webhook step's App event source is a different transport with
the same trigger: instead of a per-step URL, a published MadMax
app (Slack first) receives every event for the connected account
at one platform endpoint, POST /webhooks/inbound/:provider, and
MadMax routes it to the owning workspace by account id (team_id
for Slack). Nothing is exposed per workflow, so there is no URL or
secret to manage.
Each provider delivery is signature-verified with the app's own
secret before parsing (fail-closed), deduplicated by the provider's
event id, and filtered through a bot-loop guard so a bot can never
trigger itself. The same endpoint family serves the platform SMS
and email triggers (/inbound/surge, /inbound/resend).
Delivery semantics
MadMax doesn't re-request a failed inbound delivery — retries are
the sender's job (most webhook senders retry on non-2xx). Once a
delivery is accepted (202), execution retries happen internally.
The endpoint is rate-limited per source IP.