Stripe troubleshooting
"Stripe webhook signature mismatch"
The marketplace returns 400 with a signature error. Causes:
STRIPE_WEBHOOK_SECRETnot set, or wrong value.- Webhook controller isn't reading raw body. Verify
@Req() req: RawBodyRequest<Request>andreq.rawBodyis used inconstructEvent— notreq.body. - Some intermediary modified the body (a buggy WAF, a proxy rewriting JSON, a Node middleware re-stringifying).
Test:
stripe listen --forward-to https://api.fleapo.ai/webhooks/stripe
stripe trigger checkout.session.completed
If stripe listen shows 200, the issue is environment-specific.
"Webhook succeeds in Stripe but DB doesn't update"
Check stripe_events:
SELECT type, processed_at, error_message
FROM stripe_events
ORDER BY received_at DESC
LIMIT 10;
If processed_at is NULL and error_message is set, the handler threw. Read the error.
Common handler failures:
- Tier row for
stripePriceIdmissing — happens if a Stripe Price was created outside the marketplace. metadata.userIdabsent on the Checkout Session — happens when you craft the session yourself instead of going throughPOST .../checkout.
After fixing the underlying cause, you can replay events by clearing error_message and processed_at, then re-dispatching. Build scripts/retry-stripe-events.ts if you don't have it.
"Subscription stuck in incomplete"
The user started Stripe Checkout but 3DS / SCA failed. Stripe's customer.subscription.created fires with status: 'incomplete'. After a few retries, you get either customer.subscription.updated (status active) or incomplete_expired.
Watch for the next event before assuming the user is stuck.
"Stripe says subscription is active but local says past_due"
Webhook drift. Either:
- A
customer.subscription.updatedevent was missed (network outage, marketplace down). - An event errored and
error_messageis set.
Run reconciliation:
const sub = await stripe.subscriptions.retrieve(localSub.stripeSubscriptionId);
// Compare sub.status, sub.current_period_end, sub.cancel_at_period_end to local
Build into a scripts/reconcile-stripe.ts and run nightly until events stay caught up.
"Free tier checkout creates the sub but Stripe doesn't know"
Correct — free tiers bypass Stripe entirely. There's no Stripe Customer or Subscription. Don't go looking for one. The marketplace row carries stripeSubscriptionId: null.
If the user upgrades to a paid tier later, a new Stripe Subscription is created at that time and the row is updated.
"Old Stripe Product still listed in our admin UI"
Pricing tier deletion archives the Stripe Product (sets active: false) but the row is soft-deleted, not removed. The admin UI may still surface archived tiers depending on its filter. If you want to hard-remove:
DELETE FROM agent_pricing_tiers WHERE id = '<id>' AND <constraint that it's archived>;
Then archive any unused Stripe Products in the dashboard.
"Multiple active subscriptions for one user/agent"
The partial unique index on user_agent_subscriptions(userId, agentId) WHERE status IN ('active','trialing') should prevent this. If you see it, the index is missing in your environment — check migrations applied cleanly:
SELECT indexname, indexdef FROM pg_indexes
WHERE tablename = 'user_agent_subscriptions';
If absent, generate and apply the migration that adds it.
"Stripe Checkout success_url doesn't carry session_id"
You omitted {CHECKOUT_SESSION_ID} from the success_url. Add it:
success_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`
Stripe replaces the literal {CHECKOUT_SESSION_ID} at redirect time. Without it, your post-checkout page can't fetch session details to confirm the purchase.