How to handle Stripe webhooks with Edge Functions

This guide demonstrates how to securely handle Stripe webhooks using Azion Edge Functions. You’ll learn to process payment events in real-time, verify webhook signatures, and build a robust payment processing system at the edge.

Requirements

Before you begin, ensure you have:

  • An Azion account
  • A Stripe account with API access
  • Azion CLI installed and configured
  • Node.js version 18 or higher
  • pnpm package manager installed
  • Basic understanding of JavaScript, Edge Functions, and Stripe webhooks
  • Stripe API keys (both secret key and webhook signing secret)

Getting started

Step 1: Set up your development environment

  1. Clone the Stripe webhooks example repository:
Terminal window
git clone https://github.com/egermano/edge-functions-examples.git
cd edge-functions-examples/packages/stripe-webhooks
  1. Install the project dependencies:
Terminal window
pnpm install
  1. Review the project structure to understand the implementation:
Terminal window
ls -la

You should see the main files including the Edge Function implementation, webhook handlers, and configuration files.

Step 2: Configure environment variables

  1. Create a .env file based on the example:
Terminal window
cp .env.example .env
  1. Edit the .env file to include your Stripe credentials:
Terminal window
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_signing_secret
  1. Get your credentials from your Stripe Dashboard:
    • Secret Key: Available in API Keys section
    • Webhook Secret: Created when setting up webhook endpoints

Step 3: Configure Stripe webhook endpoint

  1. In your Stripe Dashboard, go to Developers > Webhooks
  2. Click Add endpoint
  3. Set the endpoint URL to your future Azion domain (you’ll update this after deployment)
  4. Select the events you want to listen for:
    • payment_intent.succeeded
    • payment_method.attached
    • charge.succeeded
    • invoice.payment_succeeded
    • Any other events relevant to your application

Step 4: Build the project

Compile your Edge Function for deployment:

Terminal window
pnpm build

This command builds your Edge Function with TypeScript support and prepares it for deployment.

Step 5: Test locally

Before deploying, test your webhook handler locally:

Terminal window
pnpm dev

This starts a local development server where you can test webhook processing.

Understanding webhook verification

Signature verification

Stripe webhooks include a signature header that you must verify to ensure the webhook came from Stripe:

import crypto from 'crypto';
function verifyStripeSignature(payload, signature, secret) {
const elements = signature.split(',');
const signatureHash = elements.find(element => element.startsWith('v1='));
if (!signatureHash) {
throw new Error('Invalid signature format');
}
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const actualHash = signatureHash.split('=')[1];
if (expectedHash !== actualHash) {
throw new Error('Invalid webhook signature');
}
return true;
}

Webhook event handling

The Edge Function processes different Stripe events:

