Skip to main content

Overview

Deltalytix uses Stripe webhooks to receive real-time notifications about subscription events, payment status changes, and other billing activities. The webhook endpoint processes events and updates subscription data in the database accordingly.

Endpoint

POST /api/stripe/webhooks

Authentication

Webhook requests are authenticated using Stripe signature verification to ensure they originate from Stripe.

Signature Verification

All incoming webhook events are verified using the stripe-signature header and your webhook secret:
const event = stripe.webhooks.constructEvent(
  await (await req.blob()).text(),
  req.headers.get("stripe-signature") as string,
  process.env.STRIPE_WEBHOOK_SECRET as string
);
Always verify webhook signatures to prevent unauthorized requests. Invalid signatures return a 400 error.

Configuration

Environment Variables

Configure the following environment variables:
VariableDescriptionRequired
STRIPE_WEBHOOK_SECRETWebhook signing secret from Stripe DashboardYes
STRIPE_SECRET_KEYStripe API secret keyYes
DATABASE_URLPostgreSQL connection stringYes

Setup in Stripe Dashboard

  1. Go to DevelopersWebhooks in your Stripe Dashboard
  2. Click Add endpoint
  3. Enter your webhook URL: https://your-domain.com/api/stripe/webhooks
  4. Select the events to listen to (see Supported Events)
  5. Copy the webhook signing secret and add it to your environment variables

Supported Events

The webhook endpoint handles the following Stripe event types:

checkout.session.completed

Triggered when a checkout session is successfully completed. Handles:
  • Recurring subscription checkouts (mode: subscription)
  • One-time payment checkouts for lifetime plans (mode: payment)
  • Referral code application from session metadata
Actions:
  • Creates or updates subscription record in database
  • Sets subscription plan based on product name
  • Configures billing interval (monthly, quarterly, or lifetime)
  • Applies referral codes if present in metadata
Code reference: /app/api/stripe/webhooks/route.ts:81-234

customer.subscription.created

Triggered when a new subscription is created. Actions:
  • Creates subscription record for new customer
  • Sets initial status (TRIAL or ACTIVE)
  • Records trial period if applicable
  • Links subscription to user account
Code reference: /app/api/stripe/webhooks/route.ts:351-402

customer.subscription.updated

Triggered when subscription details change (plan upgrade/downgrade, status change, etc.). Actions:
  • Updates subscription status in database
  • Handles subscription cancellation scheduling
  • Records cancellation feedback if provided
  • Updates billing period and plan information
Status mappings:
  • activeACTIVE
  • trialingTRIAL
  • past_duePAST_DUE
  • incompletePAYMENT_PENDING
  • incomplete_expiredEXPIRED
  • canceledCANCELLED
  • unpaidUNPAID
Code reference: /app/api/stripe/webhooks/route.ts:260-350

customer.subscription.deleted

Triggered when a subscription is cancelled and deleted. Actions:
  • Updates subscription status to CANCELLED
  • Sets plan to FREE
  • Records final end date
Code reference: /app/api/stripe/webhooks/route.ts:240-259

invoice.payment_failed

Triggered when an invoice payment fails. Actions:
  • Updates subscription status to PAYMENT_FAILED
  • Allows retry logic to handle payment recovery
Code reference: /app/api/stripe/webhooks/route.ts:403-417

payment_intent.succeeded

Triggered when a payment is successfully processed. Actions:
  • Logs successful payment
  • Used for monitoring and debugging
Code reference: /app/api/stripe/webhooks/route.ts:235-239

payment_intent.payment_failed

Triggered when a payment attempt fails. Actions:
  • Logs payment failure details
  • Records error message for debugging
Code reference: /app/api/stripe/webhooks/route.ts:418-424

customer.subscription.trial_will_end

Triggered 3 days before a trial period ends. Actions:
  • Can be used to send reminder emails to customers
  • Currently logged for monitoring

Request Format

Headers

Content-Type: application/json
Stripe-Signature: t=1234567890,v1=signature_hash,v0=fallback_signature

Body

Stripe sends event data as JSON:
{
  "id": "evt_1234567890",
  "object": "event",
  "api_version": "2023-10-16",
  "created": 1234567890,
  "type": "customer.subscription.updated",
  "data": {
    "object": {
      "id": "sub_1234567890",
      "customer": "cus_1234567890",
      "status": "active",
      "plan": {
        "id": "price_1234567890",
        "product": "prod_1234567890",
        "nickname": "Pro Plan"
      },
      "current_period_end": 1234567890,
      "trial_end": null,
      "cancel_at_period_end": false
    }
  }
}

Response Format

Success Response

{
  "message": "Received"
}
Status Code: 200 OK

Error Responses

Invalid Signature

{
  "message": "Webhook Error: [error details]"
}
Status Code: 400 Bad Request

Processing Error

{
  "message": "Webhook handler failed"
}
Status Code: 500 Internal Server Error

Subscription Plans

The webhook handler supports multiple subscription types:

Recurring Subscriptions

  • Monthly plans: interval: month, interval_count: 1
  • Quarterly plans: interval: month, interval_count: 3
  • Plan name is derived from Stripe product name (uppercase)

Lifetime Plans

  • One-time payment mode
  • End date set to 100 years in the future
  • interval: lifetime

Referral Code Handling

