Skip to main content

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:

EventDescriptionTriggered When
click_eventUser clicked a linkSomeone clicks your short link
install_eventApp installedUser installs your app (attributed to a link)
conversion_eventConversion trackedUser completes a conversion event in your app

Creating a Webhook

Via Dashboard

  1. Go to SettingsWebhooks
  2. Click "Create Webhook"
  3. 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
  4. Click "Create"
  5. 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

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
X-LinkForty-SignatureHMAC SHA-256 signaturesha256=a1b2c3...
X-LinkForty-EventEvent typeclick_event
X-LinkForty-Event-IDUnique event identifierevt_abc123
User-AgentLinkForty webhook agentLinkForty-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:

  1. Create HMAC using your webhook secret
  2. Hash the raw request body
  3. Compare with X-LinkForty-Signature header

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

  1. Go to SettingsWebhooks
  2. Select your webhook
  3. Click "Send Test Event"
  4. 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

  1. Install ngrok:

    npm install -g ngrok
  2. Start your local server:

    node webhook-server.js
    # Server running on http://localhost:3000
  3. Create ngrok tunnel:

    ngrok http 3000
  4. Copy the HTTPS URL:

    Forwarding  https://abc123.ngrok.io -> http://localhost:3000
  5. 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"]
    }'
  6. Trigger events by clicking your links

  7. 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

AttemptDelayTotal Time Elapsed
1Immediate0s
21 second1s
32 seconds3s
44 seconds (if retryCount > 3)7s
58 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:

  1. Webhook is active - Verify isActive: true
  2. Events subscribed - Ensure you've selected the right events
  3. URL is reachable - Must be public HTTPS
  4. Server is responding - Return 200 within timeout
  5. 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

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