Files
gh-anton-abyzov-specweave-p…/commands/webhook-setup.md
2025-11-29 17:57:01 +08:00

7.6 KiB

Payment Webhook Configuration

Generate secure webhook handlers for payment providers.

Task

You are a payment webhook security expert. Generate secure, production-ready webhook handlers.

Steps:

  1. Ask for Provider:

    • Stripe
    • PayPal
    • Square
    • Custom payment gateway
  2. Generate Webhook Endpoint (Stripe):

import crypto from 'crypto';
import express from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const app = express();

// CRITICAL: Use raw body for webhook signature verification
app.post(
  '/api/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

    let event;

    try {
      // Verify webhook signature
      event = stripe.webhooks.constructEvent(
        req.body,
        sig!,
        webhookSecret
      );
    } catch (err) {
      console.error(`Webhook signature verification failed: ${err.message}`);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // Handle event idempotently
    const eventId = event.id;
    const existingEvent = await db.webhookEvents.findUnique({
      where: { stripeEventId: eventId },
    });

    if (existingEvent) {
      console.log(`Duplicate webhook event: ${eventId}`);
      return res.status(200).json({ received: true });
    }

    // Store event (prevents duplicate processing)
    await db.webhookEvents.create({
      data: {
        stripeEventId: eventId,
        type: event.type,
        processedAt: new Date(),
      },
    });

    // Process event in background to return 200 quickly
    processWebhookEvent(event).catch((error) => {
      console.error(`Failed to process webhook: ${error.message}`);
      // Alert ops team
    });

    res.status(200).json({ received: true });
  }
);

async function processWebhookEvent(event: Stripe.Event) {
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;

    case 'customer.subscription.created':
    case 'customer.subscription.updated':
      await handleSubscriptionChange(event.data.object);
      break;

    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object);
      break;

    case 'invoice.paid':
      await handleInvoicePaid(event.data.object);
      break;

    case 'invoice.payment_failed':
      await handleInvoicePaymentFailed(event.data.object);
      break;

    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object);
      break;

    case 'payment_intent.payment_failed':
      await handlePaymentFailed(event.data.object);
      break;

    case 'charge.dispute.created':
      await handleDisputeCreated(event.data.object);
      break;

    case 'customer.created':
      await handleCustomerCreated(event.data.object);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
}
  1. Generate PayPal Webhook:
import crypto from 'crypto';

app.post('/api/webhooks/paypal', express.json(), async (req, res) => {
  const webhookId = process.env.PAYPAL_WEBHOOK_ID!;
  const webhookEvent = req.body;

  // Verify PayPal webhook signature
  const isValid = await verifyPayPalWebhook(req, webhookId);

  if (!isValid) {
    return res.status(400).send('Invalid webhook signature');
  }

  const eventType = webhookEvent.event_type;

  switch (eventType) {
    case 'PAYMENT.CAPTURE.COMPLETED':
      await handlePayPalPaymentCompleted(webhookEvent.resource);
      break;

    case 'BILLING.SUBSCRIPTION.CREATED':
      await handlePayPalSubscriptionCreated(webhookEvent.resource);
      break;

    case 'BILLING.SUBSCRIPTION.CANCELLED':
      await handlePayPalSubscriptionCancelled(webhookEvent.resource);
      break;

    default:
      console.log(`Unhandled PayPal event: ${eventType}`);
  }

  res.status(200).json({ received: true });
});

async function verifyPayPalWebhook(req, webhookId) {
  const transmissionId = req.headers['paypal-transmission-id'];
  const timestamp = req.headers['paypal-transmission-time'];
  const signature = req.headers['paypal-transmission-sig'];
  const certUrl = req.headers['paypal-cert-url'];

  const response = await fetch(
    `https://api.paypal.com/v1/notifications/verify-webhook-signature`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${await getPayPalAccessToken()}`,
      },
      body: JSON.stringify({
        transmission_id: transmissionId,
        transmission_time: timestamp,
        cert_url: certUrl,
        auth_algo: req.headers['paypal-auth-algo'],
        transmission_sig: signature,
        webhook_id: webhookId,
        webhook_event: req.body,
      }),
    }
  );

  const data = await response.json();
  return data.verification_status === 'SUCCESS';
}
  1. Generate Webhook Testing Script:
// test-webhook.ts
import { exec } from 'child_process';
import util from 'util';

const execPromise = util.promisify(exec);

async function testWebhook() {
  // Install Stripe CLI: brew install stripe/stripe-cli/stripe

  // Listen to webhooks
  const { stdout } = await execPromise('stripe listen --forward-to localhost:3000/api/webhooks/stripe');
  console.log(stdout);

  // Trigger test events
  await execPromise('stripe trigger payment_intent.succeeded');
  await execPromise('stripe trigger customer.subscription.created');
  await execPromise('stripe trigger invoice.payment_failed');
}

testWebhook();
  1. Generate Monitoring & Alerting:
// Monitor webhook failures
async function monitorWebhooks() {
  const failedEvents = await db.webhookEvents.findMany({
    where: {
      processed: false,
      createdAt: {
        lt: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
      },
    },
  });

  if (failedEvents.length > 0) {
    await sendAlert({
      type: 'webhook_failure',
      count: failedEvents.length,
      events: failedEvents.map((e) => e.type),
    });
  }
}

// Retry failed webhooks
async function retryFailedWebhooks() {
  const failedEvents = await db.webhookEvents.findMany({
    where: { processed: false },
    take: 10,
  });

  for (const event of failedEvents) {
    try {
      await processWebhookEvent(event.data);
      await db.webhookEvents.update({
        where: { id: event.id },
        data: { processed: true, processedAt: new Date() },
      });
    } catch (error) {
      console.error(`Retry failed for event ${event.id}: ${error.message}`);
    }
  }
}
  1. Generate Webhook Schema (Database):
CREATE TABLE webhook_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
  type VARCHAR(100) NOT NULL,
  data JSONB NOT NULL,
  processed BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW(),
  processed_at TIMESTAMP
);

CREATE INDEX idx_webhook_events_type ON webhook_events(type);
CREATE INDEX idx_webhook_events_processed ON webhook_events(processed);

Security Best Practices:

  • Verify webhook signatures (prevent spoofing)
  • Use raw request body for signature validation
  • Idempotency (track event IDs, prevent duplicate processing)
  • Return 200 immediately (process in background)
  • Retry logic for failures
  • Monitoring and alerting
  • HTTPS only (secure transmission)
  • IP whitelisting (optional but recommended)

Example Usage:

User: "Set up secure Stripe webhook handler"
Result: Complete webhook endpoint with signature verification, idempotency, and monitoring