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
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:
Plan Requirement
GeoRedir can send webhooks for the following events:
| Event | Triggered When |
|---|---|
| click | A visitor clicks one of your smart links |
| conversion | A conversion postback is received for a click |
| link.created | A new smart link is created |
| link.updated | A smart link's settings or rules are modified |
| link.deleted | A smart link is deleted |
| alert.triggered | An alert rule condition is met (e.g., click spike) |
Choose Your Events
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.
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'));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)If you don't want to build a custom endpoint, you can use:
HTTPS Required
https://yourserver.com/webhooks/georedir)GeoRedir will generate a webhook secret for this endpoint. Copy it — you'll need it to verify webhook signatures (see Signature Verification below).
Every webhook delivery sends a JSON payload with a consistent structure:
{
"event": "click",
"timestamp": "2026-02-25T14:30:00Z",
"data": { ... }
}{
"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": []
}
}{
"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"
}
}{
"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
}
}{
"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"
}
}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)
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
// ...
});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
$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
X-GeoRedir-Signature header before processing the payload.If your endpoint returns a non-2xx status code or times out (30-second timeout), GeoRedir retries the delivery with exponential backoff:
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1st retry | 1 minute | 1 minute |
| 2nd retry | 5 minutes | 6 minutes |
| 3rd retry | 30 minutes | 36 minutes |
| 4th retry | 2 hours | ~2.5 hours |
| 5th retry | 8 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
Every webhook delivery is logged so you can debug issues:
Each delivery log entry shows:
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 });
});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 });
});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}), 200curl -X POST https://yourserver.com/webhooks/georedir -d '' to make sure it returns a 200 response.link.created won't receive click events.crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hash_equals (PHP) to prevent timing attacks.X-GeoRedir-Signature header. This prevents spoofed events.click_id or conversion_id as an idempotency key to prevent double-processing.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