Webhooks API
Receive and process webhook events from external providers
Webhooks API
Receive webhook events from external providers to trigger workflows. Webhooks are automatically verified, stored for audit, and routed to matching workflows.
Webhook endpoints do not require API key authentication - they use provider-specific signature verification instead.
Provider Webhook Endpoints
Each provider has a dedicated endpoint that handles signature verification and event routing.
| Provider | Endpoint | Signature Header |
|---|---|---|
| Stripe | POST /webhooks/stripe/:companyId | stripe-signature |
| HubSpot | POST /webhooks/hubspot/:companyId | x-hubspot-signature-v3 |
| QuickBooks | POST /webhooks/quickbooks/:companyId | intuit-signature |
| Salesforce | POST /webhooks/salesforce/:companyId | N/A (SOAP XML) |
| PandaDoc | POST /webhooks/pandadoc/:companyId | x-pandadoc-signature |
| NetSuite | POST /webhooks/netsuite/:companyId | x-netsuite-signature |
| Gmail | POST /webhooks/gmail/:companyId | N/A (Pub/Sub) |
| Custom | POST /webhooks/custom/:companyId/:path | N/A |
Path Parameters
| Parameter | Type | Description |
|---|---|---|
companyId | string | Your company UUID |
path | string | Custom path for routing (custom webhooks only) |
Standard Response
All webhook endpoints return the same response format:
{
"received": true,
"eventId": "evt_550e8400-e29b-41d4-a716-446655440000",
"workflowsTriggered": 2
}| Field | Type | Description |
|---|---|---|
received | boolean | Always true on success |
eventId | string | Internal event ID for tracking |
workflowsTriggered | number | Number of workflows triggered |
Stripe Webhooks
Receive events from Stripe for payment and subscription events.
POST /webhooks/stripe/:companyIdHeaders
| Header | Required | Description |
|---|---|---|
stripe-signature | Yes | Stripe webhook signature |
Content-Type | Yes | application/json |
Common Event Types
| Event | Description |
|---|---|
invoice.paid | Invoice was paid |
invoice.payment_failed | Payment attempt failed |
customer.subscription.created | New subscription |
customer.subscription.updated | Subscription changed |
customer.subscription.deleted | Subscription cancelled |
payment_intent.succeeded | Payment completed |
payment_intent.payment_failed | Payment failed |
charge.refunded | Charge was refunded |
Example Payload
{
"id": "evt_1234567890",
"type": "invoice.paid",
"data": {
"object": {
"id": "in_1234567890",
"customer": "cus_xxx",
"amount_paid": 9900,
"currency": "usd",
"customer_email": "customer@example.com",
"status": "paid"
}
},
"created": 1705312000
}Stripe Configuration
- Go to Stripe Dashboard → Developers → Webhooks
- Add endpoint:
https://your-api.com/webhooks/stripe/{companyId} - Select events to receive
- Copy the signing secret to your environment
HubSpot Webhooks
Receive CRM and marketing events from HubSpot.
POST /webhooks/hubspot/:companyIdHeaders
| Header | Required | Description |
|---|---|---|
x-hubspot-signature-v3 | Yes | HubSpot v3 signature |
Content-Type | Yes | application/json |
Common Event Types
| Event | Description |
|---|---|
contact.creation | New contact created |
contact.propertyChange | Contact property updated |
contact.deletion | Contact deleted |
deal.creation | New deal created |
deal.propertyChange | Deal property updated |
company.creation | New company created |
Example Payload
[
{
"eventId": 123456789,
"subscriptionId": 12345,
"portalId": 12345678,
"occurredAt": 1705312000000,
"subscriptionType": "contact.creation",
"attemptNumber": 0,
"objectId": 12345,
"changeSource": "CRM",
"propertyName": "email",
"propertyValue": "new@example.com"
}
]HubSpot Configuration
- Go to HubSpot Settings → Integrations → Private Apps
- Create or edit your app
- Go to Webhooks tab
- Add subscription URL:
https://your-api.com/webhooks/hubspot/{companyId} - Select object types and events
QuickBooks Webhooks
Receive accounting events from QuickBooks Online.
POST /webhooks/quickbooks/:companyIdHeaders
| Header | Required | Description |
|---|---|---|
intuit-signature | Yes | Intuit webhook signature |
Content-Type | Yes | application/json |
Common Event Types
| Event | Description |
|---|---|
Customer | Customer created/updated/deleted |
Invoice | Invoice created/updated/deleted |
Payment | Payment recorded |
Bill | Bill created/updated |
Vendor | Vendor created/updated |
Account | Account created/updated |
Example Payload
{
"eventNotifications": [
{
"realmId": "1234567890",
"dataChangeEvent": {
"entities": [
{
"name": "Invoice",
"id": "123",
"operation": "Create",
"lastUpdated": "2024-01-15T10:30:00.000Z"
}
]
}
}
]
}QuickBooks Configuration
- Go to Intuit Developer Portal → My Apps
- Select your app → Webhooks
- Add endpoint:
https://your-api.com/webhooks/quickbooks/{companyId} - Select entities to subscribe to
- Copy the verifier token to your environment
Salesforce Webhooks
Receive events from Salesforce via outbound messages or Platform Events.
POST /webhooks/salesforce/:companyIdContent Types
| Content-Type | Format | Description |
|---|---|---|
text/xml | SOAP XML | Outbound messages from Workflow Rules |
application/xml | SOAP XML | Outbound messages (alternate) |
application/json | JSON | Platform Events, Change Data Capture |
SOAP XML Response
For outbound messages, Salesforce expects a SOAP acknowledgment:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:out="http://soap.sforce.com/2005/09/outbound">
<soapenv:Body>
<out:notificationsResponse>
<out:Ack>true</out:Ack>
</out:notificationsResponse>
</soapenv:Body>
</soapenv:Envelope>Common Event Types
| Event | Description |
|---|---|
Account | Account record changes |
Contact | Contact record changes |
Opportunity | Opportunity record changes |
Lead | Lead record changes |
Case | Case record changes |
| Custom objects | Any custom object changes |
Example JSON Payload (Change Data Capture)
{
"schema": "xxx",
"payload": {
"ChangeEventHeader": {
"entityName": "Account",
"recordIds": ["001xxx"],
"changeType": "UPDATE",
"changedFields": ["Name", "Industry"]
},
"Name": "Acme Corp",
"Industry": "Technology"
}
}Salesforce Configuration
For Outbound Messages:
- Setup → Workflow Rules → Create new rule
- Add Outbound Message action
- Set endpoint URL:
https://your-api.com/webhooks/salesforce/{companyId}
For Change Data Capture:
- Setup → Change Data Capture
- Select objects to track
- Configure external client with endpoint
PandaDoc Webhooks
Receive document lifecycle events from PandaDoc.
POST /webhooks/pandadoc/:companyIdHeaders
| Header | Required | Description |
|---|---|---|
x-pandadoc-signature | Yes | PandaDoc webhook signature |
Content-Type | Yes | application/json |
Common Event Types
| Event | Description |
|---|---|
document_state_changed | Document status changed |
recipient_completed | Recipient signed/completed |
document_completed | All recipients completed |
document_paid | Document payment received |
document_viewed | Document was viewed |
document_deleted | Document was deleted |
Example Payload
{
"event": "document_state_changed",
"data": {
"id": "doc_xxx",
"name": "Sales Contract",
"status": "document.completed",
"date_created": "2024-01-15T10:00:00.000Z",
"date_modified": "2024-01-15T10:30:00.000Z",
"recipients": [
{
"email": "signer@example.com",
"completed": true,
"completed_at": "2024-01-15T10:30:00.000Z"
}
]
}
}PandaDoc Configuration
- Go to PandaDoc Settings → Integrations → Webhooks
- Add webhook URL:
https://your-api.com/webhooks/pandadoc/{companyId} - Select events to receive
- Copy the shared key for signature verification
NetSuite Webhooks
Receive events from NetSuite via RESTlet or SuiteScript.
POST /webhooks/netsuite/:companyIdHeaders
| Header | Required | Description |
|---|---|---|
x-netsuite-signature | No | Optional signature for verification |
Content-Type | Yes | application/json |
Common Event Types
| Event | Description |
|---|---|
record.create | Record created |
record.update | Record updated |
record.delete | Record deleted |
scheduled.script | Scheduled script event |
Example Payload
{
"type": "record.create",
"recordType": "invoice",
"recordId": "12345",
"data": {
"entity": "123",
"trandate": "2024-01-15",
"total": 1500.00,
"status": "Open"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}NetSuite Configuration
- Create a RESTlet or SuiteScript that POSTs to your webhook endpoint
- Configure User Event Scripts to trigger on record changes
- Set up a Scheduled Script for periodic events
Gmail Webhooks
Receive email notifications via Google Cloud Pub/Sub.
POST /webhooks/gmail/:companyIdGmail uses Google Cloud Pub/Sub for push notifications. You need to configure a Pub/Sub topic and subscription.
Pub/Sub Message Format
{
"message": {
"data": "eyJlbWFpbEFkZHJlc3MiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaGlzdG9yeUlkIjoiMTIzNDU2Nzg5MCJ9",
"messageId": "123456789",
"publishTime": "2024-01-15T10:30:00.000Z"
},
"subscription": "projects/PROJECT/subscriptions/SUB"
}The data field is base64-encoded JSON containing:
{
"emailAddress": "user@example.com",
"historyId": "1234567890"
}Gmail Configuration
- Enable Gmail API in Google Cloud Console
- Create a Pub/Sub topic
- Create a push subscription with endpoint:
https://your-api.com/webhooks/gmail/{companyId} - Call
users.watch()to start receiving notifications
Custom Webhooks
Receive events from any external system with a flexible endpoint.
POST /webhooks/custom/:companyId/:pathPath Parameters
| Parameter | Type | Description |
|---|---|---|
companyId | string | Your company UUID |
path | string | Custom path used as event type |
Example
curl -X POST "https://your-api.com/webhooks/custom/00000000-0000-0000-0000-000000000001/order-received" \
-H "Content-Type: application/json" \
-d '{
"orderId": "12345",
"customer": "cust_xxx",
"total": 99.99
}'The path parameter becomes the event type for workflow matching. In the example above, workflows with trigger webhook.custom.order-received would be triggered.
Non-JSON Payloads
Custom webhooks accept any content type. Non-JSON payloads are wrapped:
{
"rawBody": "your raw content here"
}System Webhooks
These endpoints are used by internal services and typically don't need manual configuration.
Nango Webhooks
POST /webhooks/nangoReceives OAuth completion and sync events from Nango.
| Header | Description |
|---|---|
x-nango-signature | HMAC-SHA256 signature |
Event Types:
| Type | Description |
|---|---|
auth | OAuth flow completed (success or failure) |
sync | Data sync completed |
webhook | Forwarded provider webhook |
Trigger.dev Webhooks
POST /webhooks/triggerReceives run status updates from Trigger.dev.
| Header | Description |
|---|---|
x-trigger-signature | HMAC-SHA256 signature |
Event Types:
| Type | Description |
|---|---|
run.completed | Workflow run completed successfully |
run.failed | Workflow run failed |
run.cancelled | Workflow run was cancelled |
Webhook Events
All incoming webhooks are stored in the webhook_events table for audit and replay.
Event Structure
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"companyId": "00000000-0000-0000-0000-000000000001",
"provider": "stripe",
"eventType": "invoice.paid",
"eventId": "evt_external_123",
"payload": { ... },
"signatureVerified": "verified",
"status": "processed",
"workflowsTriggered": [
{ "workflowId": "wf_xxx", "runId": "run_xxx" }
],
"createdAt": "2024-01-15T10:30:00.000Z"
}Signature Verification Status
| Status | Description |
|---|---|
pending | Verification not yet attempted |
verified | Signature verified successfully |
failed | Signature verification failed |
skipped | No signature provided/required |
Event Processing Status
| Status | Description |
|---|---|
received | Event received and stored |
processing | Currently triggering workflows |
processed | All workflows triggered successfully |
failed | Processing failed |
ignored | No matching workflows found |
Signature Verification
How Verification Works
- Provider sends webhook with signature header
- API verifies signature using stored webhook secret
- If verification fails, returns
401 Unauthorized - If verification succeeds, processes the event
Stripe Signature Verification
const crypto = require('crypto');
function verifyStripeSignature(payload, signature, secret) {
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t='))?.split('=')[1];
const v1Signature = elements.find(e => e.startsWith('v1='))?.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return v1Signature === expectedSignature;
}HubSpot Signature Verification (v3)
const crypto = require('crypto');
function verifyHubSpotSignature(payload, signature, secret, method, url, timestamp) {
const sourceString = `${method}${url}${payload}${timestamp}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(sourceString)
.digest('base64');
return signature === expectedSignature;
}QuickBooks Signature Verification
const crypto = require('crypto');
function verifyQuickBooksSignature(payload, signature, verifierToken) {
const expectedSignature = crypto
.createHmac('sha256', verifierToken)
.update(payload)
.digest('base64');
return signature === expectedSignature;
}Error Responses
Invalid Signature (401)
{
"error": "Invalid signature"
}Invalid JSON (400)
{
"error": "Invalid JSON"
}Invalid Payload (400)
{
"error": "Invalid payload"
}Invalid Pub/Sub Message (400)
{
"error": "Invalid Pub/Sub message format"
}Rate Limiting
Webhook endpoints have a higher rate limit than standard API endpoints:
| Category | Limit |
|---|---|
| Webhooks | 10,000 requests/minute per IP |
When rate limited:
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded"
}
}Outbound Webhooks
Register webhook endpoints to receive notifications when workflow execution status changes.
Outbound webhooks notify your systems about workflow events. This is the inverse of inbound webhooks which trigger workflows.
Webhook Endpoint Management
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/webhook-endpoints | List webhook endpoints |
| POST | /api/v1/webhook-endpoints | Create webhook endpoint |
| GET | /api/v1/webhook-endpoints/:id | Get endpoint details |
| PUT | /api/v1/webhook-endpoints/:id | Update endpoint |
| DELETE | /api/v1/webhook-endpoints/:id | Delete endpoint |
| POST | /api/v1/webhook-endpoints/:id/rotate-secret | Rotate signing secret |
| POST | /api/v1/webhook-endpoints/:id/test | Send test webhook |
| GET | /api/v1/webhook-endpoints/:id/deliveries | List delivery attempts |
| POST | /api/v1/webhook-endpoints/:id/deliveries/:deliveryId/retry | Retry failed delivery |
Create Webhook Endpoint
POST /api/v1/webhook-endpointsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Endpoint name |
url | string | Yes | Webhook URL (HTTPS recommended) |
events | string[] | No | Event types to receive (null = all events) |
workflowIds | string[] | No | Filter to specific workflows (null = all) |
headers | object | No | Custom headers to include |
description | string | No | Description |
Example Request
{
"name": "Invoice System",
"url": "https://api.example.com/webhooks/workflows",
"events": ["workflow.completed", "workflow.failed"],
"workflowIds": ["550e8400-e29b-41d4-a716-446655440000"],
"headers": {
"Authorization": "Bearer your-token"
},
"description": "Notifies invoice system when workflows complete"
}Response (201 Created)
{
"success": true,
"data": {
"id": "whe_550e8400-e29b-41d4-a716-446655440000",
"name": "Invoice System",
"url": "https://api.example.com/webhooks/workflows",
"secret": "whsec_abc123...",
"events": ["workflow.completed", "workflow.failed"],
"workflowIds": ["550e8400-e29b-41d4-a716-446655440000"],
"headers": {"Authorization": "Bearer your-token"},
"status": "active",
"createdAt": "2024-01-15T10:00:00.000Z"
}
}The secret is only returned on creation. Store it securely for signature verification.
Webhook Event Types
| Event | Description |
|---|---|
workflow.started | Workflow execution began |
workflow.completed | Workflow completed successfully |
workflow.failed | Workflow failed with error |
workflow.timeout | Workflow exceeded timeout |
workflow.cancelled | Workflow was cancelled |
workflow.checkpoint | Long-running workflow checkpoint |
Webhook Payload Format
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"type": "workflow.completed",
"timestamp": "2024-01-15T10:30:00.000Z",
"workflowId": "550e8400-e29b-41d4-a716-446655440000",
"workflowName": "Invoice Processing",
"runId": "run_660e8400-e29b-41d4-a716-446655440001",
"companyId": "00000000-0000-0000-0000-000000000001",
"status": "completed",
"triggerType": "webhook",
"startedAt": "2024-01-15T10:29:55.000Z",
"completedAt": "2024-01-15T10:30:00.000Z",
"durationMs": 5000,
"input": {
"invoiceId": "inv_123"
},
"output": {
"processed": true,
"slackMessageId": "1234567890.123456"
}
}Failed Workflow Payload
{
"id": "evt_770e8400-e29b-41d4-a716-446655440002",
"type": "workflow.failed",
"timestamp": "2024-01-15T10:30:00.000Z",
"workflowId": "550e8400-e29b-41d4-a716-446655440000",
"workflowName": "Invoice Processing",
"runId": "run_880e8400-e29b-41d4-a716-446655440003",
"status": "failed",
"durationMs": 3000,
"error": {
"code": "STEP_ERROR",
"message": "API rate limit exceeded",
"stepId": "sync-to-crm"
}
}Signature Verification
Webhooks are signed using HMAC-SHA256 (Stripe-style format).
Headers Sent
| Header | Description |
|---|---|
X-Webhook-Signature | Signature: t={timestamp},v1={signature} |
X-Webhook-ID | Unique event ID |
X-Webhook-Timestamp | Unix timestamp |
Content-Type | application/json |
Verification Example (Node.js)
import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) {
const [timestampPart, signaturePart] = signature.split(',');
const timestamp = timestampPart.split('=')[1];
const receivedSig = signaturePart.split('=')[1];
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
);
}
// Express middleware example
app.post('/webhooks/workflows', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
console.log('Received event:', req.body.type);
res.status(200).json({ received: true });
});Delivery Retry Strategy
Failed deliveries are automatically retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 10 seconds |
| 2 | 30 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 15 minutes |
| 6 | 1 hour |
| 7 | 6 hours |
| 8 | 24 hours |
After 8 failed attempts, the delivery is marked as permanently failed.
Circuit Breaker
After 10 consecutive failures, the endpoint is automatically disabled to prevent continued failures. Re-enable by updating the endpoint status:
PUT /api/v1/webhook-endpoints/:id{
"status": "active"
}Test Webhook
Send a test webhook to verify your endpoint is working:
POST /api/v1/webhook-endpoints/:id/test{
"eventType": "workflow.completed"
}Response:
{
"success": true,
"data": {
"delivered": true,
"httpStatus": 200,
"responseBody": "{\"received\":true}",
"eventId": "evt_test_xxx"
}
}List Delivery History
GET /api/v1/webhook-endpoints/:id/deliveries?status=failedResponse:
{
"success": true,
"data": [
{
"id": "del_xxx",
"eventType": "workflow.completed",
"eventId": "evt_xxx",
"status": "failed",
"httpStatus": 500,
"attemptCount": 3,
"lastError": "Connection timeout",
"nextRetryAt": "2024-01-15T11:00:00.000Z",
"createdAt": "2024-01-15T10:30:00.000Z"
}
],
"meta": {
"cursor": null,
"hasMore": false
}
}Manually Retry Delivery
POST /api/v1/webhook-endpoints/:id/deliveries/:deliveryId/retryResponse:
{
"success": true,
"data": {
"queued": true,
"deliveryId": "del_xxx"
}
}Workflow Trigger Configuration
To trigger workflows from webhooks, configure the trigger in your workflow definition:
Stripe Example
{
"trigger": {
"type": "webhook",
"provider": "stripe",
"events": ["invoice.paid", "invoice.payment_failed"]
}
}HubSpot Example
{
"trigger": {
"type": "webhook",
"provider": "hubspot",
"events": ["contact.creation", "deal.propertyChange"]
}
}Custom Example
{
"trigger": {
"type": "webhook",
"provider": "custom",
"events": ["order-received"]
}
}Testing Webhooks
Using cURL
# Test Stripe webhook
curl -X POST "http://localhost:3000/webhooks/stripe/00000000-0000-0000-0000-000000000001" \
-H "Content-Type: application/json" \
-H "stripe-signature: t=1234567890,v1=abc123..." \
-d '{
"id": "evt_test",
"type": "invoice.paid",
"data": {
"object": {
"id": "in_test",
"amount_paid": 9900
}
}
}'
# Test custom webhook
curl -X POST "http://localhost:3000/webhooks/custom/00000000-0000-0000-0000-000000000001/test-event" \
-H "Content-Type: application/json" \
-d '{"message": "Hello from test"}'Stripe CLI
# Forward Stripe webhooks to local endpoint
stripe listen --forward-to localhost:3000/webhooks/stripe/00000000-0000-0000-0000-000000000001
# Trigger a test event
stripe trigger invoice.paidngrok for Local Development
# Start ngrok tunnel
ngrok http 3000
# Use the ngrok URL in provider webhook settings
# https://abc123.ngrok.io/webhooks/stripe/{companyId}Best Practices
Idempotency
Always design workflows to handle duplicate events:
- Providers may retry failed deliveries
- Use
eventIdto deduplicate - Make actions idempotent where possible
Quick Response
Webhook endpoints should respond quickly:
- Return
200immediately - Process workflows asynchronously
- Avoid long-running operations in webhook handler
Monitoring
Monitor webhook health:
- Track
signatureVerifiedstatus for security issues - Monitor
statusfor processing failures - Alert on high
ignoredrates
Security
- Always verify signatures when available
- Use HTTPS in production
- Store webhook secrets securely
- Rotate secrets periodically