Webhook Reliability: Async processing ensures Stripe webhooks are handled reliably even during high traffic or temporary service outages.
Stripe Webhook Processing
Subscription Created
export const processSubscriptionCreated = inngest.createFunction(
{
id: 'process-subscription-created',
retries: 3,
},
{event: 'stripe/subscription.created'},
async ({event, step}) => {
const {subscription, customer} = event.data
// Create organization from subscription
const organization = await step.run('create-organization', async () => {
return await createOrganizationService({
name: customer.name || `Organization ${subscription.id}`,
stripeCustomerId: customer.id,
stripeSubscriptionId: subscription.id,
planId: subscription.items.data[0]?.price.id,
})
})
// Update user's organization membership
await step.run('assign-user-to-organization', async () => {
const user = await getUserByStripeCustomerId(customer.id)
return await assignUserToOrganizationService({
userId: user.id,
organizationId: organization.id,
role: 'ADMIN',
})
})
// Send subscription confirmation email
await step.run('send-confirmation-email', async () => {
return await sendSubscriptionConfirmationEmailService({
email: customer.email,
userName: customer.name,
organizationName: organization.name,
plan: subscription.items.data[0]?.price.nickname,
billingCycle: subscription.items.data[0]?.price.recurring?.interval,
})
})
// Set up trial expiration reminder if applicable
if (subscription.trial_end) {
await step.sendEvent('schedule-trial-reminder', {
name: 'trial/expiration-reminder',
data: {
subscriptionId: subscription.id,
organizationId: organization.id,
trialEndsAt: new Date(subscription.trial_end * 1000).toISOString(),
},
ts: new Date(subscription.trial_end * 1000 - 7 * 24 * 60 * 60 * 1000), // 7 days before
})
}
return {
organizationId: organization.id,
subscriptionProcessed: true,
}
}
)Payment Failed
export const processPaymentFailed = inngest.createFunction(
{id: 'process-payment-failed'},
{event: 'stripe/payment.failed'},
async ({event, step}) => {
const {invoice, subscription, customer} = event.data
// Get organization details
const organization = await step.run('get-organization', async () => {
return await getOrganizationByStripeSubscriptionId(subscription.id)
})
// Update subscription status
await step.run('update-subscription-status', async () => {
return await updateOrganizationSubscriptionStatus({
organizationId: organization.id,
status: 'payment_failed',
lastFailedAt: new Date(),
})
})
// Send payment failure notification
await step.run('send-payment-failure-email', async () => {
return await sendPaymentFailedEmailService({
email: customer.email,
organizationName: organization.name,
amountDue: invoice.amount_due / 100,
currency: invoice.currency,
dueDate: new Date(invoice.due_date * 1000),
paymentUrl: invoice.hosted_invoice_url,
})
})
// Schedule retry reminder in 3 days
await step.sleep('wait-for-retry-reminder', '3d')
await step.run('send-retry-reminder', async () => {
// Check if payment was resolved in the meantime
const currentStatus = await getOrganizationSubscriptionStatus(organization.id)
if (currentStatus.status === 'payment_failed') {
return await sendPaymentRetryReminderEmailService({
email: customer.email,
organizationName: organization.name,
paymentUrl: invoice.hosted_invoice_url,
})
}
})
return {processed: true, organizationId: organization.id}
}
)Subscription Cancelled
export const processSubscriptionCancelled = inngest.createFunction(
{id: 'process-subscription-cancelled'},
{event: 'stripe/subscription.cancelled'},
async ({event, step}) => {
const {subscription, customer} = event.data
// Update organization status
const organization = await step.run('deactivate-organization', async () => {
return await updateOrganizationStatus({
stripeSubscriptionId: subscription.id,
status: 'cancelled',
cancelledAt: new Date(),
})
})
// Export user data (GDPR compliance)
const userData = await step.run('export-user-data', async () => {
return await exportOrganizationDataService(organization.id)
})
// Send cancellation confirmation
await step.run('send-cancellation-email', async () => {
return await sendSubscriptionCancelledEmailService({
email: customer.email,
organizationName: organization.name,
cancelledAt: new Date(),
dataExportUrl: userData.exportUrl,
})
})
// Schedule data deletion (30 days later)
await step.sendEvent('schedule-data-deletion', {
name: 'data/deletion-scheduled',
data: {
organizationId: organization.id,
scheduledFor: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
},
ts: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
})
return {
cancelled: true,
organizationId: organization.id,
dataExportUrl: userData.exportUrl,
}
}
)Invoice Processing
Invoice Payment Succeeded
export const processInvoicePaymentSucceeded = inngest.createFunction(
{id: 'process-invoice-payment-succeeded'},
{event: 'stripe/invoice.payment_succeeded'},
async ({event, step}) => {
const {invoice, subscription, customer} = event.data
// Update organization billing status
await step.run('update-billing-status', async () => {
return await updateOrganizationBillingStatus({
stripeSubscriptionId: subscription.id,
status: 'paid',
lastPaymentAt: new Date(),
nextBillingDate: new Date(subscription.current_period_end * 1000),
})
})
// Send payment receipt
await step.run('send-payment-receipt', async () => {
return await sendPaymentReceiptEmailService({
email: customer.email,
invoiceNumber: invoice.number,
amountPaid: invoice.amount_paid / 100,
currency: invoice.currency,
paidAt: new Date(invoice.status_transitions.paid_at * 1000),
receiptUrl: invoice.receipt_url,
})
})
// Update usage limits if plan changed
if (invoice.subscription_proration_date) {
await step.run('update-usage-limits', async () => {
const plan = subscription.items.data[0]?.price
return await updateOrganizationLimitsService({
stripeSubscriptionId: subscription.id,
planId: plan.id,
limits: await getPlanLimitsService(plan.id),
})
})
}
return {processed: true, invoiceId: invoice.id}
}
)Customer Management
Customer Updated
export const processCustomerUpdated = inngest.createFunction(
{id: 'process-customer-updated'},
{event: 'stripe/customer.updated'},
async ({event, step}) => {
const {customer, previous_attributes} = event.data
// Update organization details
await step.run('update-organization-details', async () => {
const updates: any = {}
if (previous_attributes.name) {
updates.name = customer.name
}
if (previous_attributes.email) {
updates.billingEmail = customer.email
}
if (Object.keys(updates).length > 0) {
return await updateOrganizationByStripeCustomerId({
stripeCustomerId: customer.id,
updates,
})
}
})
// Sync user profile if email changed
if (previous_attributes.email) {
await step.run('sync-user-profile', async () => {
return await updateUserEmailByStripeCustomerId({
stripeCustomerId: customer.id,
newEmail: customer.email,
})
})
}
return {updated: true, customerId: customer.id}
}
)Webhook Event Handlers
Event Router
// Add to your Stripe webhook handler
export async function handleStripeWebhook(event: Stripe.Event) {
const eventMappings = {
'customer.subscription.created': 'stripe/subscription.created',
'customer.subscription.deleted': 'stripe/subscription.cancelled',
'invoice.payment_failed': 'stripe/payment.failed',
'invoice.payment_succeeded': 'stripe/invoice.payment_succeeded',
'customer.updated': 'stripe/customer.updated',
}
const inngestEventName = eventMappings[event.type]
if (inngestEventName) {
await inngest.send({
name: inngestEventName,
data: {
...event.data.object,
eventId: event.id,
eventType: event.type,
},
})
}
}Webhook Verification
// In your webhook route
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')
try {
const event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
)
// Send to Inngest for async processing
await handleStripeWebhook(event)
return new Response('OK', {status: 200})
} catch (error) {
console.error('Webhook verification failed:', error)
return new Response('Webhook verification failed', {status: 400})
}
}Payment Flow Monitoring
Dashboard Tracking
Monitor payment processing in the Inngest dashboard:- Subscription Lifecycle: Track creation, updates, and cancellations
- Payment Status: Monitor successful and failed payments
- Error Handling: Debug webhook processing issues
- Retry Logic: See automatic retry attempts
Custom Metrics
export const trackPaymentMetrics = inngest.createFunction(
{id: 'track-payment-metrics'},
{event: 'stripe/payment.succeeded'},
async ({event, step}) => {
await step.run('record-payment-metrics', async () => {
return await recordMetrics({
event: 'payment_processed',
amount: event.data.amount / 100,
currency: event.data.currency,
customerId: event.data.customer,
timestamp: new Date(),
})
})
return {tracked: true}
}
)Error Recovery
Failed Payment Recovery
export const paymentRecoveryWorkflow = inngest.createFunction(
{id: 'payment-recovery-workflow'},
{event: 'stripe/payment.failed'},
async ({event, step}) => {
const {customer, subscription} = event.data
// Attempt 1: Send immediate notification
await step.run('send-immediate-notice', async () => {
return await sendPaymentFailedEmailService({
email: customer.email,
subscriptionId: subscription.id,
urgency: 'immediate',
})
})
// Wait 3 days, then check and send reminder
await step.sleep('wait-3-days', '3d')
const status = await step.run('check-payment-status', async () => {
return await getSubscriptionPaymentStatus(subscription.id)
})
if (status.failed) {
await step.run('send-reminder-with-help', async () => {
return await sendPaymentReminderWithHelpEmailService({
email: customer.email,
subscriptionId: subscription.id,
})
})
}
return {processed: true}
}
)Payment processing is now automated! Your Stripe webhooks will be processed reliably with automatic retries and comprehensive error handling.