Loopfour
Triggers

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"
  }
}
FieldTypeRequiredDescription
typestringYesMust be "webhook"
pathstringYesMatch key for custom webhooks; required by the schema even when provider is set
providerstringNoProvider name to match against the inbound event's provider
eventsstring[]NoAccepted by the schema, but not used for matching (see Event Filtering)
verifySignaturebooleanNoAccepted 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.

RouteProvidersVerification
POST /webhooks/:providerairwallex, hubspot, justpaidSvix signature, checked by the provider's handler
POST /webhooks/nangostripe, salesforce, quickbooks, pandadoc, netsuite, slack, gmailNango HMAC signature at ingress
POST /webhooks/custom/:companyId/:pathAnything elseNone — 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:

  1. The event arrived as custom (through /webhooks/custom/:companyId/:path) and the trigger's path equals the :path segment. The comparison is case-insensitive and treats ., -, and _ as equivalent, so order.received and order-received both match. A leading slash on path never matches.
  2. Otherwise, the trigger's provider equals 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 paid
  • invoice.payment_failed - Payment attempt failed
  • payment_intent.succeeded - Payment completed
  • customer.created - New customer created
  • subscription.created - New subscription started
  • subscription.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 created
  • update - Record updated
  • delete - Record deleted
  • undelete - 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 changed
  • company.propertyChange - Company property changed
  • deal.propertyChange - Deal property changed
  • deal.creation - Deal created
  • deal.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 changed
  • recipient_completed - Recipient signed
  • document_completed - All signatures collected
  • document_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-integration

The 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.

RouteWhat is checked
POST /webhooks/:providerThe provider handler's Svix signature check. A failure returns 401 Invalid signature; a handler that throws returns 500 Verification error
POST /webhooks/nangoNango HMAC over the raw body. Fail-closed: a missing secret returns 500, a missing or bad signature returns 401
POST /webhooks/stripestripe-signature (HMAC-SHA256 over ${timestamp}.${rawBody}, 5-minute window) when STRIPE_WEBHOOK_SECRET is set
POST /webhooks/custom/:companyId/:pathNothing

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:

  1. Your signing secret matches the provider's configuration
  2. The request hasn't been tampered with
  3. The timestamp is within acceptable bounds (for Stripe)

Webhook Response

The Stripe and custom routes return:

{
  "received": true,
  "eventId": "evt_xxx",
  "workflowsTriggered": 2
}
FieldDescription
receivedAlways true for successful receipt
eventIdUnique identifier for this webhook event
workflowsTriggeredNumber 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:

FieldDescription
idUnique event ID
company_idCompany that received this event
providerProvider name (stripe, salesforce, etc.)
event_typeEvent type (invoice.paid, etc.)
event_idProvider's event ID
payloadFull event payload (JSON)
signature_verifiedVerification status
statusProcessing status
workflows_triggeredArray 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/stripe

A 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/stripe

Troubleshooting

Webhook Not Triggering Workflow

  1. Check workflow status - Must be active
  2. Check the provider match - the trigger's provider must equal the provider the event arrived as. Events forwarded by Nango for an unmapped provider key arrive as custom, not under the provider's own name
  3. Check the path - for custom webhooks, the trigger's path must equal the URL's :path segment, with no leading slash
  4. Check signature - ensure signing secrets are configured
  5. Review webhook events - check the webhook_events table

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:

  1. Ensure idempotent workflow design
  2. Check provider retry settings
  3. 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.

Next Steps

On this page