Referral codes can be passed via checkout session metadata:
// In checkout session creation
metadata: {
  referral_code: "REF123"
}
The webhook automatically:
  1. Retrieves the referral by slug
  2. Validates the referral doesn’t belong to the purchasing user
  3. Adds user to referral list if not already present
  4. Logs application success or errors
Referral code application errors are non-fatal and won’t block subscription creation.

Security Considerations

Best Practices

  1. Signature Verification: Always verify the stripe-signature header
  2. HTTPS Only: Only accept webhooks over HTTPS in production
  3. Secret Rotation: Regularly rotate webhook signing secrets
  4. Idempotency: Handle duplicate events gracefully (Stripe may send the same event multiple times)
  5. Timeout Protection: Respond to webhooks within 10 seconds to avoid retries

Environment Security

Never expose STRIPE_WEBHOOK_SECRET in client-side code or version control. Store it securely in environment variables.
# .env.example
STRIPE_WEBHOOK_SECRET='your_stripe_webhook_secret_here'
STRIPE_SECRET_KEY='your_stripe_secret_key_here'

Testing

Local Testing with Stripe CLI

  1. Install the Stripe CLI:
    # macOS
    brew install stripe/stripe-cli/stripe
    
    # Linux
    wget https://github.com/stripe/stripe-cli/releases/latest/download/stripe_linux_amd64.tar.gz
    tar -xvf stripe_linux_amd64.tar.gz
    
  2. Login to Stripe:
    stripe login
    
  3. Forward webhooks to your local server:
    stripe listen --forward-to localhost:3000/api/stripe/webhooks
    
  4. Copy the webhook signing secret displayed and add it to your .env.local:
    STRIPE_WEBHOOK_SECRET=whsec_...
    
  5. Trigger test events:
    # Test subscription created
    stripe trigger customer.subscription.created
    
    # Test checkout completed
    stripe trigger checkout.session.completed
    
    # Test payment failed
    stripe trigger invoice.payment_failed
    

Testing in Development

Use the Stripe Dashboard to send test webhooks:
  1. Go to DevelopersWebhooks
  2. Click on your endpoint
  3. Click Send test webhook
  4. Select an event type
  5. Optionally customize the event data
  6. Click Send test webhook

Monitoring Webhook Delivery

View webhook delivery attempts in the Stripe Dashboard:
  1. Go to DevelopersWebhooks
  2. Click on your endpoint
  3. View recent deliveries with response codes and timing
  4. Click on individual events to see request/response details

Troubleshooting

Common Issues

Signature Verification Failed

Problem: Webhook returns 400 with “Webhook Error” Solutions:
  • Verify STRIPE_WEBHOOK_SECRET matches the secret in Stripe Dashboard
  • Ensure you’re using the correct endpoint URL
  • Check that the request hasn’t been modified by middleware

Database Update Failed

Problem: Webhook returns 500 with “Webhook handler failed” Solutions:
  • Verify DATABASE_URL is correctly configured
  • Check database connection and permissions
  • Review application logs for detailed error messages
  • Ensure user exists in database before subscription creation

Events Not Received

Problem: Stripe shows events sent but webhook not triggered Solutions:
  • Verify endpoint URL is accessible from the internet
  • Check firewall rules allow Stripe IPs
  • Ensure webhook endpoint is configured in Stripe Dashboard
  • Review server logs for incoming requests

Duplicate Events

Problem: Same event processed multiple times Solutions:
  • Stripe may send duplicate events, implement idempotency checks
  • Use event ID (event.id) to track processed events
  • Consider implementing event deduplication logic

Rate Limits

Stripe webhooks have the following characteristics:
  • Timeout: 10 seconds (respond within this time to avoid retries)
  • Retries: Failed webhooks are retried with exponential backoff for up to 3 days
  • Order: Events may arrive out of order; use timestamps to handle this
For long-running operations, respond to the webhook immediately with 200 OK and process the event asynchronously using a queue.

Code Example

Complete webhook handler structure:
export async function POST(req: Request) {
  // 1. Verify webhook signature
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      await (await req.blob()).text(),
      req.headers.get("stripe-signature") as string,
      process.env.STRIPE_WEBHOOK_SECRET as string
    );
  } catch (err) {
    return NextResponse.json(
      { message: `Webhook Error: ${err}` },
      { status: 400 }
    );
  }

  // 2. Check if event type is permitted
  const permittedEvents = [
    "checkout.session.completed",
    "customer.subscription.created",
    "customer.subscription.updated",
    "customer.subscription.deleted",
    "invoice.payment_failed",
    "payment_intent.succeeded",
    "payment_intent.payment_failed",
    "customer.subscription.trial_will_end"
  ];

  if (!permittedEvents.includes(event.type)) {
    return NextResponse.json({ message: "Received" }, { status: 200 });
  }

  // 3. Process event based on type
  try {
    switch (event.type) {
      case "checkout.session.completed":
        // Handle subscription or one-time payment
        break;
      case "customer.subscription.updated":
        // Update subscription status
        break;
      // ... other cases
    }
  } catch (error) {
    return NextResponse.json(
      { message: "Webhook handler failed" },
      { status: 500 }
    );
  }

  // 4. Return success response
  return NextResponse.json({ message: "Received" }, { status: 200 });
}

Build docs developers (and LLMs) love