Troubleshooting Guide
Solutions to common issues when integrating with Sales Webhooks.
🔧 Quick Diagnostics
- • Check system status at status.saleswebhooks.com
- • Verify your API key is active in Console → Settings → API Keys
- • Test your webhook endpoint at webhook.site
- • Check webhook delivery logs in Console → Webhooks → Webhook Delivery History
Common Issues
Authentication Issues
🔴 Invalid API Key Error
Getting "Invalid API key" or 401 Unauthorized errors.
Symptoms:
{
"error": {
"type": "AUTHENTICATION_FAILED",
"message": "Invalid API key"
}
}
Solutions:
- Verify you're using the correct API key format:
lwa.sk_live_...
- Check you're not using a test key (
sk_test_
) in production - Ensure the API key hasn't been revoked
- Confirm you're setting the header correctly:
X-API-Key: your_key
Debug with cURL:
curl -i https://api.saleswebhooks.com/api/v1/account \
-H "X-API-Key: lwa.sk_live_your_key_here"
Webhook Delivery Issues
🔴 Webhooks Not Being Received
Your endpoint isn't receiving webhook events.
Checklist:
- Webhook configured? Check in Console → Settings → Webhooks
- Endpoint accessible? Must be HTTPS with valid SSL certificate
- Firewall rules? Whitelist Sales Webhooks IPs:
52.14.123.0/24
- Response time? Must respond within 10 seconds
- Response code? Must return 2xx status code
Test your endpoint:
# Send test webhook from Console
curl -X POST https://api.saleswebhooks.com/api/v1/webhooks/test \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json"
Common endpoint issues:
- Using HTTP instead of HTTPS
- Self-signed SSL certificates
- Basic auth blocking the request
- Content-Type validation rejecting JSON
🔴 Webhook Signature Verification Failing
Unable to verify webhook signatures.
Common causes:
- Wrong secret: Ensure using the secret from webhook configuration
- Body parsing: Use raw body, not parsed JSON
- Encoding issues: Ensure UTF-8 encoding
- Timing: Old webhooks may fail if verified too late
Correct implementation (Node.js):
// CORRECT: Use raw body
app.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 = `${timestamp}.${req.body}`;
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload, 'utf8')
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = JSON.parse(req.body);
res.status(200).send('OK');
});
Subscription Issues
🔴 LinkedIn Profile Not Found
Getting errors when trying to subscribe to profiles.
Error:
{
"error": {
"type": "LINKEDIN_ERROR",
"message": "LinkedIn profile is private or doesn't exist"
}
}
Solutions:
- Check URL format: Must be full URL like
https://linkedin.com/in/username
- Profile visibility: Profile must be public
- Regional restrictions: Some profiles may be region-locked
- URL changes: User may have changed their custom URL
Valid URL formats:
- ✅
https://linkedin.com/in/john-doe
- ✅
https://www.linkedin.com/in/john-doe
- ✅
https://linkedin.com/company/acme-corp
- ❌
john-doe
(missing domain) - ❌
linkedin.com/in/john-doe
(missing protocol)
🔴 Duplicate Subscription Error
Getting "subscription already exists" errors.
Solutions:
- Check existing: List subscriptions first to check if exists
- Use upsert pattern: Check then create if not exists
- Handle gracefully: Treat as success if already subscribed
Idempotent subscription pattern:
async function ensureSubscription(linkedinUrl) {
// Check if already subscribed
const existing = await client.subscriptions.list({
linkedin_url: linkedinUrl
});
if (existing.data.length > 0) {
console.log('Already subscribed');
return existing.data[0];
}
// Create new subscription
return await client.subscriptions.create({
entity_type: 'contact',
linkedin_url: linkedinUrl
});
}
Rate Limiting Issues
🔴 Rate Limit Exceeded
Getting 429 Too Many Requests errors.
Understanding rate limits:
- Daily limits: Based on your plan (1K-50K requests)
- Burst limits: 10-50 requests per second
- Concurrent limits: 5-25 simultaneous requests
Best practices:
- Check headers: Monitor
X-RateLimit-Remaining
- Implement backoff: Exponential backoff on 429s
- Batch operations: Use bulk endpoints when available
- Cache responses: Avoid redundant API calls
Rate limit handler:
async function apiCallWithRateLimit(fn) {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fn();
// Log remaining rate limit
const remaining = response.headers['x-ratelimit-remaining'];
console.log(`Rate limit remaining: ${remaining}`);
return response;
} catch (error) {
if (error.status === 429) {
const retryAfter = error.headers['retry-after'] || 60;
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
lastError = error;
} else {
throw error;
}
}
}
throw lastError;
}
Performance Issues
🔴 Slow API Response Times
API calls taking longer than expected.
Optimization strategies:
- Use pagination: Limit results to what you need
- Filter server-side: Use query parameters to filter
- Parallel requests: Make independent calls concurrently
- Regional endpoints: Use closest API region
Efficient data fetching:
// Inefficient: Fetching all then filtering
const allSubs = await client.subscriptions.list({ limit: 1000 });
const activeSubs = allSubs.data.filter(s => s.status === 'active');
// Efficient: Filter server-side
const activeSubs = await client.subscriptions.list({
status: 'active',
limit: 100
});
// Parallel fetching for multiple resources
const [contacts, companies] = await Promise.all([
client.subscriptions.list({ entity_type: 'contact' }),
client.subscriptions.list({ entity_type: 'company' })
]);
Debug Tools & Techniques
🛠️ Debugging Toolkit
1. Request Bin for Webhooks
Use webhook.site to inspect webhook payloads:
- Go to webhook.site
- Copy your unique URL
- Configure it as your webhook endpoint
- Trigger test webhooks to see full payloads
2. API Explorer
Test API calls directly in Console:
Console → API Explorer → Try endpoints with your real data
3. Webhook Logs
View delivery attempts and responses:
Console → Webhooks → Webhook Delivery History
4. cURL Commands
Test raw API calls:
# Test authentication
curl -i https://api.saleswebhooks.com/api/v1/account \
-H "X-API-Key: your_key"
# Test webhook endpoint
curl -X POST https://your-endpoint.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: test" \
-d '{"test": true}'
Still Need Help?
📧 Contact Support
If you're still experiencing issues, our support team is here to help.
When contacting support, include:
- Your account email
- Request IDs from error responses
- Complete error messages
- Code snippets (sanitized of secrets)
- Timeline of when issue started
Common Integration Patterns
Resilient Webhook Handler
// Production-ready webhook handler
class WebhookHandler {
constructor(secret) {
this.secret = secret;
this.processedEvents = new Set();
}
async handle(req, res) {
try {
// 1. Verify signature
if (!this.verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Parse event
const event = JSON.parse(req.body);
// 3. Check for duplicate
if (this.processedEvents.has(event.id)) {
return res.status(200).json({ status: 'already_processed' });
}
// 4. Return immediately
res.status(200).json({ status: 'accepted' });
// 5. Process async
setImmediate(() => {
this.processEvent(event).catch(error => {
console.error('Event processing failed:', error);
// Send to dead letter queue
this.deadLetterQueue.push(event);
});
});
} catch (error) {
console.error('Webhook handler error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
verifySignature(req) {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
// Prevent replay attacks (5 minute window)
const age = Date.now() - parseInt(timestamp);
if (age > 300000) return false;
const payload = `${timestamp}.${req.body}`;
const expected = crypto
.createHmac('sha256', this.secret)
.update(payload, 'utf8')
.digest('hex');
return signature === expected;
}
async processEvent(event) {
this.processedEvents.add(event.id);
// Process based on type
switch (event.type) {
case 'contact.position_changed':
await this.handleJobChange(event);
break;
// ... other event types
}
// Clean up old event IDs (keep last 10k)
if (this.processedEvents.size > 10000) {
const arr = Array.from(this.processedEvents);
this.processedEvents = new Set(arr.slice(-10000));
}
}
}
Next Steps
- Review best practices for production deployments
- Check error reference for specific error codes
- Join our Discord community for tips