The list I wish I had on day one
I’ve integrated Stripe with Laravel apps a dozen times. Every project broke in the same places. Here are the 12 “of course that happened” moments and the exact fixes.
1) Webhooks not retrying properly
Fix: Mark your webhook controller queueable and return a 200 quickly. Do the heavy work in jobs.
// app/Http/Controllers/StripeWebhookController.php
public function __invoke(Request $request)
{
$payload = $request->all();
dispatch(new ProcessStripeEvent($payload));
return response('ok');
}
2) Idempotency keys on create checkout session
Fix: Generate one per intent and persist it. If a user double‑clicks, Stripe won’t create duplicates.
3) Trials that don’t end (Cashier)
Fix: Ensure you’re using trial_ends_at
on the subscription and not only ends_at
. Add a scheduled command that cancels stale trials daily.
4) Proration confusion on upgrades
Fix: Explicitly set proration_behavior
for upgrades/downgrades. Document it in UI copy to avoid “why did I get charged $2.37?” emails.
5) Tax settings mismatched
Fix: Pick one source of truth. If using Stripe Tax, don’t duplicate logic in Laravel. Show tax estimate pre‑checkout to reduce surprise.
6) Customer portal returning to nowhere
Fix: Always pass a return_url
and test it from mobile and desktop. Add a safe fallback route.
7) Seats vs members
Fix: If you bill for seats, enforce seat counts in your app. Sync Stripe quantity with team size and block invites beyond paid seats.
8) Inconsistent entitlements after plan change
Fix: Recompute entitlements on customer.subscription.updated
. Store entitlements in one place (e.g., features
JSON) and hydrate on login.
9) Deleted products referenced in code
Fix: Never hardcode Stripe IDs. Store them in config or the DB and reference by key. Add a feature flag for new price IDs.
10) Refunds without context
Fix: Add a refund_reason
column. When issuing a refund, store the reason and link it to the support conversation.
11) Duplicate events processing
Fix: Use Stripe event id
as a de‑dup key. Keep a table of processed IDs with a unique index.
12) Sandboxed tests that never hit the queue
Fix: In CI, run the queue worker for webhook tests or fake jobs explicitly. Don’t rely on synchronous behavior.
Pre‑launch checklist (billing)
- Create, upgrade, downgrade, and cancel from a real test account
- Dunning emails fire on
invoice.payment_failed
- Customer portal
return_url
works on mobile - Annual discount math visible and correct
- Webhook retries and dead‑letter queue monitored
If cancellations are your current headache, grab these: Churn Is a Conversation: 7 Emails That Saved Real Accounts.
Building the pricing page next? Pair this with: The SaaS Pricing Page: Wireframes, Copy, and a Laravel + Tailwind Build.