Outgoing Webhooks
Send real-time event notifications from Expedify to external systems when specific events occur in your CRM, workflows, or campaigns.
What are Outgoing Webhooks?
Outgoing webhooks allow Expedify to push data to your external systems automatically when events happen, such as:
- New contact created
- Deal stage changed
- Workflow execution completed
- Campaign email opened
- Task completed
Use Cases:
- Sync data to external CRM or database
- Trigger actions in other tools (Zapier, Make, n8n)
- Send notifications to Slack or Teams
- Update inventory systems
- Log events to analytics platforms
Creating an Outgoing Webhook
Step 1: Access Webhook Settings
- Go to Settings → Webhooks
- Click Outgoing Webhooks tab
- Click Create Webhook button
Step 2: Configure Webhook
Basic Information:
- Name: Descriptive name (e.g., "Slack New Contact Notification")
- URL: Endpoint URL where events will be sent
- Status: Active/Inactive
Event Selection:
- Choose which events trigger this webhook
- Multiple events can be selected
- Events organized by category
Authentication (Optional):
- API Key: Add authentication header
- Secret: Verify webhook signature
- Custom Headers: Additional headers
Retry Settings:
- Max Retries: Number of retry attempts (default: 3)
- Retry Delay: Time between retries (default: 5 seconds)
- Timeout: Request timeout (default: 30 seconds)
Step 3: Save and Test
- Click Save Webhook
- Click Test button to send test payload
- Verify endpoint receives test event
- Check response status
- Activate webhook
Available Events
CRM Events
Contacts
contact.created- New contact addedcontact.updated- Contact information changedcontact.deleted- Contact removedcontact.tagged- Tag added to contactcontact.assigned- Contact assigned to user
Companies
company.created- New company addedcompany.updated- Company information changedcompany.deleted- Company removedcompany.assigned- Company assigned to user
Deals
deal.created- New deal createddeal.updated- Deal information changeddeal.stage_changed- Deal moved to different stagedeal.won- Deal marked as wondeal.lost- Deal marked as lostdeal.deleted- Deal removed
Tasks
task.created- New task createdtask.updated- Task information changedtask.completed- Task marked as completetask.assigned- Task assigned to usertask.overdue- Task past due date
Workflow Events
workflow.started- Workflow execution beganworkflow.completed- Workflow execution finishedworkflow.failed- Workflow execution erroredworkflow.paused- Workflow execution pausedworkflow.cancelled- Workflow execution cancelled
Campaign Events
Email Campaigns
email.sent- Email sent to recipientemail.delivered- Email successfully deliveredemail.opened- Recipient opened emailemail.clicked- Recipient clicked linkemail.bounced- Email bouncedemail.complained- Marked as spam
WhatsApp Campaigns
whatsapp.sent- Message sentwhatsapp.delivered- Message deliveredwhatsapp.read- Message read by recipientwhatsapp.failed- Message failed to send
Form Events
form.submitted- Form submission receivedform.contact_created- Contact created from formform.lead_created- Lead created from form
Webhook Payload Format
Standard Payload Structure
{
"event": "contact.created",
"timestamp": "2024-01-15T10:30:00Z",
"organization_id": "org_abc123",
"data": {
"id": "contact_xyz789",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+1234567890",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "user_123"
},
"webhook_id": "webhook_456"
}
Payload Fields
Top Level:
event- Event type (e.g., "contact.created")timestamp- When event occurred (ISO 8601)organization_id- Your organization IDdata- Event-specific data objectwebhook_id- This webhook's ID
Data Object:
- Contains full record data
- Includes all fields and properties
- Nested objects for relationships
- Custom fields included
Example Payloads
Contact Created:
{
"event": "contact.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": "contact_xyz789",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@company.com",
"phone": "+1234567890",
"company_id": "company_abc123",
"owner": "user_456",
"tags": ["lead", "high-priority"],
"custom_fields": {
"industry": "Technology",
"budget": 50000
}
}
}
Deal Stage Changed:
{
"event": "deal.stage_changed",
"timestamp": "2024-01-15T11:00:00Z",
"data": {
"id": "deal_123",
"name": "Enterprise Deal",
"value": 100000,
"previous_stage": "Negotiation",
"current_stage": "Closed Won",
"contact_id": "contact_xyz789",
"company_id": "company_abc123",
"owner": "user_456"
}
}
Workflow Completed:
{
"event": "workflow.completed",
"timestamp": "2024-01-15T11:15:00Z",
"data": {
"workflow_id": "workflow_789",
"workflow_name": "Lead Nurture Sequence",
"execution_id": "exec_xyz123",
"status": "success",
"duration_ms": 15000,
"contact_id": "contact_xyz789",
"output": {
"email_sent": true,
"task_created": true
}
}
}
Security & Authentication
Webhook Signatures
Verify webhook authenticity using HMAC signature:
Signature Header:
X-Expedify-Signature: sha256=abc123...
Verification Process:
- Get signature from
X-Expedify-Signatureheader - Compute HMAC-SHA256 of payload using your webhook secret
- Compare computed signature with header signature
- Accept only if signatures match
Example Verification (Node.js):
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${computed}` === signature;
}
// In your webhook handler:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-expedify-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
res.status(200).send('OK');
});
Example Verification (Python):
import hmac
import hashlib
def verify_webhook(payload, signature, secret):
computed = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return f"sha256={computed}" == signature
# In your webhook handler:
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Expedify-Signature')
payload = request.get_data(as_text=True)
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Process webhook...
return 'OK', 200
Custom Headers
Add authentication headers:
- Authorization: Bearer tokens
- X-API-Key: API key authentication
- Custom: Any header your endpoint requires
Example:
Authorization: Bearer your-token-here
X-API-Key: your-api-key
X-Custom-Header: custom-value
Retry Logic
How Retries Work
When webhook fails:
- Expedify waits (default: 5 seconds)
- Retries sending webhook
- Repeats up to max retries (default: 3)
- Exponential backoff: 5s, 10s, 20s, 40s
- If all retries fail, marks webhook as failed
Success Criteria:
- HTTP status 200-299 returned
- Response received within timeout
- No network errors
Failure Triggers:
- HTTP status 400-599
- Request timeout
- Network error
- DNS resolution failure
Configuring Retries
Max Retries:
- Default: 3 attempts
- Range: 0-10 retries
- 0 = no retries
Retry Delay:
- Default: 5 seconds
- Exponential backoff applied
- Delay doubles each retry
Timeout:
- Default: 30 seconds
- Range: 5-120 seconds
- Includes connection + response time
Testing Webhooks
Using Test Button
- Find webhook in list
- Click Test button
- Test payload sent to endpoint
- View response status and body
- Check for errors
Test Payload:
{
"event": "test",
"timestamp": "2024-01-15T12:00:00Z",
"data": {
"message": "This is a test webhook from Expedify",
"webhook_id": "webhook_456"
}
}
Using Tools
RequestBin / Webhook.site:
- Go to webhook.site
- Copy unique URL
- Add as webhook URL
- Trigger event in Expedify
- View payload in RequestBin
ngrok (Local Testing):
# Start local server on port 3000
node server.js
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL in webhook settings
https://abc123.ngrok.io/webhook
Monitoring & Logs
Webhook Activity Log
View webhook execution history:
- Timestamp: When webhook fired
- Event: Event type
- Status: Success/Failed
- Response Code: HTTP status
- Response Time: Request duration
- Retry Count: Number of retries
- Error: Failure reason (if failed)
Filtering Logs
Filter by:
- Status: Success, Failed, Pending
- Event Type: Specific events
- Date Range: Time period
- Webhook: Specific webhook
Webhook Statistics
View webhook metrics:
- Total Sent: All webhook attempts
- Success Rate: % successful
- Average Response Time: Performance
- Failed Count: Number of failures
- Last Fired: Most recent execution
Managing Webhooks
Editing Webhooks
- Find webhook in list
- Click Edit button
- Update settings
- Click Save
- Changes apply immediately
Disabling Webhooks
Temporary Disable:
- Toggle webhook to Inactive
- No events sent while inactive
- Can reactivate anytime
- History preserved
Permanent Delete:
- Click Delete button
- Confirm deletion
- Webhook removed
- Cannot be undone
- History deleted
Webhook Limits
Free Plan:
- 5 webhooks maximum
- 1,000 webhook calls/month
Professional Plan:
- 25 webhooks maximum
- 10,000 webhook calls/month
Enterprise Plan:
- Unlimited webhooks
- Unlimited webhook calls
Common Integration Examples
Zapier Integration
// Zapier webhook catcher
// Trigger: Webhook by Zapier
// URL: Use webhook URL from Zapier
// In Expedify: Add Zapier webhook URL
Slack Notification
// Slack webhook format
{
"text": "New contact created: John Doe (john@example.com)"
}
// Expedify to Slack via middleware
app.post('/expedify-to-slack', (req, res) => {
const { event, data } = req.body;
if (event === 'contact.created') {
await fetch(SLACK_WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify({
text: `New contact: ${data.first_name} ${data.last_name} (${data.email})`
})
});
}
res.status(200).send('OK');
});
Database Sync
# Sync new contacts to your database
@app.route('/sync-contact', methods=['POST'])
def sync_contact():
data = request.json
if data['event'] == 'contact.created':
contact = data['data']
# Insert into your database
db.contacts.insert({
'expedify_id': contact['id'],
'name': f"{contact['first_name']} {contact['last_name']}",
'email': contact['email'],
'synced_at': datetime.now()
})
return 'OK', 200
Best Practices
-
Respond Quickly
- Acknowledge webhook with 200 OK
- Process asynchronously if slow
- Don't timeout Expedify's request
-
Verify Signatures
- Always validate webhook signatures
- Prevent unauthorized access
- Protectagainst replay attacks
-
Handle Duplicates
- Use
event_idfor deduplication - Idempotent processing
- Database unique constraints
- Use
-
Monitor Failures
- Check webhook logs regularly
- Set up failure alerts
- Investigate failed webhooks
-
Use HTTPS
- Always use secure endpoints
- Protect data in transit
- Required for production
Troubleshooting
Webhook not firing?
- Check webhook is Active
- Verify event is selected
- Test with Test button
- Check endpoint is accessible
Getting timeout errors?
- Respond faster (<30 seconds)
- Process asynchronously
- Increase timeout setting
- Check server capacity
Signature verification failing?
- Use correct secret
- Verify HMAC algorithm (SHA256)
- Check header name
- Ensure raw payload used
Too many retries?
- Fix endpoint errors
- Return 200-299 status
- Reduce processing time
- Check server uptime
Related Features
- Incoming Webhooks: Receive data from external systems
- API Keys: Alternative integration method
- Workflows: Automate based on events
Next Steps
- ✅ Create Webhook: Set up first outgoing webhook
- ✅ Select Events: Choose relevant event types
- ✅ Test Webhook: Verify endpoint receives events
- ✅ Add Security: Implement signature verification
- ✅ Monitor: Check webhook logs regularly
Need help? Contact support through Settings → Support.