Learn/Cookbook/Patterns
Webhook Handler
Handle incoming webhooks with signature verification and background processing
Process webhooks from external services (Stripe, GitHub, Slack) with proper signature verification and fast response times.
The Pattern
Webhooks require quick responses (usually under 3 seconds). Use waitUntil to acknowledge immediately and process in the background.
import { createRouter } from '@agentuity/runtime';
import crypto from 'crypto';
import paymentProcessor from '@agent/payment-processor';
import subscriptionHandler from '@agent/subscription-handler';
const router = createRouter();
router.post('/stripe', async (c) => {
// Get raw body for signature verification
const rawBody = await c.req.text();
const signature = c.req.header('stripe-signature');
// Verify signature
const secret = process.env.STRIPE_WEBHOOK_SECRET;
if (!secret || !verifyStripeSignature(rawBody, signature, secret)) {
c.var.logger.warn('Invalid webhook signature');
return c.text('Invalid signature', 401);
}
const event = JSON.parse(rawBody);
c.var.logger.info('Webhook received', { type: event.type });
// Process in background, respond immediately
c.waitUntil(async () => {
try {
switch (event.type) {
case 'payment_intent.succeeded':
await paymentProcessor.run({
paymentId: event.data.object.id,
amount: event.data.object.amount,
customerId: event.data.object.customer,
});
break;
case 'customer.subscription.updated':
await subscriptionHandler.run({
subscriptionId: event.data.object.id,
status: event.data.object.status,
});
break;
default:
c.var.logger.debug('Unhandled event type', { type: event.type });
}
} catch (error) {
c.var.logger.error('Webhook processing failed', { error, eventType: event.type });
// Store for retry
await c.var.kv.set('failed-webhooks', event.id, {
event,
error: String(error),
timestamp: Date.now(),
}, { ttl: 86400 }); // 24 hours
}
});
// Return 200 immediately
return c.json({ received: true });
});
function verifyStripeSignature(
payload: string,
signature: string | undefined,
secret: string
): boolean {
if (!signature) return false;
const parts = signature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
acc[key] = value;
return acc;
}, {} as Record<string, string>);
const timestamp = parts['t'];
const expectedSig = parts['v1'];
const signedPayload = `${timestamp}.${payload}`;
const computedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(computedSig)
);
}
export default router;Key Points
- Raw body first — Read body as text before parsing for signature verification
- Fast response — Return 200 immediately, process with
waitUntil - Error handling — Store failed webhooks for retry/debugging
- Signature verification — Always verify webhooks from external services
Slack Webhook Example
import slackHandler from '@agent/slack-handler';
router.post('/slack', async (c) => {
// Slack retries on failure - skip duplicates
if (c.req.header('x-slack-retry-num')) {
return c.text('OK');
}
const rawBody = await c.req.text();
// Verify Slack signature
const timestamp = c.req.header('x-slack-request-timestamp');
const signature = c.req.header('x-slack-signature');
const secret = process.env.SLACK_SIGNING_SECRET;
if (!verifySlackSignature(rawBody, timestamp, signature, secret)) {
return c.text('Invalid signature', 401);
}
const payload = JSON.parse(rawBody);
// Handle URL verification challenge
if (payload.type === 'url_verification') {
return c.text(payload.challenge);
}
// Process event in background
c.waitUntil(async () => {
await slackHandler.run(payload);
});
return c.text('OK');
});See Also
- HTTP Routes for route patterns
- Background Tasks for
waitUntil
Need Help?
Join our Community for assistance or just to hang with other humans building agents.
Send us an email at hi@agentuity.com if you'd like to get in touch.
Please Follow us on
If you haven't already, please Signup for your free account now and start building your first agent!