async function handleWebhookEvent(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
case 'charge.succeeded':
await handleChargeSuccess(event.data.object);
break;
case 'invoice.payment_succeeded':
await handleInvoicePayment(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
}

Deploying to Azion

Step 1: Authenticate with Azion

  1. Log in to your Azion account via CLI:
Terminal window
azion login
  1. Follow the authentication prompts to connect your CLI with your Azion account.

Step 2: Create secrets for Stripe credentials

For security, store your Stripe credentials as secrets:

Terminal window
azion create secret STRIPE_SECRET_KEY
azion create secret STRIPE_WEBHOOK_SECRET

When prompted, enter your respective Stripe credentials. This ensures your sensitive data is encrypted and secure.

Step 3: Deploy the Edge Function

Deploy your webhook handler to Azion’s edge network:

Terminal window
azion deploy

The deployment process will:

  • Upload your Edge Function code
  • Configure the edge application
  • Set up the necessary routing rules
  • Configure environment variables and secrets
  • Provide you with a unique domain

Step 4: Update Stripe webhook configuration

  1. After deployment, you’ll receive a domain like https://xxxxxxx.map.azionedge.net
  2. Go to your Stripe Dashboard > Developers > Webhooks
  3. Edit your webhook endpoint
  4. Update the URL to https://xxxxxxx.map.azionedge.net/webhook
  5. Save the changes

Step 5: Test webhook delivery

  1. Trigger test events in Stripe Dashboard
  2. Monitor webhook delivery and responses
  3. Check Edge Function logs for processing confirmation

Supported webhook events

Payment events

  • payment_intent.succeeded: Payment completed successfully
  • payment_intent.payment_failed: Payment attempt failed
  • payment_intent.canceled: Payment was canceled

Charge events

  • charge.succeeded: Charge completed successfully
  • charge.failed: Charge attempt failed
  • charge.refunded: Charge was refunded

Invoice events

  • invoice.payment_succeeded: Invoice payment completed
  • invoice.payment_failed: Invoice payment failed
  • invoice.created: New invoice created

Customer events

  • customer.created: New customer registered
  • customer.updated: Customer information updated
  • customer.deleted: Customer account deleted

Implementation examples

Payment success handler

async function handlePaymentSuccess(paymentIntent) {
const { id, amount, currency, customer } = paymentIntent;
// Update order status in your database
await updateOrderStatus(paymentIntent.metadata.order_id, 'paid');
// Send confirmation email
await sendPaymentConfirmation(customer, amount, currency);
// Trigger fulfillment process
await triggerFulfillment(paymentIntent.metadata.order_id);
console.log(`Payment succeeded: ${id} for ${amount} ${currency}`);
}

Subscription handling

async function handleSubscriptionEvent(subscription) {
const { id, customer, status, current_period_end } = subscription;
switch (status) {
case 'active':
await activateSubscription(customer, id);
break;
case 'canceled':
await cancelSubscription(customer, id);
break;
case 'past_due':
await handlePastDueSubscription(customer, id);
break;
}
// Update customer record
await updateCustomerSubscription(customer, {
subscription_id: id,
status,
next_billing_date: current_period_end
});
}

Refund processing

async function handleRefund(charge) {
const { id, amount, currency, refunded } = charge;
if (refunded) {
// Process refund in your system
await processRefund({
charge_id: id,
amount,
currency,
order_id: charge.metadata.order_id
});
// Notify customer
await sendRefundNotification(charge.customer, amount, currency);
console.log(`Refund processed: ${id} for ${amount} ${currency}`);
}
}

Testing your webhook handler

Step 1: Use Stripe CLI for local testing

  1. Install Stripe CLI
  2. Forward webhooks to your local development server:
Terminal window
stripe listen --forward-to localhost:3000/webhook
  1. Trigger test events:
Terminal window
stripe trigger payment_intent.succeeded
stripe trigger charge.succeeded
stripe trigger invoice.payment_succeeded

Step 2: Test in production

  1. Use Stripe Dashboard to send test webhooks
  2. Monitor webhook delivery and retry attempts
  3. Check response codes and processing times
  4. Verify event handling accuracy

Step 3: Error handling testing

  1. Test with invalid signatures
  2. Test with malformed payloads
  3. Test timeout scenarios
  4. Verify retry mechanisms

Security best practices

Webhook verification

  • Always verify signatures: Never process unverified webhooks
  • Use HTTPS: Ensure all webhook endpoints use HTTPS
  • Validate payload: Check event structure and required fields
  • Implement replay protection: Track processed event IDs

Secret management

  • Use Azion secrets: Store credentials securely
  • Rotate secrets regularly: Update webhook secrets periodically
  • Limit access: Restrict secret access to necessary functions
  • Monitor usage: Track secret access and usage patterns

Error handling

async function processWebhook(request) {
try {
const payload = await request.text();
const signature = request.headers.get('stripe-signature');
// Verify webhook signature
verifyStripeSignature(payload, signature, STRIPE_WEBHOOK_SECRET);
// Parse event
const event = JSON.parse(payload);
// Process event
await handleWebhookEvent(event);
return new Response('OK', { status: 200 });
} catch (error) {
console.error('Webhook error:', error);
return new Response('Error', { status: 400 });
}
}

Monitoring and logging

Event logging

function logWebhookEvent(event, status, processingTime) {
console.log({
event_id: event.id,
event_type: event.type,
status,
processing_time: processingTime,
timestamp: new Date().toISOString()
});
}

Performance monitoring

  • Response times: Monitor webhook processing speed
  • Success rates: Track successful vs failed processing
  • Error patterns: Identify common failure scenarios
  • Retry frequency: Monitor webhook retry attempts

Troubleshooting

Common issues and solutions

  • Signature verification failures: Check webhook secret configuration
  • Timeout errors: Optimize processing logic for speed
  • Duplicate processing: Implement idempotency checks
  • Missing events: Verify webhook endpoint configuration

Debugging tips

  1. Enable detailed logging: Log all webhook events and processing steps
  2. Use Stripe Dashboard: Monitor webhook delivery attempts
  3. Test locally: Use Stripe CLI for local debugging
  4. Check signatures: Verify webhook signature calculation

Advanced features

Idempotency handling

const processedEvents = new Set();
async function processWebhookWithIdempotency(event) {
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`);
return;
}
await handleWebhookEvent(event);
processedEvents.add(event.id);
}

Batch processing

async function processBatchEvents(events) {
const batchSize = 10;
for (let i = 0; i < events.length; i += batchSize) {
const batch = events.slice(i, i + batchSize);
await Promise.all(batch.map(event => handleWebhookEvent(event)));
}
}

Next steps

  • Implement comprehensive event logging and monitoring
  • Add webhook replay functionality for failed events
  • Integrate with your existing payment processing system
  • Implement advanced fraud detection mechanisms
  • Scale webhook processing for high-volume scenarios