# Billing + Stripe Handover (Apr 11, 2026) ## Context This document captures the current billing integration state between: - zpack-api - zpack-portal - Stripe (sandbox/test mode) This was developed during a developer-mode session where chat history is not persisted, so this doc acts as the continuity handoff. --- ## Current Status ### Stripe - Stripe sandbox/test mode configured - Products + recurring prices created - Checkout sessions successfully redirect to Stripe - Test checkout completed using Stripe test card - Stripe dashboard confirms: - checkout.session.completed - customer created - subscription created - invoice/payment succeeded (test) ### API (zpack-api) Working: - `ensureStripeCustomer()` persists `stripeCustomerId` - `/api/billing/checkout` successfully creates Stripe Checkout session - Billing foundation fields exist in Prisma: - `stripeCustomerId` - `subscriptionStatus` - `plan` Not yet complete: - Webhook delivery not working (API not publicly reachable) - As a result: - `subscriptionStatus` is NOT being updated - `plan` is NOT being updated Root cause: - Stripe cannot reach `/api/billing/webhook` - This is NOT a checkout issue - This is a webhook reachability issue ### Portal (zpack-portal) Working: - Billing page renders correctly for unsubscribed users - Plan selection UI present - Checkout button calls API - Redirect to Stripe works Partially complete: - Needs final alignment for: - `trialing` state - `active` state - billing-exempt/admin display --- ## Key Architectural Decisions ### Billing Model - Users can register without paying - Subscription is optional until resource enforcement is implemented - Future plan: soft gate (likely 1 free server, then require subscription) ### Stripe Integration Strategy - Use Stripe Checkout (NOT custom payment form) - Use Stripe Billing Portal for management - Use webhooks to update DB state - DB remains source of truth for access decisions ### Metadata Usage Stripe objects should include: - `metadata.userId` - optionally `metadata.plan` This is used for safer webhook reconciliation. --- ## Current Blocker ### Webhook Delivery Problem: - API is not publicly reachable - Stripe cannot POST webhook events Observed behavior: - `stripeCustomerId` set (checkout path) - `subscriptionStatus` remains null - `plan` remains null --- ## Immediate Next Steps ### Option A (Dev Testing - Recommended) Use Stripe CLI forwarding: ```bash stripe listen --forward-to http://10.60.0.18:4000/api/billing/webhook ``` Then: - copy CLI-provided `whsec_...` - set as `STRIPE_WEBHOOK_SECRET` - restart API - re-run test checkout Expected result: - webhook hits API - DB updates correctly --- ### Option B (Staging/Prod Path) Expose only webhook endpoint via public domain: Example: ``` https://billing.zerolaghub.com/api/billing/webhook ``` Requirements: - real reverse proxy or tunnel (NOT DNS redirect) - HTTPS - Stripe signature verification enabled --- ## Important Notes ### Namecheap Redirect - simple DNS/URL redirect is NOT sufficient - Stripe requires direct POST endpoint - must use proxy/tunnel instead ### Stripe CLI - does NOT need to run on API VM - but easiest if it does - must be able to reach internal API IP ### Test Cards Use Stripe test card: ``` 4242 4242 4242 4242 ``` --- ## Known Good - Checkout flow: WORKING - Customer creation: WORKING - Portal integration: WORKING enough for testing ## Known Broken - Webhook delivery: NOT working - Subscription state persistence: NOT working --- ## Next Session Starting Point 1. Get Stripe CLI installed and running 2. Verify webhook hits API 3. Confirm DB updates: - `subscriptionStatus` - `plan` 4. Refresh portal billing page 5. Verify UI reflects new state --- ## Future Work (Post-Webhook) - Implement trial handling (`trialing` state) - Add quota enforcement (soft gate) - Improve billing UI states - Add billing portal button behavior - Consider exposing webhook endpoint cleanly for production --- ## Summary The system is effectively complete except for webhook reachability. Once webhooks are flowing, billing becomes fully functional end-to-end.