Webhooks
Receive real-time notifications when events occur in your LinkForty account via HTTP callbacks to your server.
Webhooks are created and managed in the dashboard under Settings → Webhooks. Your application's role is to receive deliveries at the URL you register and verify their signatures, as shown below. (API keys are scoped to links, analytics, templates, and projects — see Authentication.)
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 four 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 (attributed installs only) |
sdk_event | SDK event tracked | Any in-app event is tracked via the SDK (all events regardless of attribution) |
Creating a Webhook
Create and manage webhooks in the dashboard under 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 —
click_event,install_event,conversion_event,sdk_event - Custom Headers: Optional headers to include on each delivery
- Retries and timeout: How aggressively to retry failed deliveries
- Click "Create"
- Save the signing secret (
whsec_…) — you'll need it to verify signatures. It's also available later in the webhook's 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"
}
}
}
SDK Event
{
"event": "sdk_event",
"event_id": "evt_012",
"timestamp": "2024-03-15T17:10:00Z",
"data": {
"eventId": "evt_012",
"installId": "install_abc",
"eventName": "purchase",
"eventData": {
"productId": "prod_123",
"revenue": 29.99,
"currency": "USD"
},
"timestamp": "2024-03-15T17:10:00Z"
}
}
sdk_event fires for every in-app event tracked via the SDK, regardless of whether the install is attributed to a link. conversion_event only fires for events from attributed installs (where the install was matched to a link click). If you want visibility into all SDK-tracked events, subscribe to sdk_event.
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;
case 'sdk_event':
handleSdkEvent(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'])
elif event['event'] == 'sdk_event':
handle_sdk_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)
case "sdk_event":
handleSdkEvent(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 a webhook with the ngrok URL: in Settings → Webhooks, create a webhook pointing at your HTTPS ngrok URL (e.g.
https://abc123.ngrok.io/webhooks/linkforty) and subscribe it to the events you want to test. -
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('\nINVALID 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('\nWEBHOOK 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
Set the retry count and timeout on each webhook in Settings → Webhooks when you create or edit it.
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 each webhook's delivery log in Settings → Webhooks regularly to catch endpoints that are timing out or returning errors, and use the dashboard's "Send test event" to confirm a fix.
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 the timeout for the webhook in Settings → Webhooks, or respond faster from your endpoint:
- Return
200immediately - Process the payload asynchronously
- Use a queue
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