Guides/Webhooks

Webhooks & Event Notifications Guide

Set up webhooks to receive real-time notifications for clicks, conversions, and link events. HMAC-SHA256 signed payloads with automatic retry logic.

9 min read

What Are Webhooks?

Webhooks are HTTP callbacks that GeoRedir sends to your server when specific events occur — like a click, conversion, or link creation. Instead of polling the API for updates, your server receives real-time notifications the moment something happens.

Common use cases include:

  • Real-time Slack/Discord notifications when a conversion comes in
  • Syncing click data to your own database or data warehouse
  • Triggering automation in Zapier, Make.com, or n8n
  • Logging link changes for audit trails
  • Custom dashboards that update in real time

Plan Requirement

Webhooks are available on Pro, Business, and Scale plans. Free plan users can upgrade to access webhooks.

Supported Events

GeoRedir can send webhooks for the following events:

EventTriggered When
clickA visitor clicks one of your smart links
conversionA conversion postback is received for a click
link.createdA new smart link is created
link.updatedA smart link's settings or rules are modified
link.deletedA smart link is deleted
alert.triggeredAn alert rule condition is met (e.g., click spike)

Choose Your Events

You can subscribe to specific events per webhook endpoint. For example, send click and conversion events to your analytics pipeline, but only link.created and link.deleted to your Slack channel.

Step 1: Create a Webhook Endpoint

Your webhook endpoint is an HTTP URL on your server that accepts POST requests. GeoRedir sends JSON payloads to this URL whenever a subscribed event occurs.

Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/georedir', (req, res) => {
  const { event, data, timestamp } = req.body;

  console.log(`Received event: ${event}`);
  console.log('Data:', JSON.stringify(data, null, 2));

  // Process the event
  switch (event) {
    case 'click':
      // Handle click event
      break;
    case 'conversion':
      // Handle conversion event
      break;
    case 'link.created':
      // Handle link creation
      break;
  }

  // Return 200 to acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3001, () => console.log('Webhook server on port 3001'));

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/georedir', methods=['POST'])
def handle_webhook():
    payload = request.json
    event = payload.get('event')
    data = payload.get('data')

    print(f"Received event: {event}")

    if event == 'click':
        # Handle click
        pass
    elif event == 'conversion':
        # Handle conversion
        pass

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3001)

No-Code Options

If you don't want to build a custom endpoint, you can use:

  • Zapier Webhooks — Create a "Webhooks by Zapier" trigger with a "Catch Hook" action. Use the generated URL as your webhook endpoint.
  • Make.com (Integromat) — Create a "Custom Webhook" module. Copy the generated URL into GeoRedir.

HTTPS Required

Your webhook endpoint must use HTTPS. GeoRedir will not send webhooks to HTTP URLs. If you're developing locally, use a tunneling tool like ngrok to expose your local server with an HTTPS URL.

