Webhooks
Stripe webhook handling for subscription and product sync.
HyperSaaS processes Stripe webhooks to keep the local database in sync with Stripe's state.
Webhook Endpoint
POST /api/subscriptions/webhook/No authentication required — requests are verified using Stripe's webhook signature.
event = stripe.Webhook.construct_event(
payload=request.body,
sig_header=request.META["HTTP_STRIPE_SIGNATURE"],
secret=STRIPE_WEBHOOK_SECRET,
)Handled Events
Subscription Events
| Event | Handler |
|---|---|
customer.subscription.created | Create/update local Subscription + SubscriptionItems |
customer.subscription.updated | Update subscription fields and items |
customer.subscription.deleted | Update subscription status to cancelled/ended |
Processing:
- Extract subscription data from event payload
- Find
StripeUserbycustomerID - Update/create
Subscriptionrecord with all fields (period, status, trial, cancellation) - Extract
workspace_idfrom subscription metadata - Update associated
Workspacewith subscription reference - Delete existing
SubscriptionItemrecords and recreate from webhook data
Product Events
| Event | Handler |
|---|---|
product.created | Create local Product + Feature mappings |
product.updated | Update Product + sync Feature mappings |
product.deleted | Update Product (mark inactive) |
Feature Sync:
Features are read from Stripe Product metadata:
Stripe Product metadata:
features: "chat rag export analytics"The handler:
- Splits the space-delimited feature string
- Creates
Featurerecords for any new feature IDs - Creates
ProductFeatureassociations - Removes orphaned associations (features no longer in metadata)
Price Events
| Event | Handler |
|---|---|
price.created | Create local Price record |
price.updated | Update Price fields |
price.deleted | Update Price (mark inactive) |
Frequency Calculation:
Stripe's price.recurring is converted to a freq string:
# Stripe: {"interval": "month", "interval_count": 1}
# Local: freq = "month_1"
# Stripe: {"interval": "year", "interval_count": 1}
# Local: freq = "year_1"Webhook Setup
1. Configure in Stripe Dashboard
Go to Developers → Webhooks and add an endpoint:
URL: https://your-domain.com/api/subscriptions/webhook/Select these events:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedproduct.createdproduct.updatedproduct.deletedprice.createdprice.updatedprice.deleted
2. Set Webhook Secret
Copy the webhook signing secret and add it to your environment:
STRIPE_WEBHOOK_SECRET=whsec_...3. Local Testing with Stripe CLI
# Forward webhook events to local server
stripe listen --forward-to localhost:8000/api/subscriptions/webhook/
# Trigger test events
stripe trigger customer.subscription.createdEvent Validation
All webhook payloads are validated with Pydantic models before processing:
class StripeEvent(BaseModel):
id: str
type: str
data: dict
# Routes to specific Pydantic models based on event type
# e.g., customer.subscription.* → StripeSubscription
# e.g., product.* → StripeProductInvalid payloads are rejected with appropriate error logging.
Idempotency
Webhook handlers use update_or_create patterns, making them safe to replay. If Stripe sends the same event twice (which it can during retries), the handler produces the same result.
Error Handling
| Scenario | Response |
|---|---|
| Invalid signature | 400 Bad Request |
| Unhandled event type | 200 OK (acknowledged but ignored) |
| Processing error | 500 (Stripe will retry) |
| Missing StripeUser | Logged as warning, event skipped |