Skip to main content

Webhooks

Receive real-time notifications when events occur in your LinkForty account via HTTP callbacks to your server.

Configured in the dashboard

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:

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 (attributed installs only)
sdk_eventSDK event trackedAny 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:

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

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"
}
}
}

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 vs conversion_event

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:

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

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

  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('\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

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

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:

  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 the timeout for the webhook in Settings → Webhooks, or respond faster from your endpoint:

  • Return 200 immediately
  • 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