Step 2: Register the Webhook in GeoRedir

  1. Go to your GeoRedir dashboard → Webhooks (in the sidebar)
  2. Click Add Webhook
  3. Enter your Endpoint URL (e.g., https://yourserver.com/webhooks/georedir)
  4. Select the events you want to subscribe to
  5. Click Create Webhook

GeoRedir will generate a webhook secret for this endpoint. Copy it — you'll need it to verify webhook signatures (see Signature Verification below).

Step 3: Understand the Payload Format

Every webhook delivery sends a JSON payload with a consistent structure:

{
  "event": "click",
  "timestamp": "2026-02-25T14:30:00Z",
  "data": { ... }
}

Click Event Payload

{
  "event": "click",
  "timestamp": "2026-02-25T14:30:00Z",
  "data": {
    "click_id": "abc123def456",
    "link_id": 42,
    "link_slug": "my-offer",
    "destination_url": "https://offer.example.com/landing",
    "ip": "203.0.113.42",
    "country": "US",
    "region": "California",
    "city": "Los Angeles",
    "device": "mobile",
    "browser": "Chrome",
    "os": "Android",
    "referer": "https://facebook.com",
    "is_unique": true,
    "fraud_flags": []
  }
}

Conversion Event Payload

{
  "event": "conversion",
  "timestamp": "2026-02-25T15:45:00Z",
  "data": {
    "conversion_id": "conv_789xyz",
    "click_id": "abc123def456",
    "link_id": 42,
    "link_slug": "my-offer",
    "revenue": 50.00,
    "currency": "USD",
    "country": "US",
    "device": "mobile"
  }
}

Link Created Event Payload

{
  "event": "link.created",
  "timestamp": "2026-02-25T10:00:00Z",
  "data": {
    "link_id": 42,
    "name": "Summer Campaign",
    "slug": "summer-campaign",
    "is_active": true,
    "rules_count": 3
  }
}

Alert Triggered Event Payload

{
  "event": "alert.triggered",
  "timestamp": "2026-02-25T16:00:00Z",
  "data": {
    "alert_id": 5,
    "alert_name": "Click spike detected",
    "metric": "clicks",
    "condition": "greater_than",
    "threshold": 1000,
    "current_value": 1247,
    "link_id": 42,
    "link_slug": "my-offer"
  }
}

Signature Verification (HMAC-SHA256)

Every webhook delivery includes an X-GeoRedir-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the webhook came from GeoRedir and wasn't tampered with.

The signature is computed as:

HMAC-SHA256(webhook_secret, raw_request_body)

Node.js Verification

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your Express handler:
app.post('/webhooks/georedir', (req, res) => {
  const signature = req.headers['x-georedir-signature'];
  const rawBody = JSON.stringify(req.body);
  const secret = process.env.GEOREDIR_WEBHOOK_SECRET;

  if (!verifySignature(rawBody, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid — process the event
  // ...
});

Python Verification

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# In your Flask handler:
@app.route('/webhooks/georedir', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-GeoRedir-Signature', '')
    raw_body = request.get_data()
    secret = os.environ['GEOREDIR_WEBHOOK_SECRET']

    if not verify_signature(raw_body, signature, secret):
        return jsonify({'error': 'Invalid signature'}), 401

    # Signature valid — process the event
    # ...

PHP Verification

<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GEOREDIR_SIGNATURE'] ?? '';
$secret = getenv('GEOREDIR_WEBHOOK_SECRET');

$expected = hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Signature valid — process the event
$data = json_decode($payload, true);
// ...

Never Skip Verification

Without signature verification, anyone who discovers your webhook URL can send fake events. Always verify the X-GeoRedir-Signature header before processing the payload.

Retry Policy

If your endpoint returns a non-2xx status code or times out (30-second timeout), GeoRedir retries the delivery with exponential backoff:

AttemptDelay After FailureCumulative Time
1st retry1 minute1 minute
2nd retry5 minutes6 minutes
3rd retry30 minutes36 minutes
4th retry2 hours~2.5 hours
5th retry8 hours~10.5 hours

After 5 failed retries, the delivery is marked as failed and no more retries are attempted.

Auto-Disable After 10 Consecutive Failures

If a webhook endpoint fails 10 consecutive deliveries (across different events), GeoRedir automatically disables the webhook to prevent unnecessary load. You'll receive an email notification. Re-enable it from the Webhooks dashboard after fixing the issue.

Delivery Logs

Every webhook delivery is logged so you can debug issues:

  1. Go to Webhooks in the sidebar
  2. Click on a webhook endpoint
  3. View the Delivery Log tab

Each delivery log entry shows:

  • Event type and timestamp
  • Request payload (the JSON body sent)
  • Response status code from your server
  • Response body (first 1KB)
  • Delivery status — success, failed, or pending retry
  • Retry count and next retry time (if applicable)

Practical Recipes

Recipe 1: Slack Notifications for Conversions

Get a Slack message every time a conversion comes in:

app.post('/webhooks/georedir', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'conversion') {
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `💰 New conversion! Link: ${data.link_slug} | Revenue: $${data.revenue} | Country: ${data.country}`
      })
    });
  }

  res.status(200).json({ received: true });
});

