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​