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

JavaScript
// Creates duplicate users!
user = await createUser(email);
subscription = await createSub(user.id);

Do This Instead

JavaScript
// 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.id and 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.

existing-user.js
JavaScript
// 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.

JavaScript
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.

JavaScript
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.

JavaScript
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

1

Local Testing with Stripe CLI

Forward webhooks to your local development server:

Terminal
Bash
# 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
2

Test with Sumo Metadata

Create a test checkout session with Sumo-style metadata:

Terminal
Bash
# 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
3

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.