Recipe 2: Google Sheets Sync

Log every click to a Google Sheet using the Sheets API:

app.post('/webhooks/georedir', async (req, res) => {
  const { event, data, timestamp } = req.body;

  if (event === 'click') {
    await sheets.spreadsheets.values.append({
      spreadsheetId: SHEET_ID,
      range: 'Clicks!A:H',
      valueInputOption: 'RAW',
      requestBody: {
        values: [[
          timestamp,
          data.click_id,
          data.link_slug,
          data.country,
          data.device,
          data.browser,
          data.is_unique ? 'Yes' : 'No',
          data.destination_url
        ]]
      }
    });
  }

  res.status(200).json({ received: true });
});

Recipe 3: Custom Database Sync

Store webhook events in your own PostgreSQL database:

import psycopg2
import json

@app.route('/webhooks/georedir', methods=['POST'])
def handle_webhook():
    payload = request.json
    event = payload['event']
    data = payload['data']
    timestamp = payload['timestamp']

    conn = psycopg2.connect(os.environ['DATABASE_URL'])
    cur = conn.cursor()
    cur.execute(
        """INSERT INTO webhook_events (event, data, received_at)
           VALUES (%s, %s, %s)""",
        (event, json.dumps(data), timestamp)
    )
    conn.commit()
    cur.close()
    conn.close()

    return jsonify({'received': True}), 200

Troubleshooting

Webhook not receiving events

  • Check the webhook is active. Go to Webhooks in the dashboard and verify the status shows "Active". It may have been auto-disabled after repeated failures.
  • Verify the URL is reachable. Test your endpoint with curl -X POST https://yourserver.com/webhooks/georedir -d '' to make sure it returns a 200 response.
  • Check event subscriptions. Make sure the webhook is subscribed to the events you expect. A webhook configured for link.created won't receive click events.
  • Check firewall rules. Your server must accept incoming POST requests from GeoRedir's IP addresses.

Signature verification failing

  • Use the raw request body. Compute the HMAC on the raw bytes, not a re-serialized version. Parsing and re-serializing JSON may change whitespace or key order, producing a different hash.
  • Check the secret. Make sure you're using the correct webhook secret (shown when the webhook was created). Each webhook endpoint has its own secret.
  • Use timing-safe comparison. Use crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hash_equals (PHP) to prevent timing attacks.

Webhook auto-disabled

  • Fix the underlying issue first. Check the delivery logs to see what error your server is returning (500, timeout, connection refused, etc.).
  • Re-enable the webhook. Once your endpoint is healthy, go to the webhook settings and click "Enable". The failure counter resets.

Best Practices

  • Return 200 quickly. Process webhook payloads asynchronously. Acknowledge receipt with a 200 status immediately, then process the event in a background job. GeoRedir has a 30-second timeout — slow processing may trigger unnecessary retries.
  • Always verify signatures. Never trust an incoming webhook without validating the X-GeoRedir-Signature header. This prevents spoofed events.
  • Handle duplicate deliveries. Due to retries, you may receive the same event more than once. Use the click_id or conversion_id as an idempotency key to prevent double-processing.
  • Monitor delivery logs. Check your webhook delivery logs periodically to catch failures early. Set up an alert for consecutive failures if you rely on webhooks for critical workflows.
  • Use separate endpoints for different purposes. Create one webhook for your analytics pipeline (click + conversion events) and another for Slack notifications (conversion + alert.triggered events). This keeps concerns separated.
  • Store the raw payload. When processing webhooks, save the raw JSON payload to your database before processing. This gives you an audit trail and makes it easy to replay events if your processing logic changes.

Ready to get started?

Create your first geo-targeted smart link in under 2 minutes. Free plan available, no credit card required.

Free plan included • No credit card required • Upgrade anytime