- ⚠️ Stripe webhooks can get to your system before your SaaS tenant is registered. This can lead to subscriptions that are not linked to any tenant.
- 🛡️ When you delay webhook processing with queues, you stop bad links in multi-tenant apps.
- 🏗️ Make lightweight "shell" tenants ahead of time. This gives Stripe events something to link to.
- 🔁 Stripe will try to send webhooks again for up to 72 hours, using exponential backoff.
- 🧩 Metadata fields in Stripe objects link subscriptions to tenants well.
Building a multi-tenant SaaS product with Stripe is more than just adding payments to a form. The way Stripe webhooks work means you often get events like customer.subscription.created before your system is ready. This is true when your tenant (organization) is not ready yet. Here, we explain why this happens. We also show ways to handle it and steps your team can take. This will help you make a payment system that works well, even with errors.
Understanding Stripe Webhooks in a Multi-Tenant Context
What Is customer.subscription.created?
The customer.subscription.created webhook is a Stripe event sent when a new subscription is made. The event has details about the subscription, like the customer ID, plan, billing cycle, and status.
This webhook can be sent in a few ways:
- Successful checkout session completion.
- API-triggered subscription creation.
- Reactivation of previously canceled subscriptions.
Because Stripe works quickly and does not know your app's state, it can send this webhook long before you are ready to use it. This is very true for multi-tenant SaaS systems.
Multi-Tenant SaaS Architecture Basics
In a multi-tenant SaaS app, different businesses or organizations share the same app setup. But they have their own data and services, separate from others.
A normal user flow looks like this:
- A user signs up with your service and starts organization creation.
- The user goes to Stripe Checkout or uses your payment API endpoint to subscribe.
- Stripe makes a subscription, charges the customer, and sends the
customer.subscription.createdwebhook. - Your backend uses the webhook and links the Stripe customer/subscription to the right tenant.
Problems happen when the subscription webhook gets there before Step 1 is done. This mismatch between the Stripe subscription and the organization in your database causes big problems.
The Problem: Webhook Arrives Before Tenant Is Created
Race Conditions in Customer Signup Flows
The main problem is a race condition: Stripe is fast. Sometimes it is faster than your system. A customer might pay in milliseconds. But your backend could still be finishing user records or checking data.
Imagine this:
User starts sign-up --> Payment going on --> Stripe finishes transaction
↓
Webhook sent right away
Meanwhile: user onboarding process still holding up tenant creation
Now your webhook handler gets a customer.subscription.created event. But it does not know where to link it because the organization is not in your database yet.
Real-World Scenarios Where This Happens
Even old systems see this. Below are usual reasons:
- Slow database saves: For example, if your org creation needs many database tables or hard checks.
- OAuth delays: Apps that use third-party logins (Google, Microsoft) can be very slow when getting tokens or user info.
- Serverless latency: Cold starts in AWS Lambda or Vercel edge functions can slow down setup processes after payment.
- UI errors or users going back: Users might refresh, hit back, or quit in the middle of your sign-up process.
Why You Shouldn’t Ignore the Webhook or Fail Silently
Are you thinking of just throwing away events that can't be linked, or sending an "OK" but ignoring them? Do not.
- 🧩 Lost links: If you do not link the subscription to the right tenant, you lose track of money.
- 😱 Hard to fix: It is almost impossible to fix unlinked payments days later.
- 🔁 Billing issues: If you do not link subscriptions, your reports will show wrong monthly recurring revenue (MRR) and churn numbers.
Every incoming webhook needs a clear way to handle it, even if it is put off.
Strategy 1 – Store Inbound Webhooks for Deferred Processing
Logging Webhooks in a Durable Queue or Table
When there is no tenant for an incoming subscription, the best thing to do is put off processing. Put the webhook data in a queue or a database table that saves data. You can then use it later.
// Pseudo-logic
if (!findOrgByStripeCustomer(subscription.customer)) {
logWebhook(payload, 'deferred');
return 200;
}
processWebhook(subscription);
You can do this with a queue using:
- ✅ AWS SQS or SNS for many queues
- ✅ Redis + Sidekiq (for Ruby apps)
- ✅ RabbitMQ or Apache Kafka for large messaging systems
- ✅ A database table that tracks retries
The main thing is to make sure the webhook is never lost. It is only paused until what it needs is ready.
Implementing Retry Logic Wisely
Choose between two ways to try again:
- Checking Jobs: Check the table of put-off events often. If the tenant is there now, run the saved webhook handlers again.
- Tenant-Creation Hooks: When a tenant is made, look right away for related Stripe events in the webhook queue by customer ID or metadata.
Since Stripe tries to send webhooks again when they fail, using exponential backoff over 72 hours, your backup system gives you more time without errors.
Matching Subscriptions to Future Entities
A good, but often unused, feature: Stripe metadata. You can put your internal IDs into Stripe objects. This means you can always link things again.
const session = await stripe.checkout.sessions.create({
customer_email: user.email,
metadata: {
internal_user_id: 'u_987',
pending_org_id: 'org_tmp_123'
}
});
Then use this metadata inside your webhook handler:
const orgId = event.data.object.metadata.pending_org_id ?? null;
if (orgId && findOrg(orgId)) {
linkSubscriptionToOrg(event.id, orgId);
}
Metadata helps you link events to items, even later.
Strategy 2 – Immediately Create a Lightweight Org Skeleton
Early Tenant Record Creation on Payment Initiation
Stop race conditions by making tenants earlier in the process. When a user starts signing up, make an "incomplete" or pending tenant record right away. This is true even if the rest of the setup is not done.
// During early signup step
const orgRecord = createOrg({
name: null,
status: 'pending',
email: user.email
});
launchStripeCheckout(orgRecord.id);
This makes sure that no matter how fast customer.subscription.created gets there, you will have an Org ID to link it to right away.
Using Stripe Checkout or Customer Metadata to Pass Org IDs
When you make checkout sessions or customer profiles, put internal tenant IDs in their metadata field:
await stripe.checkout.sessions.create({
customer_email: user.email,
metadata: {
org_id: 'org_123',
user_id: 'u_456',
subscription_plan: 'pro'
}
});
Now your webhook logic can safely:
- Verify metadata matches expected values
- Resume incomplete org setup where it left off
Handling Partial Data States Gracefully
Of course, this means you will sometimes have temporary organization records that are not fully working yet. Be ready for this:
- Add fields like
status(incomplete,setting up,active) - Make admin tools to check or remove unused orgs
- Always show the org status in your App UI or Admin Console
Your data model must be flexible in multi-tenant systems that use quick processes like Stripe's.
Additional Safeguards for a Resilient Webhook System
Idempotency and Deduplication Keys
Write webhook handlers to be idempotent—they should safely ignore repeat events.
if (hasProcessedEvent(event.id)) {
return 200;
}
// Business logic only runs once
handleSubscriptionCreated(event);
markEventAsProcessed(event.id);
Stripe event IDs are unique everywhere. So, you can safely use them as main IDs in an event log table.
Validating Webhook Authenticity
Never trust webhook data you get without checking. Use Stripe's webhook signature validation to check they are real.
import stripe
event = stripe.Webhook.construct_event(
payload, sig_header, STRIPE_ENDPOINT_SECRET
)
This stops bad HTTP events or fake ones that try to make subscriptions.
Monitoring and Alerting on Webhook Failures
Connect to monitoring tools with:
- 📉 Error tracking: Sentry, Rollbar
- 🔍 Log search: Elasticsearch, Datadog, CloudWatch
- 📊 Custom dashboards: Watch things like
- % of webhooks that cannot be linked
- How long it takes to use events
- How big the queue is
This warning system makes sure you fix things before customers or Finance teams see problems.
Architectural Patterns for Scaling Subscription Handling
Event-Driven Framework Integration
Using an event-driven system can hide the problems of needing other things between tenants and subscriptions.
- Stripe webhook triggers →
subscription.created - Internal EventBridge or Kafka event →
CreateOrAttachTenantJob - Worker checks current state → uses or puts off accordingly
Benefits:
- Business logic is not tightly linked
- Jobs that can be tried again with exponential backoff
- Works well across many services
This is very helpful in big systems where making an org uses many small services.
Transactional Outbox and SAGA Pattern
One advanced way to do things is the Transactional Outbox Pattern:
- When making a tenant, write an “outbox” entry in the same database transaction.
- A worker uses outbox entries after saving, which starts any webhook logic needed.
- This is good to use with SAGA tools for many-step processes.
It is more complex, but this pattern keeps things together. This is needed for big company billing systems.
Final Recommendation and Decision Framework
When to Use Delayed vs Early Models
- 🚀 Startups (0–1 stage): Choose early “shell org” creation—it is simpler, with fewer parts.
- 📦 Growing SaaS (Series A–B): Use queues and webhook buffers that save data to keep data safe and track it.
- 🏗️ Large-scale enterprises: Use both ways. Add ways to watch what happens. Regularly check put-off processing.
Mapping Team Goals to Solution Approach
| Team Type | Suggested Model |
|---|---|
| Solo Dev / MVP | Org shell + metadata in Stripe |
| Mid-size SaaS Teams | Webhook persistent queue + retries |
| Enterprise Engineering | EventBus-based + Outbox patterns |
Build for Resilience, Not Just for Success Paths
Stripe’s webhook-first, quick model works well. But only if you handle unusual cases well. You might handle subscriptions later with a queue, or make tenant skeletons ahead of time. The goal is the same: make sure no webhook is lost in the rush.
Start using one of the ways above. And test for when things go wrong, not just when they go right. Let metadata guide you, idempotency be your safety, and monitoring be your warning system.
Every dollar Stripe helps you collect depends on trusting that webhooks are used fully and correctly.
Citations
Stripe. (2023). Webhooks Overview. Retrieved from https://stripe.com/docs/webhooks
Stripe. (2023). Secure your webhooks. Retrieved from https://stripe.com/docs/webhooks/signatures
Stripe. (2023). Checkout metadata customization. Retrieved from https://stripe.com/docs/checkout/metadata