Skip to main content

Webhook Security

Ensure your webhook endpoints are secure and verify that requests actually come from tagd-ai.

Why Security Matters

Without verification:

  • Anyone could send fake events to your endpoint
  • Attackers could trigger unauthorized actions
  • Data could be compromised

Signature Verification

How It Works

  1. You provide a secret when creating the webhook
  2. tagd-ai signs every payload with your secret
  3. You verify the signature before processing
  4. Invalid signatures are rejected

Signature Header

Each webhook request includes:

X-tagd-ai-Signature: sha256=abc123...

The signature is an HMAC-SHA256 hash of the request body using your secret.

Verifying Signatures

Node.js

const crypto = require('crypto');

const WEBHOOK_SECRET = process.env.TAGD_WEBHOOK_SECRET;

function verifySignature(payload, signature) {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');

const provided = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(provided)
);
}

// Express middleware
function webhookMiddleware(req, res, next) {
const signature = req.headers['x-tagd-signature'];
const payload = JSON.stringify(req.body);

if (!signature) {
return res.status(401).send('Missing signature');
}

if (!verifySignature(payload, signature)) {
return res.status(401).send('Invalid signature');
}

next();
}

app.post('/webhook', webhookMiddleware, (req, res) => {
// Signature verified, safe to process
handleEvent(req.body);
res.sendStatus(200);
});

Python

import hmac
import hashlib
import os

WEBHOOK_SECRET = os.environ.get('TAGD_WEBHOOK_SECRET')

def verify_signature(payload, signature):
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()

return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-tagd-ai-Signature', '')

if not verify_signature(request.data, signature):
abort(401, 'Invalid signature')

# Signature verified, safe to process
handle_event(request.json)
return '', 200

Ruby

require 'openssl'

WEBHOOK_SECRET = ENV['TAGD_WEBHOOK_SECRET']

def verify_signature(payload, signature)
expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
'sha256',
WEBHOOK_SECRET,
payload
)

Rack::Utils.secure_compare(expected, signature)
end

post '/webhook' do
signature = request.env['HTTP_X_TAGD_SIGNATURE']
payload = request.body.read

halt 401, 'Invalid signature' unless verify_signature(payload, signature)

# Signature verified, safe to process
handle_event(JSON.parse(payload))
status 200
end

PHP

<?php
$webhookSecret = getenv('TAGD_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TAGD_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);

if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}

// Signature verified, safe to process
$event = json_decode($payload, true);
handleEvent($event);

http_response_code(200);

Secret Management

Generating a Strong Secret

# Generate random secret
openssl rand -hex 32

Or in your language:

const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');

Storing Secrets

DO:

  • Use environment variables
  • Use secret management services (AWS Secrets Manager, etc.)
  • Rotate secrets periodically

DON'T:

  • Hardcode in source code
  • Commit to version control
  • Log or display secrets

Rotating Secrets

  1. Generate new secret
  2. Update webhook with new secret
  3. Update your application
  4. Old secret stops working immediately

Additional Security Measures

IP Allowlisting

tagd-ai sends webhooks from known IPs:

52.xxx.xxx.xxx
54.xxx.xxx.xxx

Configure your firewall to only accept from these IPs.

To get current IPs:

curl https://api.tagd-ai.com/v1/webhooks/ips

HTTPS Only

  • Webhooks only sent to HTTPS endpoints
  • Ensures encryption in transit
  • Protects payload confidentiality

Request Timeouts

tagd-ai waits 30 seconds for response:

  • Respond quickly (< 5 seconds ideal)
  • Long processing = retries
  • Retries could cause duplicates

Idempotency

Process each event only once:

  1. Store processed event IDs
  2. Check before processing
  3. Skip if already processed
const processedEvents = new Set();

function handleWebhook(event) {
if (processedEvents.has(event.id)) {
console.log('Duplicate event, skipping');
return;
}

processedEvents.add(event.id);
processEvent(event);
}

Timestamp Validation

Prevent replay attacks:

function validateTimestamp(timestamp) {
const eventTime = new Date(timestamp);
const now = new Date();
const fiveMinutes = 5 * 60 * 1000;

return Math.abs(now - eventTime) < fiveMinutes;
}

app.post('/webhook', (req, res) => {
const { created_at } = req.body;

if (!validateTimestamp(created_at)) {
return res.status(400).send('Event too old');
}

// Process event
});

Common Vulnerabilities

Missing Signature Check

Vulnerable:

app.post('/webhook', (req, res) => {
// No signature verification!
handleEvent(req.body);
res.sendStatus(200);
});

Secure:

app.post('/webhook', verifySignature, (req, res) => {
handleEvent(req.body);
res.sendStatus(200);
});

Timing Attacks

Vulnerable:

if (signature === expected) { ... }

Secure:

if (crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) { ... }

Raw Body Parsing

Ensure you're verifying the raw body:

// Express: Use raw body for verification
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
const rawBody = req.body; // Buffer
const signature = req.headers['x-tagd-signature'];

if (!verifySignature(rawBody, signature)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(rawBody);
handleEvent(event);
res.sendStatus(200);
});

Monitoring and Alerts

Log All Requests

app.post('/webhook', (req, res) => {
console.log({
timestamp: new Date().toISOString(),
signature_valid: verifySignature(req.body, req.headers['x-tagd-signature']),
event_type: req.body.event,
event_id: req.body.id
});

// ... rest of handler
});

Alert on Failures

Notify when:

  • Multiple signature failures
  • Unknown event types
  • Processing errors

Next Steps