Webhook Triggers
Trigger workflows from external webhook events
Webhook Triggers
Webhook triggers start workflows when external systems send events to Loopfour. A webhook trigger is declared on the workflow's trigger configuration — via the API, a template, or an import — not by dragging a block onto the canvas.
Configuration
{
"trigger": {
"type": "webhook",
"provider": "stripe",
"path": "stripe"
}
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "webhook" |
path | string | Yes | Match key for custom webhooks; required by the schema even when provider is set |
provider | string | No | Provider name to match against the inbound event's provider |
events | string[] | No | Accepted by the schema, but not used for matching (see Event Filtering) |
verifySignature | boolean | No | Accepted by the schema, but not read — verification is decided per route |
How Events Reach Loopfour
There are three inbound paths. Which one a provider uses determines what you put in the trigger config.
| Route | Providers | Verification |
|---|---|---|
POST /webhooks/:provider | airwallex, hubspot, justpaid | Svix signature, checked by the provider's handler |
POST /webhooks/nango | stripe, salesforce, quickbooks, pandadoc, netsuite, slack, gmail | Nango HMAC signature at ingress |
POST /webhooks/custom/:companyId/:path | Anything else | None — the URL is the shared secret |
Stripe also has a dedicated POST /webhooks/stripe route for Stripe Connect. See the Webhooks API reference for its request and response details.
Per-provider URLs of the form /webhooks/stripe/{companyId}, /webhooks/hubspot/{companyId}, /webhooks/salesforce/{companyId}, /webhooks/quickbooks/{companyId}, /webhooks/pandadoc/{companyId}, /webhooks/netsuite/{companyId}, and /webhooks/gmail/{companyId} do not exist. Earlier versions of this page advertised them; they were never implemented. Use the routes in the table above.
POST to /webhooks/:provider for a provider with no registered handler returns 404 with { "error": "Unknown provider: <name>" }.
Matching Rules
A workflow with a webhook trigger is selected when:
- The event arrived as
custom(through/webhooks/custom/:companyId/:path) and the trigger'spathequals the:pathsegment. The comparison is case-insensitive and treats.,-, and_as equivalent, soorder.receivedandorder-receivedboth match. A leading slash onpathnever matches. - Otherwise, the trigger's
providerequals the event's provider.
Only active workflows in the receiving company are considered.
Provider Examples
Stripe
Stripe events arrive either through the Nango forwarder (after you authorize Stripe via Nango Connect) or through the dedicated Connect endpoint.
{
"name": "Payment Received Notification",
"trigger": {
"type": "webhook",
"provider": "stripe",
"path": "stripe"
},
"steps": [...]
}Common Stripe Events:
invoice.paid- Invoice was paidinvoice.payment_failed- Payment attempt failedpayment_intent.succeeded- Payment completedcustomer.created- New customer createdsubscription.created- New subscription startedsubscription.canceled- Subscription was canceled
Salesforce
Salesforce supports three webhook types:
{
"trigger": {
"type": "salesforce_event",
"eventKind": "platform_event",
"platformEventName": "Invoice_Created__e"
}
}{
"trigger": {
"type": "salesforce_event",
"eventKind": "cdc",
"cdcEntity": "Opportunity",
"cdcChangeTypes": ["create", "update"]
}
}{
"trigger": {
"type": "webhook",
"provider": "salesforce",
"path": "salesforce"
}
}Salesforce CDC Change Types:
create- Record createdupdate- Record updateddelete- Record deletedundelete- Record restored
HubSpot
HubSpot events arrive through POST /webhooks/hubspot and are matched by the dedicated hubspot_event trigger type, which supports per-event selection:
{
"trigger": {
"type": "hubspot_event",
"eventKind": "deal.propertyChange",
"propertyName": "dealstage"
}
}HubSpot Event Kinds:
contact.propertyChange- Contact property changedcompany.propertyChange- Company property changeddeal.propertyChange- Deal property changeddeal.creation- Deal createddeal.deletion- Deal deleted*- Match all events
QuickBooks
{
"trigger": {
"type": "webhook",
"provider": "quickbooks",
"path": "quickbooks"
}
}QuickBooks event types are derived from the notification entity and operation, lowercased — for example invoice.create, payment.update, customer.delete.
PandaDoc
{
"trigger": {
"type": "webhook",
"provider": "pandadoc",
"path": "pandadoc"
}
}PandaDoc Events:
document_state_changed- Document status changedrecipient_completed- Recipient signeddocument_completed- All signatures collecteddocument_paid- Payment received
NetSuite
{
"trigger": {
"type": "webhook",
"provider": "netsuite",
"path": "netsuite"
}
}Custom Webhooks
For providers not listed above, use custom webhooks. path is the match key — no provider field:
{
"trigger": {
"type": "webhook",
"path": "my-integration"
}
}Custom Webhook URL:
https://your-domain.com/webhooks/custom/{COMPANY_ID}/my-integrationThe body is parsed as JSON when possible; a non-JSON payload is wrapped as { "rawBody": "..." }. There is no signature check on this route — treat the URL as a shared secret and serve it over HTTPS only.
Event Filtering
The events array is accepted by the trigger schema but is not applied when matching. Every event that reaches the matched route starts the workflow.
To narrow what runs:
- Subscribe to only the events you want in the provider's own webhook settings.
- Put a Condition block at the top of the workflow and branch on
{{input}}.
Signature Verification
Verification happens at the route, before workflow matching.
| Route | What is checked |
|---|---|
POST /webhooks/:provider | The provider handler's Svix signature check. A failure returns 401 Invalid signature; a handler that throws returns 500 Verification error |
POST /webhooks/nango | Nango HMAC over the raw body. Fail-closed: a missing secret returns 500, a missing or bad signature returns 401 |
POST /webhooks/stripe | stripe-signature (HMAC-SHA256 over ${timestamp}.${rawBody}, 5-minute window) when STRIPE_WEBHOOK_SECRET is set |
POST /webhooks/custom/:companyId/:path | Nothing |
Store webhook signing secrets securely in environment variables. Never commit them to version control.
Handling Verification Failures
When signature verification fails, the webhook returns 401 Unauthorized:
{
"error": "Invalid signature"
}Check that:
- Your signing secret matches the provider's configuration
- The request hasn't been tampered with
- The timestamp is within acceptable bounds (for Stripe)
Webhook Response
The Stripe and custom routes return:
{
"received": true,
"eventId": "evt_xxx",
"workflowsTriggered": 2
}| Field | Description |
|---|---|
received | Always true for successful receipt |
eventId | Unique identifier for this webhook event |
workflowsTriggered | Number of workflows triggered by this event |
The POST /webhooks/:provider dispatcher returns { "received": true, "count": N }, where count is the number of events in the batch that resolved to a connection and were routed.
Input Data
The full webhook payload is available in your workflow as {{input}}:
Stripe Example
{
"input": {
"id": "evt_1234",
"type": "invoice.paid",
"data": {
"object": {
"id": "in_xxx",
"amount_paid": 9900,
"customer": "cus_xxx",
"customer_email": "customer@example.com"
}
}
}
}Access nested data in your steps:
{
"config": {
"to": "{{input.data.object.customer_email}}",
"amount": "{{input.data.object.amount_paid / 100}}"
}
}Salesforce CDC Example
{
"input": {
"ChangeEventHeader": {
"entityName": "Opportunity",
"changeType": "UPDATE",
"changedFields": ["Amount", "StageName"]
},
"Id": "006xxx",
"Name": "Big Deal",
"Amount": 50000,
"StageName": "Closed Won"
}
}Webhook Events Table
All incoming webhooks are stored in the webhook_events table for audit and replay:
| Field | Description |
|---|---|
id | Unique event ID |
company_id | Company that received this event |
provider | Provider name (stripe, salesforce, etc.) |
event_type | Event type (invoice.paid, etc.) |
event_id | Provider's event ID |
payload | Full event payload (JSON) |
signature_verified | Verification status |
status | Processing status |
workflows_triggered | Array of triggered workflow runs |
Testing Webhooks
Using cURL
The custom route needs no signature, which makes it the simplest one to exercise end to end:
curl -X POST https://workflow.loopfour.ai/webhooks/custom/{COMPANY_ID}/my-integration \
-H "Content-Type: application/json" \
-d '{
"orderId": "12345",
"total": 99.99
}'Using Stripe CLI
# Forward Stripe webhooks to the Connect endpoint on localhost
stripe listen --forward-to localhost:3000/webhooks/stripeA Stripe event with no account field cannot be attributed to a company. It is acknowledged with { "received": true, "ignored": true } and no workflow runs.
Using ngrok
# Expose local server
ngrok http 3000
# Use the ngrok URL in provider webhook settings
# https://abc123.ngrok.io/webhooks/stripeTroubleshooting
Webhook Not Triggering Workflow
- Check workflow status - Must be
active - Check the provider match - the trigger's
providermust equal the provider the event arrived as. Events forwarded by Nango for an unmapped provider key arrive ascustom, not under the provider's own name - Check the path - for custom webhooks, the trigger's
pathmust equal the URL's:pathsegment, with no leading slash - Check signature - ensure signing secrets are configured
- Review webhook events - check the
webhook_eventstable
Duplicate Events
Loopfour does not drop repeated events — the provider's event id is stored and
indexed on webhook_events so you can spot duplicates, but a redelivered event
starts the workflow again. If you receive duplicates:
- Ensure idempotent workflow design
- Check provider retry settings
- Verify webhook acknowledgement is reaching provider
Timeout Issues
Webhooks must respond within provider timeouts:
- Stripe: 30 seconds
- HubSpot: 5 seconds
- QuickBooks: 30 seconds
The API acknowledges webhooks immediately and processes asynchronously.
Loopfour