Webhooks
Receive real-time notifications when events occur in your LinkForty account via HTTP callbacks to your server.
Overview
Webhooks allow you to build integrations that subscribe to events in LinkForty. When an event occurs (like a link click or app install), LinkForty sends an HTTP POST request to your configured webhook URL with event data.
Benefits:
- ✅ Real-time - Get notified immediately when events happen
- ✅ Efficient - No need to poll APIs for updates
- ✅ Secure - HMAC signature verification ensures authenticity
- ✅ Reliable - Automatic retries with exponential backoff
- ✅ Flexible - Subscribe to specific events you care about
Event Types
LinkForty supports three event types:
| Event | Description | Triggered When |
|---|---|---|
click_event | User clicked a link | Someone clicks your short link |
install_event | App installed | User installs your app (attributed to a link) |
conversion_event | Conversion tracked | User completes a conversion event in your app |
Creating a Webhook
Via Dashboard
- Go to Settings → Webhooks
- Click "Create Webhook"
- Configure:
- Name: Descriptive name (e.g., "Slack Notifications")
- URL: Your endpoint URL (must be HTTPS)
- Events: Select which events to receive
- Custom Headers: Optional headers to include
- Click "Create"
- Save the secret - You'll need it to verify signatures
Via API
curl -X POST https://api.linkforty.com/api/webhooks \
-H "Authorization: Bearer $LINKFORTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Webhook",
"url": "https://api.yourapp.com/webhooks/linkforty",
"events": ["click_event", "install_event", "conversion_event"],
"retryCount": 3,
"timeoutMs": 10000,
"headers": {
"X-Custom-Header": "your-value"
}
}'
Response:
{
"id": "webhook_abc123",
"name": "Production Webhook",
"url": "https://api.yourapp.com/webhooks/linkforty",
"secret": "whsec_3f7a8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
"events": ["click_event", "install_event", "conversion_event"],
"isActive": true,
"retryCount": 3,
"timeoutMs": 10000,
"createdAt": "2024-03-15T10:30:00Z"
}
Important: Save the secret immediately - it won't be shown again (except in the webhook details view).
Webhook Payload
Every webhook delivery includes:
Headers
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
X-LinkForty-Signature | HMAC SHA-256 signature | sha256=a1b2c3... |
X-LinkForty-Event | Event type | click_event |
X-LinkForty-Event-ID | Unique event identifier | evt_abc123 |
User-Agent | LinkForty webhook agent | LinkForty-Webhook/1.0 |
Body
{
"event": "click_event",
"event_id": "evt_a1b2c3d4e5f6",
"timestamp": "2024-03-15T14:23:12Z",
"data": {
"id": "click_123",
"linkId": "link_abc",
"shortCode": "abc123",
"clickedAt": "2024-03-15T14:23:12Z",
"ipAddress": "192.168.1.1",
"deviceType": "mobile",
"platform": "iOS",
"browser": "Safari",
"countryCode": "US",
"city": "San Francisco",
"utmSource": "instagram",
"utmMedium": "social",
"utmCampaign": "spring-sale",
"referrer": "https://www.instagram.com/"
}
}
Event Payloads
Click Event
{
"event": "click_event",
"event_id": "evt_123",
"timestamp": "2024-03-15T14:23:12Z",
"data": {
"id": "click_id",
"linkId": "link_abc",
"shortCode": "abc123",
"clickedAt": "2024-03-15T14:23:12Z",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"deviceType": "mobile",
"platform": "iOS",
"browser": "Safari",
"countryCode": "US",
"city": "San Francisco",
"utmSource": "instagram",
"utmCampaign": "spring-sale"
}
}
Install Event
{
"event": "install_event",
"event_id": "evt_456",
"timestamp": "2024-03-15T15:45:30Z",
"data": {
"id": "install_id",
"linkId": "link_abc",
"shortCode": "abc123",
"installedAt": "2024-03-15T15:45:30Z",
"firstOpenAt": "2024-03-15T15:46:12Z",
"platform": "iOS",
"platformVersion": "17.0",
"confidenceScore": 0.85,
"attributionWindowHours": 168
}
}
Conversion Event
{
"event": "conversion_event",
"event_id": "evt_789",
"timestamp": "2024-03-15T16:20:00Z",
"data": {
"id": "conversion_id",
"linkId": "link_abc",
"eventName": "purchase",
"eventValue": 29.99,
"currency": "USD",
"properties": {
"productId": "prod_123",
"category": "electronics"
}
}
}
Verifying Webhook Signatures
Always verify webhook signatures to ensure requests are from LinkForty.
How It Works
LinkForty signs every webhook with HMAC SHA-256:
- Create HMAC using your webhook secret
- Hash the raw request body
- Compare with
X-LinkForty-Signatureheader
Node.js/TypeScript Example
import crypto from 'crypto';
import express from 'express';
const WEBHOOK_SECRET = 'whsec_your_secret_here';
app.post('/webhooks/linkforty', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-linkforty-signature'] as string;
const body = req.body.toString('utf8');
// Verify signature
if (!verifyWebhookSignature(body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Parse and handle event
const event = JSON.parse(body);
handleWebhookEvent(event);
res.status(200).send('OK');
});
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
// Remove 'sha256=' prefix
const receivedSignature = signature.replace('sha256=', '');
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
function handleWebhookEvent(event: any) {
console.log(`Received ${event.event}:`, event.data);
switch (event.event) {
case 'click_event':
handleClickEvent(event.data);
break;
case 'install_event':
handleInstallEvent(event.data);
break;
case 'conversion_event':
handleConversionEvent(event.data);
break;
default:
console.log('Unknown event type:', event.event);
}
}
Python Example
import hmac
import hashlib
import json
from flask import Flask, request, Response
WEBHOOK_SECRET = 'whsec_your_secret_here'
app = Flask(__name__)
@app.route('/webhooks/linkforty', methods=['POST'])
def webhook():
signature = request.headers.get('X-LinkForty-Signature', '')
body = request.get_data()
# Verify signature
if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
return Response('Invalid signature', status=401)
# Parse and handle event
event = json.loads(body)
handle_webhook_event(event)
return Response('OK', status=200)
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
# Remove 'sha256=' prefix
received_signature = signature.replace('sha256=', '')
# Compute expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(received_signature, expected_signature)
def handle_webhook_event(event):
print(f"Received {event['event']}: {event['data']}")
if event['event'] == 'click_event':
handle_click_event(event['data'])
elif event['event'] == 'install_event':
handle_install_event(event['data'])
elif event['event'] == 'conversion_event':
handle_conversion_event(event['data'])
Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strings"
)
const WebhookSecret = "whsec_your_secret_here"
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-LinkForty-Signature")
body, _ := io.ReadAll(r.Body)
// Verify signature
if !verifyWebhookSignature(body, signature, WebhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Parse and handle event
var event WebhookEvent
json.Unmarshal(body, &event)
handleWebhookEvent(event)
w.WriteHeader(http.StatusOK)
}
func verifyWebhookSignature(payload []byte, signature, secret string) bool {
// Remove 'sha256=' prefix
receivedSignature := strings.TrimPrefix(signature, "sha256=")
// Compute expected signature
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Constant-time comparison
return hmac.Equal([]byte(receivedSignature), []byte(expectedSignature))
}
type WebhookEvent struct {
Event string `json:"event"`
EventID string `json:"event_id"`
Timestamp string `json:"timestamp"`
Data map[string]interface{} `json:"data"`
}
func handleWebhookEvent(event WebhookEvent) {
switch event.Event {
case "click_event":
handleClickEvent(event.Data)
case "install_event":
handleInstallEvent(event.Data)
case "conversion_event":
handleConversionEvent(event.Data)
}
}
Testing Webhooks
Test from Dashboard
- Go to Settings → Webhooks
- Select your webhook
- Click "Send Test Event"
- Check your server logs
Test Payload:
{
"event": "click_event",
"event_id": "test_evt_123",
"timestamp": "2024-03-15T14:00:00Z",
"data": {
"id": "test_click_123",
"linkId": "test-link-id",
"clickedAt": "2024-03-15T14:00:00Z",
"deviceType": "test",
"platform": "test",
"test": true
}
}
Test Locally with ngrok
-
Install ngrok:
npm install -g ngrok -
Start your local server:
node webhook-server.js
# Server running on http://localhost:3000 -
Create ngrok tunnel:
ngrok http 3000 -
Copy the HTTPS URL:
Forwarding https://abc123.ngrok.io -> http://localhost:3000 -
Create webhook with ngrok URL:
curl -X POST https://api.linkforty.com/api/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Local Testing",
"url": "https://abc123.ngrok.io/webhooks/linkforty",
"events": ["click_event"]
}' -
Trigger events by clicking your links
-
See events in your terminal in real-time
Testing Script
// test-webhook.js
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 3000;
const SECRET = process.env.WEBHOOK_SECRET || 'your-secret-here';
app.post('/webhooks/linkforty', express.raw({ type: 'application/json' }), (req, res) => {
console.log('\n=== WEBHOOK RECEIVED ===');
console.log('Headers:', req.headers);
const signature = req.headers['x-linkforty-signature'];
const body = req.body.toString('utf8');
// Verify signature
const expectedSig = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
const receivedSig = signature.replace('sha256=', '');
console.log('\n=== SIGNATURE VERIFICATION ===');
console.log('Expected: ', expectedSig);
console.log('Received: ', receivedSig);
console.log('Valid: ', expectedSig === receivedSig);
if (expectedSig !== receivedSig) {
console.log('\n❌ INVALID SIGNATURE');
return res.status(401).send('Invalid signature');
}
// Parse event
const event = JSON.parse(body);
console.log('\n=== EVENT DATA ===');
console.log('Event: ', event.event);
console.log('Event ID: ', event.event_id);
console.log('Timestamp: ', event.timestamp);
console.log('Data: ', JSON.stringify(event.data, null, 2));
console.log('\n✅ WEBHOOK PROCESSED\n');
res.status(200).send('OK');
});
app.listen(PORT, () => {
console.log(`Webhook server listening on http://localhost:${PORT}`);
console.log(`Secret: ${SECRET}\n`);
});
Run it:
WEBHOOK_SECRET=your_secret_here node test-webhook.js
Retry Behavior
LinkForty automatically retries failed webhook deliveries with exponential backoff.
Retry Schedule
| Attempt | Delay | Total Time Elapsed |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1 second | 1s |
| 3 | 2 seconds | 3s |
| 4 | 4 seconds (if retryCount > 3) | 7s |
| 5 | 8 seconds (if retryCount > 4) | 15s |
Default: 3 retries (configurable 1-10)
When Retries Occur
Webhooks are retried if:
- HTTP status code ≠ 200
- Connection timeout (default: 10 seconds)
- Network error
- DNS resolution failure
When Retries Stop
Retries stop when:
- ✅ Webhook returns HTTP 200
- ❌ Maximum retry count reached
- ❌ Timeout exceeded on all attempts
Configuring Retries
curl -X POST https://api.linkforty.com/api/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Retry Example",
"url": "https://api.yourapp.com/webhooks",
"events": ["click_event"],
"retryCount": 5,
"timeoutMs": 15000
}'
Best Practices
1. Respond Quickly
Return 200 OK immediately, process asynchronously:
✅ Good:
app.post('/webhooks/linkforty', async (req, res) => {
// Verify signature
if (!verifySignature(req.body, req.headers['x-linkforty-signature'])) {
return res.status(401).send('Invalid signature');
}
// Respond immediately
res.status(200).send('OK');
// Process async (don't await)
processWebhookAsync(req.body).catch(console.error);
});
❌ Bad:
app.post('/webhooks/linkforty', async (req, res) => {
// Don't do slow operations before responding
await database.processEvent(req.body); // ❌ Slow
await sendEmail(req.body); // ❌ Slow
res.status(200).send('OK');
});
2. Idempotency
Handle duplicate events (use event_id):
async function handleWebhookEvent(event: WebhookEvent) {
// Check if already processed
const exists = await db.checkEventProcessed(event.event_id);
if (exists) {
console.log('Event already processed:', event.event_id);
return;
}
// Process event
await processEvent(event);
// Mark as processed
await db.markEventProcessed(event.event_id);
}
3. Use Queues for Processing
import { Queue } from 'bull';
const webhookQueue = new Queue('webhooks');
app.post('/webhooks/linkforty', (req, res) => {
// Verify signature
if (!verifySignature(req.body, req.headers['x-linkforty-signature'])) {
return res.status(401).send('Invalid signature');
}
// Add to queue
webhookQueue.add(req.body);
res.status(200).send('OK');
});
// Process queue
webhookQueue.process(async (job) => {
const event = job.data;
await handleWebhookEvent(event);
});
4. Log Everything
app.post('/webhooks/linkforty', (req, res) => {
const startTime = Date.now();
// Log incoming webhook
console.log({
type: 'webhook_received',
event_id: req.headers['x-linkforty-event-id'],
event_type: req.headers['x-linkforty-event'],
timestamp: new Date().toISOString(),
});
// Verify and process...
// Log completion
console.log({
type: 'webhook_processed',
event_id: req.headers['x-linkforty-event-id'],
duration_ms: Date.now() - startTime,
});
res.status(200).send('OK');
});
5. Monitor Failures
Check delivery logs regularly:
curl https://api.linkforty.com/api/webhooks/webhook_123/deliveries \
-H "Authorization: Bearer $API_KEY"
Use Cases
1. Slack Notifications
async function handleClickEvent(data: ClickEventData) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🔗 New click on ${data.shortCode} from ${data.city}, ${data.countryCode}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Link Clicked*\n• Short Code: \`${data.shortCode}\`\n• Location: ${data.city}, ${data.countryCode}\n• Device: ${data.deviceType} (${data.platform})\n• Campaign: ${data.utmCampaign || 'None'}`
}
}
]
})
});
}
2. CRM Integration
async function handleInstallEvent(data: InstallEventData) {
// Add to CRM
await crmClient.createLead({
source: 'linkforty',
campaign: data.utmCampaign,
platform: data.platform,
attributedTo: data.shortCode,
installedAt: data.installedAt,
});
}
3. Analytics Pipeline
async function handleConversionEvent(data: ConversionEventData) {
// Send to analytics
await analyticsClient.track({
userId: data.userId,
event: data.eventName,
properties: {
value: data.eventValue,
currency: data.currency,
linkId: data.linkId,
...data.properties,
},
});
}
Troubleshooting
Webhooks Not Receiving Events
Check:
- Webhook is active - Verify
isActive: true - Events subscribed - Ensure you've selected the right events
- URL is reachable - Must be public HTTPS
- Server is responding - Return 200 within timeout
- Firewall rules - Allow LinkForty IPs (if applicable)
Signature Verification Failing
Common Issues:
- Using wrong secret
- Modifying request body before verification
- Not using raw body (express requires
express.raw()) - Incorrect HMAC algorithm (must be SHA-256)
Debug:
console.log('Received Signature:', req.headers['x-linkforty-signature']);
console.log('Raw Body:', req.body.toString('utf8'));
console.log('Expected Signature:', crypto.createHmac('sha256', secret).update(req.body).digest('hex'));
Timeouts
Increase timeout:
curl -X PUT https://api.linkforty.com/api/webhooks/webhook_123 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "timeoutMs": 30000 }'
Or respond faster:
- Return 200 immediately
- Process asynchronously
- Use queues
API Reference
- Create Webhook - Create new webhook
- List Webhooks - Get all webhooks
- Test Webhook - Send test event
- Delivery Logs - View delivery history
Security
- ✅ Always verify signatures - Never trust unverified requests
- ✅ Use HTTPS - Webhooks must use HTTPS URLs
- ✅ Keep secrets safe - Store in environment variables
- ✅ Rotate secrets - If compromised, regenerate
- ✅ Whitelist IPs - Optionally restrict to LinkForty IPs
- ✅ Rate limit - Protect your endpoint from abuse
Next Steps
- Set up your first webhook
- Test with ngrok locally
- Integrate with your systems
- Monitor delivery logs