Webhooks Guide
Webhooks Guide
Webhooks allow you to receive real-time notifications when events occur in Sure Send CRM. Instead of polling the API for changes, webhooks push data to your application as soon as something happens.
Overview
When an event occurs (like a new contact being created or a stage being updated), Sure Send CRM will send an HTTP POST request to the URL you specify, containing details about what happened.
Webhook Security
Security is critical for webhooks. Every webhook delivery includes a cryptographic signature that you must verify to ensure the request is legitimate.
Signature Verification
When you create a webhook, you receive a secret key that is used to sign all webhook deliveries. The secret key is only shown once during webhook creation - store it securely.
Every webhook delivery includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body. You must verify this signature to ensure the webhook is from Sure Send CRM and hasn't been tampered with.
Verification Examples
Ruby:
require 'openssl'
require 'rack/utils'
def verify_webhook(request, secret_key)
received_signature = request.headers['X-Webhook-Signature']
body = request.body.read
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, body)
# Use constant-time comparison to prevent timing attacks
Rack::Utils.secure_compare(computed_signature, received_signature)
endNode.js:
const crypto = require('crypto');
function verifyWebhook(req, secretKey) {
const receivedSignature = req.headers['x-webhook-signature'];
const body = JSON.stringify(req.body);
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(body)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(computedSignature)
);
}Python:
import hmac
import hashlib
def verify_webhook(request, secret_key):
received_signature = request.headers.get('X-Webhook-Signature')
body = request.body
computed_signature = hmac.new(
secret_key.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
# Use compare_digest to prevent timing attacks
return hmac.compare_digest(computed_signature, received_signature)PHP:
function verifyWebhook($request, $secretKey) {
$receivedSignature = $request->header('X-Webhook-Signature');
$body = $request->getContent();
$computedSignature = hash_hmac('sha256', $body, $secretKey);
// Use hash_equals to prevent timing attacks
return hash_equals($computedSignature, $receivedSignature);
}Creating a Webhook
Endpoint: POST /api/partner/webhooks
curl -X POST https://api.suresend.ai/api/partner/webhooks \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Contact Sync Webhook",
"url": "https://your-app.com/webhooks/crm-events",
"events": ["peopleCreated", "peopleUpdated", "peopleStageUpdated"]
}'Response:
{
"id": "webhook-uuid",
"name": "Contact Sync Webhook",
"url": "https://your-app.com/webhooks/crm-events",
"status": "active",
"events": ["peopleCreated", "peopleUpdated", "peopleStageUpdated"],
"secretKey": "your-secret-key-store-this-securely",
"systemIdentifier": "your-system",
"retryCount": 0,
"createdAt": "2025-10-16T12:00:00Z",
"updatedAt": "2025-10-16T12:00:00Z"
}⚠️ Important: The secretKey is only returned during webhook creation. Store it securely - you cannot retrieve it later.
Requirements
- Maximum 2 webhooks per event per system identifier
- URL must use HTTPS
- URL must return 200-299 status within 5 seconds
Available Events
Get the complete list of available events:
curl https://api.suresend.ai/api/partner/webhooks/events \
-H "Authorization: Bearer YOUR_API_TOKEN"Common Events
People (Contacts):
peopleCreated- New contact createdpeopleUpdated- Contact details updatedpeopleDeleted- Contact deletedpeopleTagsAdded- Tags added to contactpeopleTagsRemoved- Tags removed from contactpeopleStageUpdated- Contact pipeline stage changedpeopleAssigned- Contact assigned to user/teampeopleUnassigned- Contact unassigned
Companies:
companiesCreated- New company createdcompaniesUpdated- Company details updatedcompaniesDeleted- Company deleted
Communication:
emailsCreated- Email sent or receivedcallsCreated- Call loggedtextMessagesCreated- SMS sent or receivednotesCreated- Note added
Tasks & Activities:
tasksCreated- Task createdtasksCompleted- Task marked completeappointmentsCreated- Meeting/appointment scheduled
Webhook Payload Format
All webhooks are delivered with this standard format:
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventCreated": "2025-10-16T12:00:00Z",
"event": "peopleCreated",
"resourceIds": ["123e4567-e89b-12d3-a456-426614174000"],
"uri": "https://api.suresend.ai/api/partner/people/123e4567-e89b-12d3-a456-426614174000",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"stage": "Lead",
"createdAt": "2025-10-16T12:00:00Z"
}
}Fields:
eventId- Unique identifier for this event (use for idempotency)eventCreated- When the event occurredevent- Event type (matches your subscribed events)resourceIds- Array of affected resource IDsuri- Direct API link to fetch the full resourcedata- Event-specific payload data
Delivery Headers
POST /webhooks/crm-events HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-Webhook-Signature: abc123...
User-Agent: SureSendCRM-Webhook/1.0
Handling Webhooks
Basic Webhook Handler
const express = require('express');
const crypto = require('crypto');
const app = express();
// Important: Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/crm-events', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secretKey = process.env.WEBHOOK_SECRET_KEY;
// 1. Verify signature
const computedSig = crypto
.createHmac('sha256', secretKey)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computedSig))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Parse payload
const payload = JSON.parse(req.body.toString());
// 3. Check for duplicate (idempotency)
if (isDuplicateEvent(payload.eventId)) {
return res.status(200).json({ message: 'Already processed' });
}
// 4. Process the webhook
switch (payload.event) {
case 'peopleCreated':
handlePersonCreated(payload.data);
break;
case 'peopleUpdated':
handlePersonUpdated(payload.data);
break;
default:
console.log('Unhandled event:', payload.event);
}
// 5. Respond quickly (< 5 seconds)
res.status(200).json({ received: true });
// 6. Do heavy processing async
processWebhookAsync(payload);
});
app.listen(3000);Retry Logic
Sure Send CRM automatically retries failed webhook deliveries:
- Success: HTTP status 200-299 received within 5 seconds
- Failure: Any other status, timeout, or network error
- Retry Schedule: Exponential backoff (1min, 5min, 15min, 1hr, 6hr, 24hr)
- Max Retries: After 10 consecutive failures, webhook is automatically disabled
- Reset: A successful delivery resets the failure counter
Webhook Status:
active- Enabled and receiving deliveriesdisabled- Manually disabled (no deliveries)failed- Automatically disabled after 10 consecutive failures
Testing Webhooks
Test a Webhook Endpoint
curl -X POST https://api.suresend.ai/api/partner/webhooks/{webhook-id}/test \
-H "Authorization: Bearer YOUR_API_TOKEN"This sends a test payload:
{
"eventId": "test-event-uuid",
"eventCreated": "2025-10-16T12:00:00Z",
"event": "testEvent",
"resourceIds": ["test-resource-id"],
"uri": "https://api.suresend.ai/api/test",
"data": {
"message": "This is a test webhook from SureSendCRM"
}
}Local Testing with ngrok
For local development, use ngrok to expose your local server:
# Start your local server
node server.js
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL in your webhook
# https://abc123.ngrok.io/webhooks/crm-eventsManaging Webhooks
List All Webhooks
curl https://api.suresend.ai/api/partner/webhooks \
-H "Authorization: Bearer YOUR_API_TOKEN"Get Webhook Details
curl https://api.suresend.ai/api/partner/webhooks/{webhook-id} \
-H "Authorization: Bearer YOUR_API_TOKEN"Update a Webhook
curl -X PUT https://api.suresend.ai/api/partner/webhooks/{webhook-id} \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "disabled"
}'Delete a Webhook
curl -X DELETE https://api.suresend.ai/api/partner/webhooks/{webhook-id} \
-H "Authorization: Bearer YOUR_API_TOKEN"Best Practices
Security
- Always verify signatures - Never process webhooks without verification
- Use constant-time comparison - Prevents timing attacks
- Store secret keys securely - Use environment variables or secret management
- Use HTTPS only - Never expose webhook endpoints over HTTP
- Validate payload structure - Check data types and required fields
Reliability
- Respond quickly - Return 200 within 5 seconds, process async
- Implement idempotency - Use
eventIdto detect duplicates - Handle retries gracefully - Don't fail on duplicate events
- Log webhook deliveries - Keep audit trail for debugging
- Monitor webhook health - Track failure rates and alert on issues
Performance
- Queue heavy processing - Don't block webhook response
- Batch database operations - Reduce transaction overhead
- Rate limit your endpoint - Protect from abuse
- Scale horizontally - Load balance webhook handlers
Error Handling
app.post('/webhooks/crm-events', async (req, res) => {
try {
// Verify signature
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse payload
const payload = JSON.parse(req.body.toString());
// Check idempotency
if (await isDuplicate(payload.eventId)) {
return res.status(200).json({ message: 'Already processed' });
}
// Validate payload
if (!isValidPayload(payload)) {
console.error('Invalid payload:', payload);
return res.status(400).json({ error: 'Invalid payload' });
}
// Respond immediately
res.status(200).json({ received: true });
// Process async
await processWebhookAsync(payload);
} catch (error) {
console.error('Webhook error:', error);
// Still return 200 if we've already responded
if (!res.headersSent) {
res.status(500).json({ error: 'Internal error' });
}
}
});Troubleshooting
Webhook Not Receiving Events
- ✅ Check webhook status is
active - ✅ Verify events are subscribed in webhook configuration
- ✅ Ensure URL is accessible from internet (not localhost)
- ✅ Check firewall rules allow incoming connections
- ✅ Verify SSL certificate is valid
Signature Verification Failing
- ✅ Use the correct secret key (from webhook creation)
- ✅ Verify raw request body (don't parse before verifying)
- ✅ Use constant-time comparison functions
- ✅ Check header name is exactly
X-Webhook-Signature
Webhook Disabled After Failures
- ✅ Check your endpoint responds within 5 seconds
- ✅ Return 200-299 status code on success
- ✅ Fix any errors causing 500 responses
- ✅ Re-enable webhook after fixing issues
Testing Issues
- ✅ Use ngrok for local testing
- ✅ Check webhook test endpoint (
/webhooks/{id}/test) - ✅ Verify secret key is stored correctly
- ✅ Check application logs for errors
Example: Full Webhook Integration
// server.js
const express = require('express');
const crypto = require('crypto');
const { processContact, updateContact } = require('./services/contacts');
const { trackEvent } = require('./services/events');
const app = express();
app.use(express.raw({ type: 'application/json' }));
// Store processed event IDs (use Redis in production)
const processedEvents = new Set();
function verifyWebhook(req, secretKey) {
const signature = req.headers['x-webhook-signature'];
const body = req.body;
const computed = crypto
.createHmac('sha256', secretKey)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed)
);
}
app.post('/webhooks/crm', async (req, res) => {
const secretKey = process.env.WEBHOOK_SECRET_KEY;
// Verify signature
if (!verifyWebhook(req, secretKey)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse payload
const payload = JSON.parse(req.body.toString());
// Check idempotency
if (processedEvents.has(payload.eventId)) {
console.log('Duplicate event:', payload.eventId);
return res.status(200).json({ message: 'Already processed' });
}
// Mark as processed
processedEvents.add(payload.eventId);
// Respond quickly
res.status(200).json({ received: true });
// Process async
try {
switch (payload.event) {
case 'peopleCreated':
await processContact(payload.data);
await trackEvent('contact_created', payload.data);
break;
case 'peopleUpdated':
await updateContact(payload.data);
await trackEvent('contact_updated', payload.data);
break;
case 'peopleStageUpdated':
await handleStageChange(payload.data);
await trackEvent('stage_changed', payload.data);
break;
default:
console.log('Unhandled event:', payload.event);
}
} catch (error) {
console.error('Error processing webhook:', error);
// Don't throw - we already responded with 200
}
});
app.listen(process.env.PORT || 3000, () => {
console.log('Webhook server running');
});Next Steps
- Explore Events API to understand event data
- Learn about People API for contact management
- Check Rate Limiting for API limits
- Review Security Best Practices for API keys
Need Help?
- API Reference: See full webhook endpoints in sidebar
- Support: [email protected]
- Examples: Check our GitHub examples (coming soon)
Updated 9 days ago
