Webhooks
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your CRM. Instead of polling the API for changes, webhooks push data to your specified URL automatically.
How Webhooks Work
- Configure: Create a webhook endpoint with your URL and select which events to subscribe to
- Trigger: When an event occurs (e.g., contact created), we queue a delivery
- Deliver: Our system sends an HTTP POST request to your URL with the event data
- Verify: Your server verifies the signature and processes the payload
- Retry: If delivery fails, we automatically retry with exponential backoff
Supported Events
Contact Events
| Event | Description |
|---|---|
contact.created | A new contact was created |
contact.updated | An existing contact was modified |
contact.deleted | A contact was soft-deleted |
Organization Events
| Event | Description |
|---|---|
organization.created | A new organization was created |
organization.updated | An existing organization was modified |
organization.deleted | An organization was soft-deleted |
Deal Events
| Event | Description |
|---|---|
deal.created | A new deal was created |
deal.updated | An existing deal was modified (non-stage change) |
deal.stage_changed | A deal moved to a different pipeline stage |
deal.deleted | A deal was soft-deleted |
Activity Events
| Event | Description |
|---|---|
activity.created | A new activity (call, email, meeting, note) was logged |
Task Events
| Event | Description |
|---|---|
task.created | A new task was created |
task.completed | A task was marked as complete |
Webhook Payload Format
All webhook payloads follow a consistent structure:
{ "id": "evt_550e8400-e29b-41d4-a716-446655440000", "event": "contact.created", "created_at": "2024-01-15T10:30:00.000Z", "api_version": "2025-12-01", "data": { "id": "123e4567-e89b-12d3-a456-426614174000", "first_name": "John", "last_name": "Doe", "job_title": "Purchasing Manager", "organization_id": "987e6543-e21b-12d3-a456-426614174999", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" }}Payload Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier |
event | string | Event type (e.g., contact.created) |
created_at | string | ISO 8601 timestamp of when event occurred |
api_version | string | API version used for this payload |
data | object | The full resource data at time of event |
HTTP Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
User-Agent | SellerCockpit-Webhook/1.0 |
X-Webhook-ID | Your webhook endpoint’s unique ID |
X-Webhook-Event | The event type (e.g., contact.created) |
X-Webhook-Timestamp | Unix timestamp when request was signed |
X-Webhook-Signature | HMAC-SHA256 signature for verification |
Custom Headers
You can configure additional custom headers when creating your webhook endpoint. These are useful for authentication tokens or routing information.
Security & Signature Verification
Every webhook request is signed using HMAC-SHA256. Always verify signatures to ensure requests are authentic.
Signature Format
X-Webhook-Signature: sha256=abc123def456...Verification Process
- Extract the timestamp from
X-Webhook-Timestampheader - Get the raw request body (do not parse it first)
- Construct the signed payload:
{timestamp}.{body} - Compute HMAC-SHA256 using your webhook secret (without
whsec_prefix) - Compare your computed signature with the
X-Webhook-Signatureheader
Example: Node.js Verification
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) { // Remove 'whsec_' prefix from secret const secretKey = secret.startsWith('whsec_') ? secret.slice(6) : secret;
// Construct signed payload const signedPayload = `${timestamp}.${payload}`;
// Compute expected signature const expectedSignature = crypto .createHmac('sha256', secretKey) .update(signedPayload) .digest('hex');
// Compare signatures (constant-time comparison) const receivedSig = signature.replace('sha256=', ''); return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(receivedSig) );}
// Express.js exampleapp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(payload); console.log('Received event:', event.event, event.data.id);
res.status(200).send('OK');});Example: Python Verification
import hmacimport hashlib
def verify_webhook_signature(payload: str, signature: str, timestamp: str, secret: str) -> bool: # Remove 'whsec_' prefix from secret secret_key = secret[6:] if secret.startswith('whsec_') else secret
# Construct signed payload signed_payload = f"{timestamp}.{payload}"
# Compute expected signature expected_signature = hmac.new( secret_key.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest()
# Compare signatures received_sig = signature.replace('sha256=', '') return hmac.compare_digest(expected_signature, received_sig)
# Flask example@app.route('/webhook', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET): return 'Invalid signature', 401
event = request.json print(f"Received event: {event['event']} - {event['data']['id']}")
return 'OK', 200Replay Attack Prevention
To prevent replay attacks, verify that the timestamp is recent (e.g., within 5 minutes):
const MAX_TIMESTAMP_AGE = 5 * 60 * 1000; // 5 minutes
function isTimestampValid(timestamp) { const timestampMs = parseInt(timestamp) * 1000; const now = Date.now(); return Math.abs(now - timestampMs) < MAX_TIMESTAMP_AGE;}Retry Policy
If your endpoint doesn’t respond with a 2xx status code within 30 seconds, we automatically retry using exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour |
| 5th retry | 4 hours |
After 5 failed attempts, the delivery is marked as permanently failed. You can manually retry failed deliveries from the webhook settings UI.
Best Practices for Your Endpoint
- Respond quickly: Return
200 OKimmediately, then process asynchronously - Be idempotent: Handle duplicate deliveries gracefully using the event
id - Use HTTPS: We only deliver to secure endpoints
- Log everything: Keep records for debugging delivery issues
Delivery Status
Each webhook delivery is tracked with one of these statuses:
| Status | Description |
|---|---|
pending | Queued for delivery |
success | Successfully delivered (2xx response) |
retrying | Delivery failed, scheduled for retry |
failed | All retry attempts exhausted |
View delivery history and retry failed deliveries from Settings > Developer > Webhooks.
Testing Webhooks
Send Test Event
Use the “Test” button in the webhook settings UI to send a sample event:
{ "id": "evt_test_123", "event": "test.webhook", "created_at": "2024-01-15T10:30:00.000Z", "api_version": "2025-12-01", "data": { "message": "This is a test webhook delivery" }}Local Development
For local development, use a tunneling service like:
- ngrok -
ngrok http 3000 - localtunnel -
lt --port 3000 - Cloudflare Tunnel
These create a public HTTPS URL that forwards to your local server.
Webhook Debugging Services
These services let you inspect webhook payloads:
- webhook.site - View and debug webhooks
- RequestBin - Collect and inspect HTTP requests
Webhook Limits
| Limit | Value |
|---|---|
| Webhooks per company | 25 |
| Events per webhook | Unlimited |
| Payload timeout | 30 seconds |
| Max retry attempts | 5 |
| Response body stored | First 10,000 characters |
Managing Webhooks
Creating a Webhook
- Go to Settings > Developer > Webhooks
- Click Create Webhook
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- Optionally add custom headers
- Copy and save your webhook secret
Updating a Webhook
- Edit the URL, events, or custom headers at any time
- Changes take effect immediately for new events
Regenerating Secret
If your webhook secret is compromised:
- Click Regenerate Secret on the webhook
- Update your server with the new secret
- The old secret is immediately invalidated
Deleting a Webhook
Deleting a webhook immediately stops all deliveries. Pending retries are cancelled.
API Reference
For programmatic webhook management via the REST API, see:
Integration Examples
n8n
- Create a Webhook node in n8n
- Copy the webhook URL
- Create a webhook in SellerCockpit with this URL
- Connect additional nodes to process the data
Zapier
- Create a new Zap with “Webhooks by Zapier” trigger
- Choose “Catch Hook”
- Copy the custom webhook URL
- Create a webhook in SellerCockpit
- Add actions to process incoming events
Make (Integromat)
- Create a new scenario with a Webhooks module
- Add a “Custom webhook” trigger
- Copy the webhook URL
- Configure your SellerCockpit webhook
- Add modules to handle the events
Troubleshooting
Webhook not receiving events
- Check webhook is Active in settings
- Verify the URL is correct and HTTPS
- Check your server is accessible from the internet
- Review delivery history for error details
Invalid signature errors
- Use the raw request body (don’t parse before verifying)
- Check you’re using the correct secret (without
whsec_prefix in computation) - Verify timestamp format (Unix seconds, not milliseconds)
- Ensure constant-time string comparison
Deliveries timing out
- Return
200 OKimmediately, process async - Increase server timeout if needed
- Check for network/firewall issues
Too many retries
- Ensure your endpoint returns
2xxstatus - Check for server errors in your logs
- Verify your endpoint can handle the payload size
Next Steps
- Create your first API key to manage webhooks via API
- Browse webhook endpoints for programmatic management
- View rate limits for API usage constraints