MCP Server Webhook Implementation: Complete Real-Time Events Guide
Real-time event processing is critical for modern ChatGPT applications. Whether you're handling payment confirmations from Stripe, SMS notifications from Twilio, or custom business events, webhooks enable your MCP server to react instantly without polling.
This guide shows you how to implement production-ready webhook receivers with signature verification, retry logic, idempotency handling, and queue-based processing. By the end, you'll have a secure, scalable webhook architecture ready for OpenAI approval.
What Are Webhooks and Why Do MCP Servers Need Them?
Webhooks are HTTP callbacks that external services send to your server when events occur. Instead of your server repeatedly checking for updates (polling), webhooks push data to you immediately when something happens.
Common MCP Server Webhook Use Cases
- Payment Events (Stripe): Process
checkout.session.completedto upgrade user plans - Communication Events (Twilio): Handle incoming SMS/voice calls and route to ChatGPT
- CRM Updates (Salesforce): Sync contact changes to your ChatGPT app database
- GitHub Events: Trigger CI/CD pipelines when code is pushed
- Custom Business Logic: Order confirmations, appointment cancellations, inventory updates
Webhooks vs Polling
| Approach | Latency | Server Load | Scalability | Cost |
|---|---|---|---|---|
| Webhooks | Instant (< 1s) | Low (event-driven) | High | Low |
| Polling | 30-60s delay | High (constant requests) | Poor | High |
For real-time ChatGPT applications, webhooks are the clear winner.
Building a Secure Webhook Receiver
A production webhook endpoint requires three critical components: signature verification, replay attack prevention, and idempotency handling.
Express.js Webhook Endpoint with Signature Verification
Stripe, Twilio, and most professional services sign webhook payloads using HMAC-SHA256. Never trust unsigned webhooks—attackers can forge requests if you don't verify signatures.
const express = require('express');
const crypto = require('crypto');
const app = express();
// CRITICAL: Use raw body for signature verification
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify signature (prevents forgery)
const payload = req.body;
const expectedSig = crypto
.createHmac('sha256', endpointSecret)
.update(payload)
.digest('hex');
const signatures = sig.split(',').reduce((acc, pair) => {
const [key, value] = pair.split('=');
acc[key] = value;
return acc;
}, {});
if (signatures.v1 !== expectedSig) {
console.error('⚠️ Invalid signature');
return res.status(401).send('Signature mismatch');
}
event = JSON.parse(payload);
// Prevent replay attacks (check timestamp)
const timestamp = parseInt(signatures.t, 10);
const tolerance = 300; // 5 minutes
if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) {
return res.status(400).send('Timestamp too old');
}
} catch (err) {
console.error('Webhook error:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Process event (more on this below)
await processWebhook(event);
// Always respond 200 immediately
res.status(200).send('Received');
}
);
Replay Attack Prevention
Malicious actors can capture legitimate webhook payloads and replay them. The timestamp validation (5-minute tolerance) prevents old webhooks from being reprocessed.
Best Practice: Store processed webhook IDs in Redis/Firestore for 24 hours and reject duplicates:
const processedWebhooks = new Set(); // Use Redis in production
async function processWebhook(event) {
const webhookId = event.id;
if (processedWebhooks.has(webhookId)) {
console.log('Duplicate webhook, ignoring');
return;
}
processedWebhooks.add(webhookId);
// Process event...
}
Idempotency Handling
Network failures mean webhook providers will retry failed requests multiple times. Your handler must be idempotent—processing the same event 10 times should have the same effect as processing it once.
// Idempotency middleware
async function idempotentHandler(req, res, next) {
const idempotencyKey = req.headers['idempotency-key'] || req.body.id;
// Check if already processed
const existing = await db.collection('processed_webhooks')
.doc(idempotencyKey)
.get();
if (existing.exists) {
console.log('Idempotent request, returning cached response');
return res.status(200).json(existing.data().response);
}
// Process and cache result
next();
}
app.post('/webhooks/stripe', idempotentHandler, async (req, res) => {
const result = await processPayment(req.body);
// Cache response for 24 hours
await db.collection('processed_webhooks').doc(req.body.id).set({
response: result,
timestamp: Date.now()
});
res.status(200).json(result);
});
Learn more about securing ChatGPT apps with proper authentication and validation.
Event Processing Architecture
Critical Rule: Webhook handlers should respond within 5 seconds. If processing takes longer (database writes, external API calls, email sending), use asynchronous processing.
Queue-Based Processing with BullMQ
const { Queue, Worker } = require('bullmq');
const Redis = require('ioredis');
const connection = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
maxRetriesPerRequest: null
});
// Create queue
const webhookQueue = new Queue('webhooks', { connection });
// Webhook endpoint (responds fast)
app.post('/webhooks/stripe', async (req, res) => {
// Verify signature (omitted for brevity)
// Add to queue
await webhookQueue.add('stripe.payment', req.body, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000 // Start with 2s, doubles each retry
},
removeOnComplete: 100,
removeOnFail: 500
});
// Respond immediately
res.status(200).send('Queued');
});
// Worker (processes async)
const worker = new Worker('webhooks', async (job) => {
const { type, data } = job.data;
console.log(`Processing ${type} webhook:`, data.id);
switch (type) {
case 'stripe.payment':
await handleStripePayment(data);
break;
case 'twilio.sms':
await handleTwilioSMS(data);
break;
default:
throw new Error(`Unknown webhook type: ${type}`);
}
}, { connection });
// Error handling
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err.message);
// Send to dead letter queue after max retries
if (job.attemptsMade >= 5) {
deadLetterQueue.add('failed-webhook', job.data);
}
});
Error Handling and Retry Logic
Exponential Backoff: Retry failed webhooks with increasing delays:
async function exponentialBackoff(fn, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
await exponentialBackoff(async () => {
await updateFirestore(event.userId, { plan: 'professional' });
});
Dead Letter Queues
After exhausting retries, move failed webhooks to a dead letter queue for manual investigation:
const deadLetterQueue = new Queue('dead-letter', { connection });
// Monitor dead letter queue
const dlqWorker = new Worker('dead-letter', async (job) => {
// Send alert to Slack/email
await sendAlert({
message: `Webhook failed after 5 retries: ${job.data.id}`,
data: job.data,
timestamp: Date.now()
});
// Log to monitoring service
console.error('DLQ:', job.data);
}, { connection });
Common Webhook Integrations
Stripe Payment Webhooks
async function handleStripePayment(event) {
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await db.collection('subscriptions').doc(session.metadata.userId).set({
plan: session.metadata.plan,
stripeCustomerId: session.customer,
status: 'active',
startDate: Date.now()
});
break;
case 'customer.subscription.deleted':
await db.collection('subscriptions').doc(event.data.object.id).update({
status: 'canceled',
canceledAt: Date.now()
});
break;
}
}
Learn more about Stripe integration.
Twilio SMS/Voice Webhooks
async function handleTwilioSMS(event) {
const { From, Body } = event;
// Store message
await db.collection('messages').add({
from: From,
body: Body,
timestamp: Date.now()
});
// Trigger ChatGPT response
const response = await sendToChatGPT(Body);
// Reply via Twilio
await twilioClient.messages.create({
to: From,
from: process.env.TWILIO_PHONE,
body: response
});
}
GitHub CI/CD Webhooks
async function handleGitHubPush(event) {
if (event.ref === 'refs/heads/main') {
// Trigger deployment
await deployToProduction({
commit: event.head_commit.id,
message: event.head_commit.message,
author: event.pusher.name
});
}
}
Custom Business Logic
async function handleOrderConfirmation(event) {
const { orderId, userId, items } = event;
// Update inventory
for (const item of items) {
await db.collection('inventory').doc(item.sku).update({
quantity: FieldValue.increment(-item.quantity)
});
}
// Send confirmation email
await sendEmail({
to: event.userEmail,
subject: `Order #${orderId} confirmed`,
template: 'order-confirmation',
data: { orderId, items }
});
}
Security Best Practices
IP Whitelist Validation
Many services (like Stripe) publish webhook IP ranges. Validate incoming requests:
const STRIPE_IPS = [
'3.18.12.63', '3.130.192.231', '13.235.14.237',
// ... (full list from Stripe docs)
];
function validateIP(req, res, next) {
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (!STRIPE_IPS.includes(clientIP)) {
console.warn(`Rejected webhook from ${clientIP}`);
return res.status(403).send('Forbidden');
}
next();
}
app.post('/webhooks/stripe', validateIP, webhookHandler);
Rate Limiting Webhook Endpoints
Prevent DDoS attacks with rate limiting:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many webhook requests',
standardHeaders: true,
legacyHeaders: false
});
app.post('/webhooks/*', webhookLimiter);
HTTPS Enforcement
Never accept webhooks over HTTP. Configure your server to reject non-HTTPS requests:
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.status(403).send('HTTPS required');
}
next();
});
For production deployments, use OAuth 2.1 with proper token validation.
Secret Rotation
Rotate webhook secrets every 90 days:
// Support old and new secrets during rotation period
const WEBHOOK_SECRETS = [
process.env.STRIPE_WEBHOOK_SECRET_NEW,
process.env.STRIPE_WEBHOOK_SECRET_OLD
];
function verifySignature(payload, signature) {
return WEBHOOK_SECRETS.some(secret => {
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === expectedSig;
});
}
Testing Webhook Implementations
Local Testing with Webhook.site
During development, use webhook.site to capture and inspect webhook payloads without deploying your server.
ngrok for Local Development
# Expose local server to internet
ngrok http 3000
# Copy HTTPS URL to webhook provider
https://abc123.ngrok.io/webhooks/stripe
Automated Testing
const request = require('supertest');
const crypto = require('crypto');
describe('Stripe Webhooks', () => {
it('should process payment webhooks', async () => {
const payload = JSON.stringify({
id: 'evt_test123',
type: 'checkout.session.completed',
data: { object: { id: 'cs_test', metadata: { userId: 'user123' } } }
});
const signature = crypto
.createHmac('sha256', process.env.STRIPE_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
const response = await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', `t=${Date.now()},v1=${signature}`)
.send(payload);
expect(response.status).toBe(200);
});
});
Production Checklist
Before going live, verify:
- Signature verification implemented for all webhook providers
- Replay attack prevention (timestamp + deduplication)
- Idempotency handling for retry scenarios
- Queue-based processing for long-running operations
- Exponential backoff retry logic (5 attempts minimum)
- Dead letter queue for failed webhooks
- IP whitelist validation (where supported)
- Rate limiting (100 requests/minute)
- HTTPS enforcement (reject HTTP)
- Monitoring and alerting (Datadog, Sentry)
- Secret rotation procedure (90-day cycle)
Next Steps
You now have a production-ready webhook architecture for your MCP server. Webhooks enable real-time reactivity—critical for ChatGPT apps that feel instant and responsive.
Recommended Reading:
- Complete MCP Server Development Guide - Master tool handlers and transport layers
- ChatGPT App Security Guide - Implement OAuth 2.1 and token validation
- SaaS Integration Patterns - Connect Stripe, Salesforce, and more
Ready to build your ChatGPT app with real-time webhook processing? Start with MakeAIHQ's no-code platform and deploy webhook-enabled MCP servers in 48 hours—no coding required.
External Resources: