Skip to main content

Stripe Webhook Deep Dive

This page covers webhook event handling, signature verification, supported event types, email notifications, and error handling patterns.

Overview

The Stripe webhook endpoint processes incoming events from Stripe, verifies their authenticity via signature verification, maps them to internal event types, and dispatches them to specialized handlers. Each handler updates the database via WebhookSubscriptionService and sends transactional emails.

Route Table

MethodPathAuthDescription
POST/api/stripe/webhookStripe signatureProcess incoming Stripe webhook events

Signature Verification

Every incoming webhook must include a stripe-signature header. The provider verifies it using Stripe's constructEvent method:

const event = this.stripe.webhooks.constructEvent(
payload,
signature,
this.webhookSecret
);

If the signature is missing, the endpoint returns 400:

{ "error": "No signature provided" }

If the signature is invalid, the constructEvent call throws and the endpoint returns:

{ "error": "Webhook processing failed" }

Event Type Mapping

Stripe event types are mapped to internal WebhookEventType values:

Stripe EventInternal TypeHandler
customer.subscription.createdSUBSCRIPTION_CREATEDhandleSubscriptionCreated
customer.subscription.updatedSUBSCRIPTION_UPDATEDhandleSubscriptionUpdated
customer.subscription.deletedSUBSCRIPTION_CANCELLEDhandleSubscriptionCancelled
invoice.payment_succeededSUBSCRIPTION_PAYMENT_SUCCEEDEDhandleSubscriptionPaymentSucceeded
invoice.payment_failedSUBSCRIPTION_PAYMENT_FAILEDhandleSubscriptionPaymentFailed
payment_intent.succeededPAYMENT_SUCCEEDEDhandlePaymentSucceeded
payment_intent.payment_failedPAYMENT_FAILEDhandlePaymentFailed
customer.subscription.trial_will_endSUBSCRIPTION_TRIAL_ENDINGhandleSubscriptionTrialEnding
billing_portal.session.updatedBILLING_PORTAL_SESSION_UPDATEDLogged only

Webhook Processing Flow

Stripe sends POST -> Read raw body -> Extract stripe-signature header
-> stripeProvider.handleWebhook(body, signature)
-> stripe.webhooks.constructEvent() (signature verification)
-> Map event type to internal type
-> Return { received: true, type, id, data }
-> Switch on webhookResult.type
-> Call appropriate handler
-> Handler updates DB + sends email
-> Return { received: true }

Event Handlers

Subscription Created

Handles new subscription creation:

  1. Checks if the subscription is a sponsor ad (special handling)
  2. Calls webhookSubscriptionService.handleSubscriptionCreated(data) to update the database
  3. Extracts plan information (name, amount, billing period)
  4. Sends a welcome email with subscription details and features

Subscription Updated

Handles subscription changes (plan upgrades, downgrades, etc.):

  1. Updates the database via webhookSubscriptionService.handleSubscriptionUpdated(data)
  2. Extracts updated plan information
  3. Sends an update notification email

Subscription Cancelled

Handles subscription cancellations:

  1. Checks for sponsor ad subscriptions
  2. Updates the database via webhookSubscriptionService.handleSubscriptionCancelled(data)
  3. Sends a cancellation email with the cancellation reason and reactivation URL

Payment Succeeded (One-time)

Handles successful one-time payments:

  1. Extracts customer info and payment details
  2. Formats the amount and payment method
  3. Sends a payment confirmation email with receipt URL

Payment Failed

Handles failed one-time payments:

  1. Extracts error information from last_payment_error
  2. Constructs retry and payment method update URLs
  3. Sends a payment failure notification email

Subscription Payment Succeeded

Handles successful recurring subscription payments:

  1. Updates the database via webhookSubscriptionService.handleSubscriptionPaymentSucceeded(data)
  2. Extracts invoice and subscription details
  3. Sends a subscription payment receipt email

Subscription Payment Failed

Handles failed recurring subscription payments:

  1. Updates the database via webhookSubscriptionService.handleSubscriptionPaymentFailed(data)
  2. Sends a failure notification with retry and payment update URLs

Trial Ending

Handles 3-day trial ending notifications from Stripe:

  1. Updates the database via webhookSubscriptionService.handleSubscriptionTrialEnding(data)
  2. Sends a trial ending reminder email

Email Notifications

Each handler uses the paymentEmailService to send transactional emails. Email configuration is loaded securely via getEmailConfig():

function createEmailData(baseData: any, emailConfig: ReturnType<typeof getEmailConfig>) {
return {
...baseData,
companyName: emailConfig.companyName,
companyUrl: emailConfig.companyUrl,
supportEmail: emailConfig.supportEmail
};
}
EventEmail Template
Subscription createdsendNewSubscriptionEmail
Subscription updatedsendUpdatedSubscriptionEmail
Subscription cancelledsendCancelledSubscriptionEmail
Payment succeededsendPaymentSuccessEmail
Payment failedsendPaymentFailedEmail
Subscription payment successsendSubscriptionPaymentSuccessEmail
Subscription payment failedsendSubscriptionPaymentFailedEmail
Trial endingsendUpdatedSubscriptionEmail

The webhook includes special handling for sponsor ad subscriptions. These are identified by checking metadata:

function isSponsorAdSubscription(data: Record<string, unknown>): boolean {
const metadata = data.metadata as Record<string, string> | undefined;
return metadata?.type === 'sponsor_ad';
}

Sponsor ad events trigger:

  • Activation: Confirms payment and sets the ad to pending admin review
  • Cancellation: Deactivates the sponsor ad
  • Renewal: Extends the sponsor ad end date

Plan Features

The getSubscriptionFeatures function maps plan names to feature lists used in welcome emails:

const features: Record<string, string[]> = {
'Free Plan': ['Access to basic features', 'Email support', 'Limited storage'],
'Standard Plan': ['All advanced features', 'Priority support', 'Unlimited storage', ...],
'Premium Plan': ['All Pro features', 'Dedicated support', 'Custom features', ...]
};

Error Handling

The webhook endpoint follows a resilient pattern:

  • Each individual handler is wrapped in its own try/catch block
  • Handler failures are logged but do not cause the webhook to return an error
  • The outer try/catch catches signature verification and parsing errors
  • Returns 400 for all webhook-level failures to tell Stripe not to retry on permanent errors
try {
// ... signature verification and event dispatch
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 400 });
}

Configuration Requirements

VariableRequiredDescription
STRIPE_SECRET_KEYYesStripe secret API key
STRIPE_WEBHOOK_SECRETYesWebhook signing secret (from Stripe Dashboard)

To configure the webhook in Stripe Dashboard:

  1. Navigate to Developers > Webhooks
  2. Add endpoint URL: https://yourdomain.com/api/stripe/webhook
  3. Select the events listed in the event mapping table above
  4. Copy the signing secret to STRIPE_WEBHOOK_SECRET

Security Considerations

  • Signature verification is mandatory; requests without valid signatures are rejected
  • The raw request body is used for signature verification (not parsed JSON)
  • Webhook secrets should never be committed to version control
  • The endpoint does not require session authentication (Stripe calls it directly)
  • Sensitive data in error messages is sanitized for production environments