Webhook Automation for Real-Time Integrations
Build powerful event-driven integrations with SnipLink webhooks. Automate workflows, sync data in real-time, and trigger actions instantly when links are created, clicked, or modified.
What You'll Build
This tutorial covers complete webhook integration from setup to production deployment, including security, error handling, and real-world automation scenarios.
Plan Requirement: Webhooks require a Pro plan or higher. Free tier accounts must use polling for updates.
Why Use Webhooks?
Webhooks enable real-time, event-driven architectures with significant advantages:
- Real-Time - Get notified instantly when events occur (no polling)
- Efficient - Reduce API calls by 99% compared to polling
- Scalable - Handle millions of events without rate limits
- Reliable - Automatic retries with exponential backoff
- Flexible - Subscribe only to events you need
Webhook vs Polling
// ❌ Polling (inefficient, slow, wastes resources)
async function pollForNewLinks() {
while (true) {
const links = await fetchLinks();
const newLinks = links.filter(isNew);
if (newLinks.length > 0) {
await processNewLinks(newLinks);
}
await sleep(60000); // Check every minute
}
}
// Problems: Delayed notifications, wasted API calls, rate limits
// ✅ Webhooks (instant, efficient, scalable)
async function handleWebhook(event) {
if (event.type === 'link.created') {
await processNewLink(event.data.link);
}
}
// Benefits: Instant notifications, zero polling, unlimited scaleStep 1: Create Webhook Endpoint
Basic Webhook Handler (Node.js/Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
const WEBHOOK_SECRET = process.env.SNIPLINK_WEBHOOK_SECRET;
// Parse JSON bodies
app.use(express.json());
// Verify webhook signature
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha256', secret);
const expectedSignature = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Webhook endpoint
app.post('/webhooks/sniplink', async (req, res) => {
const signature = req.headers['x-sniplink-signature'] as string;
const payload = JSON.stringify(req.body);
// Verify signature
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
console.log(`Received webhook: ${event.type}`);
// Acknowledge receipt immediately
res.status(200).json({ received: true });
// Process event asynchronously (don't block response)
processWebhookEvent(event).catch((error) => {
console.error('Webhook processing failed:', error);
});
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Next.js API Route
// app/api/webhooks/sniplink/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.SNIPLINK_WEBHOOK_SECRET!;
function verifySignature(payload: string, signature: string): boolean {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const expectedSignature = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
export async function POST(request: NextRequest) {
try {
const signature = request.headers.get('x-sniplink-signature');
const payload = await request.text();
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
const event = JSON.parse(payload);
// Process event (add to queue, database, etc.)
await handleWebhookEvent(event);
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
);
}
}
async function handleWebhookEvent(event: any) {
// Your business logic here
console.log(`Processing event: ${event.type}`);
}Cloudflare Worker
// Perfect for serverless webhook handling
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const signature = request.headers.get('x-sniplink-signature');
const payload = await request.text();
// Verify signature
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(env.WEBHOOK_SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
const expectedSig = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(payload)
);
const expectedHex = Array.from(new Uint8Array(expectedSig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
if (signature !== expectedHex) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(payload);
// Process event (send to queue, database, etc.)
await env.WEBHOOK_QUEUE.send(event);
return new Response('OK', { status: 200 });
},
};Step 2: Register Webhook with SnipLink
Create Webhook via API
const API_KEY = process.env.SNIPLINK_API_KEY;
const WORKSPACE_ID = 'your-workspace-id';
const API_BASE = 'https://api.sniplink.co/v1';
async function registerWebhook(
url: string,
events: string[],
secret?: string
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/webhooks`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
events,
secret, // For HMAC signature verification
enabled: true,
}),
}
);
const { data } = await response.json();
return data.webhook;
}
// Register webhook for link events
const webhook = await registerWebhook(
'https://yourapp.com/webhooks/sniplink',
[
'link.created',
'link.updated',
'link.deleted',
'link.clicked',
],
'your-secure-random-secret'
);
console.log(`Webhook ID: ${webhook.id}`);
console.log(`Webhook URL: ${webhook.url}`);
console.log(`Subscribed events: ${webhook.events.join(', ')}`);Available Events
link.created- New link createdlink.updated- Link modifiedlink.deleted- Link permanently deletedlink.clicked- Link clicked by userlink.expired- Link reached expiration datelink.max_clicks_reached- Link hit click limitqr.scanned- QR code scanned
Step 3: Handle Webhook Events
Event Handler Pattern
interface WebhookEvent {
id: string;
type: string;
timestamp: string;
workspaceId: string;
data: any;
}
// Event handler registry
const eventHandlers = {
'link.created': handleLinkCreated,
'link.updated': handleLinkUpdated,
'link.deleted': handleLinkDeleted,
'link.clicked': handleLinkClicked,
};
async function processWebhookEvent(event: WebhookEvent) {
const handler = eventHandlers[event.type];
if (!handler) {
console.warn(`No handler for event type: ${event.type}`);
return;
}
try {
await handler(event);
} catch (error) {
console.error(`Handler failed for ${event.type}:`, error);
throw error; // Re-throw for retry logic
}
}
// Individual event handlers
async function handleLinkCreated(event: WebhookEvent) {
const { link } = event.data;
console.log(`New link created: ${link.shortUrl}`);
// Your automation logic
await notifySlack(`New link: ${link.shortUrl}`);
await syncToCRM(link);
await generateQRCode(link.id);
}
async function handleLinkUpdated(event: WebhookEvent) {
const { link, changes } = event.data;
console.log(`Link updated: ${changes.join(', ')}`);
// Only act on specific changes
if (changes.includes('url')) {
await notifySlack(`⚠️ Destination URL changed for ${link.shortUrl}`);
await auditLog('link_url_changed', link);
}
}
async function handleLinkDeleted(event: WebhookEvent) {
const { link } = event.data;
console.log(`Link deleted: ${link.shortUrl}`);
// Archive before it's gone
await archiveLink(link);
await cleanupAssociatedData(link.id);
}
async function handleLinkClicked(event: WebhookEvent) {
const { link, click } = event.data;
console.log(`Link clicked: ${link.shortUrl} from ${click.country}`);
// Real-time analytics
await updateRealtimeDashboard(link.id, click);
// Conversion tracking
if (click.utmCampaign === 'high-value') {
await trackConversion(link, click);
}
}Real-World Automation Examples
1. Slack Notifications
async function notifySlack(message: string) {
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
await fetch(SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: message,
},
},
],
}),
});
}
async function handleLinkCreated(event: WebhookEvent) {
const { link } = event.data;
await notifySlack(`
:link: *New Short Link Created*
URL: <${link.shortUrl}|${link.shortUrl}>
Destination: ${link.url}
Title: ${link.title || 'Untitled'}
Tags: ${link.tags.join(', ') || 'None'}
Created: ${new Date(link.createdAt).toLocaleString()}
`);
}2. CRM Sync (Salesforce, HubSpot)
async function syncToCRM(link: any) {
// Find or create contact based on link metadata
const contactEmail = link.metadata?.email;
if (!contactEmail) return;
// Update contact in CRM
await fetch('https://api.hubspot.com/crm/v3/objects/contacts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HUBSPOT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
email: contactEmail,
last_sniplink_shared: link.shortUrl,
last_sniplink_date: link.createdAt,
total_links_shared: link.clickCount,
},
}),
});
}
async function handleLinkClicked(event: WebhookEvent) {
const { link, click } = event.data;
// Track engagement in CRM
if (link.metadata?.contactId) {
await updateCRMEngagement(link.metadata.contactId, {
action: 'link_clicked',
linkUrl: link.shortUrl,
clickedAt: click.timestamp,
location: `${click.city}, ${click.country}`,
device: click.device,
});
}
}3. Analytics Platform Sync
import Analytics from 'analytics';
import segmentPlugin from '@analytics/segment';
const analytics = Analytics({
app: 'sniplink-integration',
plugins: [
segmentPlugin({
writeKey: process.env.SEGMENT_WRITE_KEY,
}),
],
});
async function handleLinkClicked(event: WebhookEvent) {
const { link, click } = event.data;
// Send to Segment/Google Analytics
analytics.track('Link Clicked', {
linkId: link.id,
shortUrl: link.shortUrl,
destination: link.url,
slug: link.slug,
tags: link.tags,
country: click.country,
city: click.city,
device: click.device,
browser: click.browser,
referrer: click.referrer,
utmSource: click.utmSource,
utmMedium: click.utmMedium,
utmCampaign: click.utmCampaign,
});
// Also track as conversion if needed
if (link.tags.includes('conversion-tracked')) {
analytics.track('Conversion', {
linkId: link.id,
value: link.metadata?.conversionValue || 0,
});
}
}4. Automated QR Code Generation
async function handleLinkCreated(event: WebhookEvent) {
const { link } = event.data;
// Auto-generate QR code for links tagged 'print'
if (link.tags.includes('print')) {
const qrCode = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${link.id}/qr`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
format: 'png',
size: 1024,
foregroundColor: '#000000',
backgroundColor: '#ffffff',
}),
}
).then((r) => r.json());
// Upload to S3 for storage
await uploadToS3(`qr-codes/${link.id}.png`, qrCode.data.imageData);
// Notify team
await notifySlack(`QR code generated for ${link.shortUrl}`);
}
}5. Email Campaign Integration
async function handleLinkClicked(event: WebhookEvent) {
const { link, click } = event.data;
// Trigger follow-up email for high-intent clicks
if (
link.tags.includes('email-campaign') &&
!click.isBot &&
click.utmMedium === 'email'
) {
// Wait 1 hour, then send follow-up
await scheduleFollowUpEmail({
email: link.metadata.recipientEmail,
linkClicked: link.shortUrl,
clickedAt: click.timestamp,
location: `${click.city}, ${click.country}`,
device: click.device,
delayHours: 1,
});
}
}
async function scheduleFollowUpEmail(data: any) {
// Use background job queue (BullMQ, SQS, etc.)
await jobQueue.add(
'send-follow-up-email',
data,
{ delay: data.delayHours * 60 * 60 * 1000 }
);
}Production-Ready Implementation
Idempotency
Prevent duplicate processing of the same event:
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function processWebhookEvent(event: WebhookEvent) {
const eventKey = `webhook:processed:${event.id}`;
// Check if already processed
const alreadyProcessed = await redis.get(eventKey);
if (alreadyProcessed) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
try {
// Process event
const handler = eventHandlers[event.type];
await handler(event);
// Mark as processed (expire after 7 days)
await redis.setex(eventKey, 7 * 24 * 60 * 60, 'true');
} catch (error) {
console.error(`Processing failed for event ${event.id}:`, error);
throw error;
}
}Retry Logic with Exponential Backoff
async function processWithRetry(
event: WebhookEvent,
maxRetries = 3
) {
let attempt = 0;
while (attempt < maxRetries) {
try {
await processWebhookEvent(event);
return; // Success
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
console.error(`Failed after ${maxRetries} attempts:`, error);
// Send to dead letter queue
await deadLetterQueue.add(event);
throw error;
}
// Exponential backoff: 2^attempt seconds
const delayMs = Math.pow(2, attempt) * 1000;
console.log(`Retry attempt ${attempt} after ${delayMs}ms`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}Queue-Based Processing
import { Queue, Worker } from 'bullmq';
const webhookQueue = new Queue('sniplink-webhooks', {
connection: { host: 'localhost', port: 6379 },
});
// Add webhook events to queue
app.post('/webhooks/sniplink', async (req, res) => {
// Verify signature...
const event = req.body;
// Add to queue immediately
await webhookQueue.add('process-event', event, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
// Acknowledge immediately
res.status(200).json({ received: true });
});
// Process events from queue
const worker = new Worker(
'sniplink-webhooks',
async (job) => {
await processWebhookEvent(job.data);
},
{ connection: { host: 'localhost', port: 6379 } }
);
worker.on('failed', (job, error) => {
console.error(`Job ${job?.id} failed:`, error);
});Monitoring and Alerting
import * as Sentry from '@sentry/node';
async function processWebhookEvent(event: WebhookEvent) {
const transaction = Sentry.startTransaction({
op: 'webhook',
name: `Process ${event.type}`,
});
try {
await processWithRetry(event);
// Track success metrics
await metrics.increment('webhook.processed', {
type: event.type,
status: 'success',
});
} catch (error) {
// Track failure metrics
await metrics.increment('webhook.processed', {
type: event.type,
status: 'failed',
});
// Send to error tracking
Sentry.captureException(error, {
tags: {
eventType: event.type,
eventId: event.id,
},
});
throw error;
} finally {
transaction.finish();
}
}Testing Webhooks
Local Development with ngrok
# Install ngrok
brew install ngrok # macOS
# or download from https://ngrok.com
# Start your local server
npm run dev # Running on localhost:3000
# Expose to internet
ngrok http 3000
# Use the ngrok URL for webhook registration
# https://abc123.ngrok.io/webhooks/sniplinkTest Event Payloads
// Create test webhook events
async function sendTestEvent() {
const testEvent = {
id: 'evt_test_123',
type: 'link.created',
timestamp: new Date().toISOString(),
workspaceId: 'ws_test',
data: {
link: {
id: 'lnk_test_123',
url: 'https://example.com/test',
shortUrl: 'https://r.sniplink.co/test123',
slug: 'test123',
title: 'Test Link',
tags: ['test'],
createdAt: new Date().toISOString(),
},
},
};
// Send to your local endpoint
await fetch('http://localhost:3000/webhooks/sniplink', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sniplink-Signature': generateTestSignature(testEvent),
},
body: JSON.stringify(testEvent),
});
}Troubleshooting
Webhook Not Receiving Events
- Verify endpoint is publicly accessible (not localhost)
- Check webhook is enabled and subscribed to correct events
- Ensure endpoint returns 200 status within 5 seconds
- Check webhook logs in SnipLink dashboard
Signature Verification Fails
- Ensure you're using the raw request body (not parsed JSON)
- Verify webhook secret matches registration
- Check header name is exactly
x-sniplink-signature - Use timing-safe comparison to prevent timing attacks
Events Processing Too Slowly
- Respond with 200 immediately, process asynchronously
- Use background job queue (BullMQ, SQS) for processing
- Avoid blocking operations in webhook handler
- Scale webhook processing workers horizontally
Next Steps
Webhooks Guide →
Complete API reference for webhook management and security
Event Reference →
Detailed documentation of all webhook event types and payloads
Analytics Dashboard →
Build real-time dashboards using webhook click events
Bulk Operations →
Handle webhook events from bulk link creation workflows
Need Help with Webhooks?
Having trouble setting up webhook integrations? Loading contact information or check our troubleshooting guide.