Payment Webhook Configuration

Complete guide to configure payment webhooks for Stripe, Creem, and PayPal

Payment Webhook Configuration Guide

This guide explains how to configure payment webhooks for your application to receive real-time payment notifications.

Overview

The application has a unified webhook endpoint that supports multiple payment providers:

https://your-domain.com/api/payment/notify/[provider]

Supported Providers

  • Stripe: https://your-domain.com/api/payment/notify/stripe
  • Creem: https://your-domain.com/api/payment/notify/creem
  • PayPal: https://your-domain.com/api/payment/notify/paypal

Environment Variables

Configure the following variables in your .env file:

Stripe Configuration

STRIPE_SECRET_KEY="sk_test_xxx..."
STRIPE_PUBLISHABLE_KEY="pk_test_xxx..."
STRIPE_SIGNING_SECRET="whsec_xxx..."  # Webhook signing secret (required!)
STRIPE_ENABLED="true"

Creem Configuration

CREEM_API_KEY="xxx..."
CREEM_ENVIRONMENT="sandbox"  # or "production"
CREEM_SIGNING_SECRET="xxx..."  # Webhook signing secret
CREEM_ENABLED="true"

PayPal Configuration

PAYPAL_CLIENT_ID="xxx..."
PAYPAL_CLIENT_SECRET="xxx..."
PAYPAL_ENVIRONMENT="sandbox"  # or "production"
PAYPAL_ENABLED="true"

Default Provider

DEFAULT_PAYMENT_PROVIDER="stripe"  # stripe, creem, or paypal

Stripe Webhook Setup

Step 1: Access Stripe Dashboard

Visit Stripe Webhooks Dashboard

Step 2: Add Endpoint

  1. Click "Add endpoint"
  2. Enter Endpoint URL: https://your-domain.com/api/payment/notify/stripe
  3. Select the following events:

Required Events:

  • checkout.session.completed - Checkout completed
  • invoice.payment_succeeded - Payment succeeded (subscription renewal)
  • invoice.payment_failed - Payment failed
  • customer.subscription.updated - Subscription updated
  • customer.subscription.deleted - Subscription canceled

Step 3: Get Signing Secret

  1. After creating the endpoint, click "Reveal" to show the Signing Secret
  2. Copy the whsec_xxx... format key
  3. Add it to your .env file as STRIPE_SIGNING_SECRET

Step 4: Test Webhook

Using Stripe CLI to test locally:

# Install Stripe CLI (macOS)
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhook to local
stripe listen --forward-to localhost:3000/api/payment/notify/stripe

# Trigger test event (in another terminal)
stripe trigger checkout.session.completed
stripe trigger invoice.payment_succeeded
stripe trigger customer.subscription.updated

Webhook Events

The application handles the following webhook events:

Event TypeStripe EventDescription
CHECKOUT_SUCCESScheckout.session.completedHandles first payment/subscription creation
PAYMENT_SUCCESSinvoice.payment_succeededHandles subscription renewal payment
PAYMENT_FAILEDinvoice.payment_failedHandles payment failure
SUBSCRIBE_UPDATEDcustomer.subscription.updatedUpdates subscription information
SUBSCRIBE_CANCELEDcustomer.subscription.deletedHandles subscription cancellation

Webhook Flow

POST /api/payment/notify/stripe

Verify webhook signature (Signing Secret)

Parse event type

Handle based on event type:

  ┌─ CHECKOUT_SUCCESS
  │    ↓ Find order
  │    ↓ Update order status to SUCCESS
  │    ↓ Create credit transaction
  │    ↓ Create subscription record (if subscription)

  ┌─ PAYMENT_SUCCESS (subscription renewal)
  │    ↓ Find subscription
  │    ↓ Update subscription period
  │    ↓ Create renewal order
  │    ↓ Add renewal credits

  ┌─ SUBSCRIBE_UPDATED
  │    ↓ Find subscription
  │    ↓ Update subscription info

  └─ SUBSCRIBE_CANCELED
       ↓ Find subscription
       ↓ Update subscription status to CANCELED

Security Verification

The webhook endpoint uses signature verification to ensure security:

// Stripe example
async getPaymentEvent({ req }: { req: Request }): Promise<PaymentEvent> {
  const rawBody = await req.text();
  const signature = req.headers.get('stripe-signature') as string;

  if (!rawBody || !signature) {
    throw new Error('Invalid webhook request');
  }

  if (!this.configs.signingSecret) {
    throw new Error('Signing Secret not configured');
  }

  // Verify signature
  const event = this.client.webhooks.constructEvent(
    rawBody,
    signature,
    this.configs.signingSecret
  );

  // Process event...
}

Creating Payments with Metadata

When creating payments, you must set the order_no in metadata for webhooks to work correctly:

const paymentService = await getPaymentService();

const session = await paymentService.createPayment({
  order: {
    type: PaymentType.ONE_TIME,
    price: {
      amount: 1000, // in cents (10.00 USD)
      currency: 'usd',
    },
    description: 'Purchase credits',
    customer: {
      email: user.email,
      name: user.name,
    },
    successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/payment/success`,
    cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/payment/cancel`,

    // Important: Set metadata for webhook processing
    metadata: {
      order_no: orderNo, // Order number (required)
      user_id: userId, // User ID
      product_id: productId, // Product ID
    },
  },
});

Monitoring and Debugging

View Webhook Logs

# Production logs
tail -f /var/log/app/production.log | grep "payment notify"

# Or check Stripe Dashboard
# Webhooks → your endpoint → view logs

Common Issues

Issue 1: Webhook signature verification failed

Error: No signatures found matching the expected signature for payload
Solution: Check if STRIPE_SIGNING_SECRET is correctly configured

Issue 2: Order not found

Error: order not found
Cause: order_no in metadata doesn't exist
Check: Verify metadata.order_no is set when creating payment

Issue 3: Webhook timeout

Cause: Processing logic takes too long (Stripe requires response within 5 seconds)
Suggestion: Use background job queue for time-consuming operations

Configuration Checklist

Before deployment, verify:

  • ✅ Configured SIGNING_SECRET in .env file
  • ✅ Configured correct webhook URL in payment provider dashboard
  • ✅ Webhook URL is accessible from public internet (production)
  • ✅ Selected correct webhook event types
  • ✅ Tested at least one payment flow
  • ✅ Checked webhook logs for errors

Testing in Production

  1. Go to Stripe Dashboard → Webhooks → your endpoint
  2. Click "Send test webhook"
  3. Select event type and send test

Troubleshooting

If webhooks are not working:

  1. Check endpoint accessibility: Use curl to test

    curl -X POST https://your-domain.com/api/payment/notify/stripe
  2. Verify signing secret: Check it matches between Stripe and .env

  3. Check logs: Look for error messages in application logs

  4. Test locally: Use Stripe CLI to forward webhooks to local development

  • Webhook handler: src/app/api/payment/notify/[provider]/route.ts
  • Payment service: src/shared/services/payment.ts
  • Stripe provider: src/extensions/payment/stripe.ts
  • Payment types: src/extensions/payment/index.ts

Further Reading

Payment Webhook Configuration