Webhooks Overview
Integration trigger nodes (stripe_trigger, slack_trigger, github_trigger, etc.) receive events from third-party services via webhooks. This page explains the model that's common to all of them — once you understand it, every integration trigger works the same way.
One URL per connection, not per workflow
Every webhook-capable connection has its own delivery URL:
https://<your-deployment>/api/conn-webhook/<connection-webhook-token>The token is stored in connection.metadata.webhook_token when you create the connection. The URL services every workflow that uses that connection — the dispatcher fans inbound deliveries to all matching trigger nodes. You register the URL once with the third party (or it auto-registers), not once per workflow.
Fetch the runtime info for any connection:
bash
curl -H "Authorization: Bearer $TOKEN" \
https://<your-deployment>/api/connections/<connection-id>/webhook-infoReturns the public URL, current status, registration mode, last error if any, and a link to the relevant trigger-node setup docs.
Auto-register vs. manual setup
| Integration | Registration | Why |
|---|---|---|
| GitHub | auto | Repository-scoped webhooks via REST API |
| Jira | auto | Workspace-wide webhook via REST API |
| Shopify | auto | Per-topic subscriptions via Admin API |
| Stripe | auto | Account-level endpoint via REST API |
| Telegram | auto | Bot API setWebhook |
| Linear | auto | Workspace-wide webhook via GraphQL |
| Airtable | auto | Per-base webhook via REST API |
| Mailchimp | auto | Per-audience webhook via REST API |
| Twilio | auto | Per-phone-number SmsUrl via REST API |
| Slack | manual | Event Subscriptions are app-level config — Slack has no API to mutate them |
| HubSpot | manual | Webhook targets are app-level config in the OAuth app — HubSpot has no API to mutate them |
| Dropbox | manual | Webhook URIs are app-level config — Dropbox has no API to mutate them |
Auto-registered connections call the third party's API on save (or on resync) and store the returned external webhook ID + signing secret in metadata. The user doesn't see or copy anything.
Manual-setup connections leave it to the user to paste the URL into the third-party app dashboard. The connection's webhook_status is pending_manual_setup after creation — that's the signal you have work to do in the third-party console. The signing-secret half is read from connection credentials (Slack: signing_secret; HubSpot: client_secret; Dropbox: app_secret).
Re-syncing when the URL changes
If WEBHOOK_BASE_URL changes (e.g. you switched Cloudflare tunnels), existing third-party registrations point at the dead URL. Re-register them:
bash
# one connection
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://<your-deployment>/api/connections/<id>/resync-webhook
# all webhook-capable connections in the team
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://<your-deployment>/api/connections/resync-webhooksResync preserves the connection's webhook_token (so the URL path doesn't change), unregisters the previous external webhook with the third party, and re-registers against the current WEBHOOK_BASE_URL. For manual-setup connections, resync is a no-op against the third party — you still have to re-paste the URL if the public base changed.
Full local-dev runbook: see Local webhook testing in the repo's docs/operations/local-webhook-testing.md.
Verifying what the third party has
For drivers that support it (Shopify today; others can be added), you can ask the third party what it thinks is registered:
bash
curl -H "Authorization: Bearer $TOKEN" \
https://<your-deployment>/api/connections/<id>/external-webhooksUseful when our metadata.external_webhook_id looks fine but events aren't arriving — confirms whether the third party's view matches ours.
Signature verification
Every inbound webhook is signature-verified before the workflow runs. The scheme differs per provider but the failure surface is the same: a 401 signature mismatch in the logs almost always means the connection's stored signing secret has drifted from what the third party is signing with. Fix by resyncing the connection.
Two providers explicitly don't sign:
- Jira — no signing by default; the 26-char ULID in the URL is the security boundary.
- Mailchimp — no signing; same URL-token-is-secret model.
Treat their URLs as bearer secrets — don't share them.
Workflow-side gotchas
Common stumbles when an integration trigger doesn't fire:
- Workflow isn't published. The dispatcher matches only published versions; working drafts fire only via the test webhook route.
eventsfilter mismatch. Each driver'sParseEventTypereturns a provider-specific string (jira:issue_created,Issue,payment_intent.succeeded,change). The trigger'seventsfilter must contain that exact value or"*". Some older workflows may have a staleevent_typesfield instead ofevents— re-pick events in the UI and save+publish to fix.- PAT/scope insufficient on the connection. Auto-registration fails with HTTP 403 if the token can't manage webhooks.
metadata.webhook_status: "error"+webhook_last_errorwill say which scope is missing. - Manual-setup connections aren't pasted in yet.
webhook_status: "pending_manual_setup"is exactly that signal.