How to handle customer.subscription.created if a organization(tenant) doesn’t exist yet in Stripe webhook?
I'm using Django and Stripe for a multi-tenant SaaS. I register users and organizations manually from a view, and I create the customer and subscription using Stripe API before saving to the database.
class TestRegisterView(APIView):
permission_classes = ()
def post(self, request, *args, **kwargs):
user_data = request.data.get('user', None)
organization_data = request.data.get('organization', None)
payment_data = request.data.get('payment', None)
if None in [user_data, organization_data, payment_data]:
return Response(data={'message': 'Missing data'}, status=400)
price = PriceModel.objects.filter(stripe_price_id=payment_data['price_id'])
if not price.exists():
return Response(data={'msg': 'Price not found'}, status=400)
user = UserModel.objects.filter(
Q(email=user_data['email']) | Q(username=user_data['username']))
if user.exists():
return Response(data={'msg': 'Customer with that email or username already exists'}, status=400)
organization = OrganizationModel.objects.filter(
Q(name=organization_data['name']) |
Q(company_email=organization_data['company_email']) |
Q(phone_number=organization_data['phone_number']))
if organization.exists():
return Response(data={'message': 'Organization already exists'}, status=400)
user_serializer = POSTUserSerializer(data=user_data)
organization_serializer = POSTOrganizationSerializer(data=organization_data)
if user_serializer.is_valid() and organization_serializer.is_valid():
stripe_connection = StripeSingleton()
try:
stripe_customer = stripe_connection.Customer.create(
email=payment_data['email'],
name=f"{user_data['first_name']} {user_data['last_name']}",
)
except Exception:
return Response(data={'msg': 'Error creating Stripe customer'}, status=400)
try:
subscription = stripe_connection.Subscription.create(
customer=stripe_customer.id,
items=[{'price': payment_data['price_id']}],
payment_behavior='default_incomplete',
expand=['latest_invoice.confirmation_secret'],
)
except Exception:
return Response(data={'msg': 'Error creating Stripe subscription'}, status=400)
try:
with transaction.atomic():
user = user_serializer.save()
organization = organization_serializer.save(
owner_id=user.id,
stripe_customer_id=stripe_customer.id
)
user.organization = organization
user.save()
with schema_context(organization.schema_name):
UserLevelPermissionModel.objects.create(
user=user,
level=UserLevelPermissionModel.UserLevelEnum.ADMIN,
)
except Exception:
stripe_connection.Subscription.delete(subscription.id)
stripe_connection.Customer.delete(stripe_customer.id)
return Response(data={'msg': 'Error creating user or organization'}, status=400)
return Response(data={
'subscription_id': subscription.id,
'client_secret': subscription.latest_invoice.confirmation_secret
}, status=201)
errors = {}
errors.update(user_serializer.errors)
errors.update(organization_serializer.errors)
return Response(data=errors, status=400)
Here’s my webhook:
class StripeWebhookView(APIView):
permission_classes = [AllowAny]
def post(self, request, *args, **kwargs):
payload = request.body
sig_header = request.headers.get('stripe-signature')
try:
event = stripe.Webhook.construct_event(
payload=payload,
sig_header=sig_header,
secret=settings.STRIPE_SIGNING_SECRET
)
except Exception:
return Response(status=400)
event_type = event['type']
data_object = event['data']['object']
if event_type == 'invoice.payment_succeeded':
billing_reason = data_object.get('billing_reason')
organization = OrganizationModel.objects.get(
stripe_customer_id='cus_Sr7NCr3urlBdws'
)
if billing_reason == 'subscription_create':
print(data_object)
elif event_type == 'customer.subscription.created':
subscription = data_object
customer_id = subscription.get("customer")
try:
organization = OrganizationModel.objects.get(
stripe_customer_id=customer_id
)
# handle subscription creation logic here
except OrganizationModel.DoesNotExist:
# I don't know what to do here if my tenant doesn't exist yet
return Response(status=200)
My issue
Sometimes the webhook customer.subscription.created arrives before the organization (and tenant schema) is created in the database.
Since Stripe does not guarantee webhook order, this breaks the logic. The event cannot be processed because the organization isn't there yet. If I respond 200 OK, Stripe does not retry it. If I return 400, Stripe retries too many times, and it still might not work until the org is created.
My question
According to Stripe best practices:
- How should I safely handle events like customer.subscription.created when the customer/organization might not exist yet?
- Is it recommended to store events temporarily (in a table like WebhookEventModel) and process them later once the tenant exists?
- Or is there a better way to delay processing of these events?
- How do teams usually manage this race condition?
Thanks in advance.
First Suggestion
I've looked through the post
function in your TestRegisterView
a few times and it is still unclear to me why you need to defer creation of the organization until after you create the Customer and Subscription using the Stripe SDK.
So my first suggestion would be to defer the creation of any Stripe objects until after the necessary entities are created in your database. I'm going to assume you have a reason and offer another suggestion but I think you should give this serious consideration. It might be more elegant the way you are currently doing it but I still think you should consider deferring Customer & Subscription creation.
Second Suggestion
Another approach that may solve this involves adding Celery to your project. Having an async task queue for handling Stripe webhooks is a good idea anyway especially as your database grows and queries take longer to execute.
Specifically there is a feature in the @shared_task
decorator that might work here. When you decorate a task function you can specify the number of retries and what back off strategy to use. In your case you could specifies retries with exponential back off. This would allow for the processing of the webhook event payload to fail once (or more), wait some time during which the organization gets created, and then retry the operation.
Such a decorator might look like the following
@app.task(autoretry_for=(OrganizationModel.DoesNotExist,), retry_backoff=True)
def task:
# your code for handling the webhook event goes here