Webhook Handler — Agentuity Documentation

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.

typescriptsrc/api/webhooks/route.ts
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;

Slack Webhook Example

typescriptsrc/api/webhooks/route.ts
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');
});

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

See Also