Knowledge > Processes > Checkout Flow
Checkout Flow
How a customer goes from clicking "Buy" to having an active subscription with admin access. This document covers ALL four properties: ChurchWiseAI, PewSearch, IllustrateTheWord, and SermonWise. All share ONE Stripe account (churchwiseai@gmail.com) with separate webhook endpoints.
Property-by-Property Flows
A. ChurchWiseAI (churchwiseai.com)
ChurchWiseAI has TWO checkout paths: the generic pricing page and the church-specific admin upgrade.
Path 1: Pricing Page Checkout (New Customer)
-
Visitor browses churchwiseai.com/pricing.
-
Selects a plan (e.g., Pro Chat $34.95/mo, Voice Starter $39.95/mo, Bundle $79.95/mo).
-
Clicks "Get Started" which links to
/api/stripe/checkout?price=<price_key>. -
The checkout route: a. Validate the
pricequery parameter against thePRICE_IDSmap. Return 400 if invalid. b. Look up the actual Stripe price ID for this key. c. Determine if this is a one-time purchase (AI Starter Kit $4.95) or subscription. d. Determine trial eligibility:- Chat plans (starter_chat, pro_chat, suite_chat + annual variants) get 14-day free trial
- Voice plans and bundles do NOT get a trial (per-minute Cartesia costs)
- One-time purchases do not get a trial
e. Build session metadata:
product= price key,user_id(if provided),tier(if provided). f. Create a Stripe Checkout session: - Mode: "subscription" (or "payment" for one-time)
- Trial: 14 days for eligible plans
- Metadata propagated to both session and
subscription_data - Success URL:
/thank-you?session_id={CHECKOUT_SESSION_ID}&product=<key> - Cancel URL:
/pricing - Promo codes allowed g. Redirect (303) to the Stripe Checkout URL.
-
Customer completes payment on Stripe's hosted checkout page.
-
Stripe redirects to
/thank-youon success. -
Stripe fires
checkout.session.completedwebhook to/api/stripe/webhook.
Path 2: Church-Specific Checkout (Admin Upgrade)
-
An existing church admin clicks "Upgrade" in the admin dashboard or onboard flow.
-
This calls
/api/stripe/church-checkout?token=<admin_token>&tier=<plan>. -
The church-checkout route: a. Authenticate — resolve the admin token via
resolveTokenOrHeaders(). Return 401 if invalid. b. Validate tier — check againstisValidPlanKey(). Return 400 if invalid. c. Look up price ID — viagetPriceIdForTier(). Return 500 if no price configured. d. Upsert premium_churches — create row if missing, return admin_token for redirect. -
Upgrade path (existing active subscription): a. If the church already has
status = "active"AND astripe_subscription_id:- Retrieve the existing subscription from Stripe
- If it is active/trialing in Stripe:
- If already on the requested price, redirect to dashboard (no-op)
- Otherwise, update the subscription in-place with proration (no new checkout session)
- Redirect to
/admin/[token]?upgraded=trueb. This prevents double billing (BUG-051).
-
New subscription path (no existing subscription): a. Create Stripe Checkout session:
- Chat plans get 14-day trial
- Metadata:
church_id,premium_id,tier - Success URL:
/admin/[token]?activated=true - Cancel URL:
/pricing - Pre-fill customer if we have a stripe_customer_id from previous subscription
- Support for promo codes via
promoquery parameter b. Redirect to Stripe Checkout.
CWA Webhook Processing
-
The CWA webhook at
/api/stripe/webhookhandles these events:checkout.session.completed: a. Extractchurch_idandtierfrom session metadata. b. CallactivateChurch():- Idempotency: skip if same subscription_id already recorded.
- Normalize tier via
normalizePlanKey()(maps to plan + channel). - Update
premium_churches: status=active, plan, channel, activated_at, stripe IDs. - Set
is_premium=trueonchurchestable. - Auto-provision chatbot: create
organization_settingsrow if not exists (so the chatbot works immediately without manual "Enable" step). Skip if already exists (idempotent). - Create admin team member: insert into
church_team_memberswith role=admin (for Team Members list in dashboard). Skip if admin already exists. - Voice plan provisioning: if the plan includes voice (voice_starter, voice_pro, bundle), create a
church_voice_agentsstub with default settings. Send a voice setup alert email to founder (Twilio number needs manual provisioning). - Send welcome email with magic link to
/admin/[token]. Retries 3 times with 2-second delays. Includes voice setup info if plan includes voice. - Skip welcome email for upgrades (admin already has access). c. Add admin to MailerLite "cwa-customers" group (non-blocking).
customer.subscription.deleted: a. Find premium record bystripe_subscription_id. b. Set status = "cancelled". c. Setis_premium = falseon churches table. d. Add to MailerLite "cwa-cancelled" group.customer.subscription.updated: a. Find premium record bystripe_subscription_id. b. If subscription is past_due/unpaid --> set status = "expired", is_premium = false. c. If subscription is active --> update plan/channel from metadata, set status = "active".invoice.payment_failed: a. Find premium record by subscription_id or customer_id. b. Set status = "past_due". c. Create a Stripe billing portal session. d. Send payment failed email with portal link.customer.subscription.trial_will_end: a. Find premium record by subscription_id. b. Send trial ending email with admin token link.AI Starter Kit (one-time purchase): a. If
product = "ai_starter_kit", send starter kit delivery email. b. Add to MailerLite "starter-kit" group.SermonWise Pro (handled by CWA webhook, not a separate endpoint): a. If
product = "sermon_pro", updateprofiles.subscription_tier= "sermon_pro". b. On delete: set back to "free". c. On update past_due: set to "free". On re-active: set to "sermon_pro". d. Add to MailerLite "sermonwise-pro" group.ShareWiseAI (handled by CWA webhook): a. If
product = "social", updatesocial_subscriptionstable with tier, status, Stripe IDs. b. On delete: set tier="free", status="cancelled". c. On update: handle past_due and reactivation. d. Add to MailerLite "sharewise-paid" group.
B. PewSearch (pewsearch.com)
PewSearch has ONE checkout path via the claim flow (see claim-flow.md for full detail).
-
The claim form submits to
/api/stripe/pre-checkoutwhich: a. Creates/upserts apremium_churchesrow. b. Creates a Stripe customer. c. Creates a Stripe Checkout session with metadata:church_id,premium_id,tier. d. No free trial (PewSearch plans are not trial-eligible). e. Returns the checkout URL. -
After payment, the PewSearch webhook at
/api/stripe/webhookhandles:checkout.session.completed: a. Validate metadata (church_id + premium_id must match a real record). b. Skip CWA product tiers (filtered byCWA_TIERSarray). c. CallactivateChurch():- Idempotency check (same subscription_id = skip).
- Update premium_churches: status=active, plan=tier, activated_at, Stripe IDs.
- Pro Website: set website_template="protestant_modern", chatbot_enabled=true.
- Set is_premium=true on churches.
- Bust page cache (revalidate church page, claim page, vanity page).
- Send welcome email with magic link (3 retries).
customer.subscription.created(backup activation): a. Same activation logic — handles the edge case where checkout webhook is missed. b. Also skips CWA tiers.customer.subscription.deleted: a. Set status="cancelled", plan="free". b. Clear: website_template, chatbot_enabled, vanity_slug. c. Set is_premium=false. d. Revalidate cached pages.customer.subscription.updated: a. Past_due/unpaid --> status="expired". b. Active --> update plan from metadata, status="active".customer.subscription.trial_will_end: a. Send trial reminder email.invoice.payment_failed: a. Set status="past_due". b. Send payment failure email.
C. IllustrateTheWord (illustratetheword.com)
-
ITW uses Supabase Auth (not token-based). User must be logged in.
-
User visits
/pricing, selects monthly ($9.95) or annual ($99.50/yr). -
Clicks "Subscribe" which POSTs to
/api/stripe/checkout: a. Verify Supabase auth — must have a logged-in user. Return 401 if not. b. Determine billing cycle (monthly or annual). c. Look up price ID from thePLANSconfig. d. Check for existing Stripe customer on theuser_subscriptionsrecord. e. Create Stripe customer if none exists (withsupabase_user_idin metadata). f. Create Stripe Checkout session:- Customer: existing or new
- Success URL:
/profile?subscription=success - Cancel URL:
/pricing?subscription=cancelled - Metadata:
supabase_user_id - No trial period g. Return the checkout URL (client redirects).
-
After payment, the ITW webhook at
/api/stripe/webhookhandles:checkout.session.completed: a. Extractsupabase_user_idfrom session metadata. b. Retrieve the full subscription from Stripe to get the price ID. c. Look up thepricing_tier_idfrom thepricing_tierstable by Stripe price ID. d. Upsertuser_subscriptionswith: user_id, stripe_customer_id, stripe_subscription_id, status, pricing_tier_id, current_period_end.customer.subscription.updated: a. Find the user by stripe_customer_id inuser_subscriptions. b. Upsert with new status and period end.customer.subscription.deleted: a. Find user by stripe_customer_id. b. Upsert with status="canceled".customer.subscription.trial_will_end: a. Send trial reminder email.invoice.payment_failed: a. Updateuser_subscriptionsstatus to "past_due". b. Send payment failed email.
D. SermonWise (sermonwise.ai)
- SermonWise shares the CWA codebase (hostname rewrite at
/sermons). - The checkout uses the SAME
/api/stripe/checkoutendpoint as CWA withprice=sermon_pro(orsermon_pro_annual). - The success URL is
/sermons/thank-you(hostname-aware redirect back to sermonwise.ai). - The webhook handling is in the CWA webhook (see step 13, "SermonWise Pro" section):
- Updates
profiles.subscription_tier(not premium_churches). - Uses
user_idfrom session metadata to identify the Supabase Auth user.
- Updates
Trial Handling
| Product | Trial | Duration | Notes |
|---|---|---|---|
| CWA Chat plans (Starter/Pro/Suite) | Yes | 14 days | Monthly AND annual |
| CWA Voice plans | No | -- | Per-minute Cartesia costs |
| CWA Bundle plans | No | -- | Include voice |
| PewSearch Premium/Pro Website | No | -- | |
| ITW Premium | No | -- | |
| SermonWise Pro | No | -- | |
| AI Starter Kit | N/A | -- | One-time purchase |
Trial mechanics:
subscription_data.trial_period_days = 14on eligible Stripe Checkout sessions.- 3 days before trial ends, Stripe fires
customer.subscription.trial_will_end. - CWA and PewSearch both handle this event by sending a trial reminder email.
- When the trial ends, Stripe automatically charges the card. If it fails,
invoice.payment_failedfires.
Annual vs Monthly
| Property | Monthly | Annual | Savings |
|---|---|---|---|
| CWA Chat | $14.95-$59.95 | $149.50-$599.50/yr | ~17% (pay for 10 months) |
| CWA Voice/Bundle | $39.95-$99.95 | Not available | -- |
| PewSearch | $9.95-$19.95 | Not available | -- |
| ITW | $9.95 | $99.50/yr | ~17% |
| SermonWise | $19.95 | $199.50/yr | ~17% |
Metadata Propagation
Metadata is critical for webhook processing. It flows from checkout to subscription:
Checkout Session
metadata: { church_id, premium_id, tier, product, user_id }
|
v
subscription_data.metadata: { church_id, tier } (for CWA/PewSearch)
or
session.metadata: { supabase_user_id } (for ITW)
or
session.metadata: { product: "sermon_pro", user_id } (for SermonWise)
or
session.metadata: { product: "social", user_id, tier } (for ShareWiseAI)
The webhook handler routes to the correct activation logic based on these metadata fields.
Post-Activation Summary
| Property | DB Update | Admin Access | |
|---|---|---|---|
| CWA | premium_churches status=active, plan, channel; churches is_premium=true; auto-provision chatbot + voice stub | Welcome email with magic link | /admin/[token] |
| PewSearch | premium_churches status=active, plan; churches is_premium=true; Pro Website defaults | Welcome email with magic link | /admin/[token] |
| ITW | user_subscriptions upsert with tier + Stripe IDs | None (user is already logged in) | /profile |
| SermonWise | profiles subscription_tier=sermon_pro | None (user is already logged in) | sermonwise.ai dashboard |
| ShareWiseAI | social_subscriptions tier, status, Stripe IDs | None (user is already logged in) | sharewiseai.com app |
Webhook Deduplication
All webhook handlers implement idempotency:
- CWA: Checks if
stripe_subscription_idalready matches the existing record. If so, skips. - PewSearch: Same check — if already active with same subscription_id, skips.
- ITW: Uses
upsertSubscription()which is naturally idempotent.
This prevents duplicate welcome emails and double-writes from Stripe retry behavior.