Best Practices
Production-ready patterns for handling edge cases, ensuring idempotency, and testing your Sumo integration.
Idempotency
Stripe may send the same webhook multiple times. Your handler must be idempotent - processing the same event twice should produce the same result.
Don't Do This
// Creates duplicate users!
user = await createUser(email);
subscription = await createSub(user.id); Do This Instead
// Safe for duplicate webhooks
user = await findOrCreateUser(email);
sub = await findOrCreateSub(user.id, subId); Idempotency Strategies
- Use unique constraints: Add unique index on
stripe_subscription_id - Track processed events: Store
event.idand skip if seen before - Use findOrCreate patterns: Database-level upserts handle concurrent requests
Common Edge Cases
User Already Exists
A customer may already have an account from a direct purchase or previous Sumo deal.
// Check if user exists first
let user = await db.users.findByEmail(email);
if (user) {
// Existing user - just link the new subscription
await db.subscriptions.create({
userId: user.id,
stripeSubscriptionId: subscriptionId,
});
} else {
// New user from Sumo - create account
user = await db.users.create({ email, source: 'sumo' });
} Multiple License Quantities
Customers can purchase multiple licenses. Check the sumo_quantity metadata.
const quantity = parseInt(session.metadata.sumo_quantity, 10) || 1;
// Provision correct number of seats/licenses
await provisionAccess(user.id, tierId, quantity); Welcome Email for Existing Users
Don't send a "welcome" email to existing users. Send an "upgrade" or "new subscription" email instead.
if (isNewUser) {
await sendWelcomeEmail(user.email);
} else {
await sendNewSubscriptionEmail(user.email, tierId);
} Failed User Provisioning
If user creation fails, return a 500 error so Stripe retries the webhook.
try {
await handleSumoPurchase(session);
return { status: 200 };
} catch (error) {
console.error('Provisioning failed:', error);
// Return 500 so Stripe retries
return { status: 500, error: 'Provisioning failed' };
} Testing Your Integration
Local Testing with Stripe CLI
Forward webhooks to your local development server:
# Install and login
stripe login
# Forward webhooks to localhost
stripe listen --forward-to localhost:8000/webhooks/stripe
# Trigger a test event
stripe trigger checkout.session.completed Test with Sumo Metadata
Create a test checkout session with Sumo-style metadata:
# Create checkout session with Sumo metadata
stripe checkout sessions create \
--success-url="http://localhost:3000/success" \
--cancel-url="http://localhost:3000/cancel" \
--mode=subscription \
--line-items[0][price]=price_xxx \
--line-items[0][quantity]=1 \
--metadata[sumo_campaign_id]=test_campaign \
--metadata[sumo_product_id]=test_product \
--metadata[sumo_product_tier_id]=test_tier \
--metadata[sumo_quantity]=1 Verify in Your Dashboard
After completing a test purchase, verify:
- User was created in your database
- Subscription is linked to the user
- Access was provisioned correctly
- Welcome email was sent
Security Checklist
Always Verify Signatures
Never process webhooks without verifying the stripe-signature header.
Use HTTPS Only
Your webhook endpoint must use HTTPS in production. Stripe won't send webhooks to HTTP endpoints.
Secure Environment Variables
Never commit STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET to version control.
Log for Auditing
Log webhook events (without sensitive data) for debugging and audit trails.
Final Step
Ready to Launch?
Complete the readiness checklist to verify your integration is production-ready.