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 trackedAn in-app event is tracked via the SDK (attributed installs only)

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.

A webhook belongs to the workspace, not to the person who created it: it receives events for every link in the workspace, whoever created the link and whether it was made in the dashboard, through the API, or by an SDK. If two webhooks in the same workspace point at the same URL, that URL receives each event twice.

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​

Every delivery has the same envelope; data depends on the event.

{
"event": "click_event",
"event_id": "0b9d7c1e-5f3a-4c2b-9a8e-1d2c3b4a5f60",
"timestamp": "2026-03-15T14:23:12.000Z",
"data": { }
}

Event Payloads​

Click Event​

{
"event": "click_event",
"event_id": "0b9d7c1e-5f3a-4c2b-9a8e-1d2c3b4a5f60",
"timestamp": "2026-03-15T14:23:12.000Z",
"data": {
"id": "0b9d7c1e-5f3a-4c2b-9a8e-1d2c3b4a5f60",
"linkId": "6a1f0c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"clickedAt": "2026-03-15T14:23:12.000Z",
"ipAddress": "203.0.113.24",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) ...",
"deviceType": "ios",
"platform": "iOS",
"countryCode": "US",
"countryName": "United States",
"region": "California",
"city": "San Francisco",
"latitude": 37.7749,
"longitude": -122.4194,
"timezone": "America/Los_Angeles",
"utmSource": "instagram",
"utmMedium": "social",
"utmCampaign": "spring-sale",
"referrer": "https://www.instagram.com/"
}
}

deviceType is ios, android or web.

Install Event​

Sent when a new install is attributed to a link. deepLinkData is the same object the SDK receives on first launch, including any query parameters the click carried in customParameters.

{
"event": "install_event",
"event_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"timestamp": "2026-03-15T15:45:30.000Z",
"data": {
"id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"linkId": "6a1f0c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"fingerprintHash": "3f2a…",
"confidenceScore": 90,
"installedAt": "2026-03-15T15:45:30.000Z",
"deepLinkData": {
"shortCode": "spring24",
"deepLinkPath": "/products/42",
"customParameters": { "promo": "spring", "fbclid": "IwAR..." },
"isDeferred": true
},
"ipAddress": "203.0.113.24",
"userAgent": "MyApp/1.0 iOS/18.0",
"platform": "iOS"
}
}

confidenceScore runs from 0 to 100.

Conversion Event and SDK Event​

Both are sent for in-app events tracked through the SDK on an install that was attributed to a link, with the same data:

{
"event": "conversion_event",
"event_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"timestamp": "2026-03-15T16:20:00.000Z",
"data": {
"eventId": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"installId": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"linkId": "6a1f0c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"eventName": "purchase",
"eventData": { "productId": "prod_123", "revenue": 29.99, "currency": "USD" },
"timestamp": "2026-03-15T16:20:00.000Z"
}
}

Events from installs that weren't attributed to a link aren't sent to webhooks.

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 Settings → Webhooks
  2. Select your webhook
  3. Click "Send Test Event"
  4. Check your server logs, or the webhook's delivery history

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.

Delivery History​

Every delivery attempt is recorded, including test events and retries. Open Settings → Webhooks and choose View deliveries on a webhook to see each attempt: the event, the HTTP status your endpoint returned (or the error, if it could not be reached), and the attempt number. The history is kept for 30 days.

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. Delivery history - View deliveries on the webhook shows whether LinkForty sent the event and what your endpoint answered
  2. Webhook is active - Verify isActive: true
  3. Events subscribed - Ensure you've selected the right events
  4. URL is reachable - Must be public HTTPS
  5. Server is responding - Return 200 within timeout
  6. 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