Skip to main content

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)

  1. Visitor browses churchwiseai.com/pricing.

  2. Selects a plan (e.g., Pro Chat $34.95/mo, Voice Starter $39.95/mo, Bundle $79.95/mo).

  3. Clicks "Get Started" which links to /api/stripe/checkout?price=<price_key>.

  4. The checkout route: a. Validate the price query parameter against the PRICE_IDS map. 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.
  5. Customer completes payment on Stripe's hosted checkout page.

  6. Stripe redirects to /thank-you on success.

  7. Stripe fires checkout.session.completed webhook to /api/stripe/webhook.

Path 2: Church-Specific Checkout (Admin Upgrade)

  1. An existing church admin clicks "Upgrade" in the admin dashboard or onboard flow.

  2. This calls /api/stripe/church-checkout?token=<admin_token>&tier=<plan>.

  3. The church-checkout route: a. Authenticate — resolve the admin token via resolveTokenOrHeaders(). Return 401 if invalid. b. Validate tier — check against isValidPlanKey(). Return 400 if invalid. c. Look up price ID — via getPriceIdForTier(). Return 500 if no price configured. d. Upsert premium_churches — create row if missing, return admin_token for redirect.

  4. Upgrade path (existing active subscription): a. If the church already has status = "active" AND a stripe_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=true b. This prevents double billing (BUG-051).
  5. 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 promo query parameter b. Redirect to Stripe Checkout.

CWA Webhook Processing

  1. The CWA webhook at /api/stripe/webhook handles these events:

    checkout.session.completed: a. Extract church_id and tier from session metadata. b. Call activateChurch():

    • 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=true on churches table.
    • Auto-provision chatbot: create organization_settings row 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_members with 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_agents stub 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 by stripe_subscription_id. b. Set status = "cancelled". c. Set is_premium = false on churches table. d. Add to MailerLite "cwa-cancelled" group.

    customer.subscription.updated: a. Find premium record by stripe_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", update profiles.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", update social_subscriptions table 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).

  1. The claim form submits to /api/stripe/pre-checkout which: a. Creates/upserts a premium_churches row. 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.

  2. After payment, the PewSearch webhook at /api/stripe/webhook handles:

    checkout.session.completed: a. Validate metadata (church_id + premium_id must match a real record). b. Skip CWA product tiers (filtered by CWA_TIERS array). c. Call activateChurch():

    • 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)

  1. ITW uses Supabase Auth (not token-based). User must be logged in.

  2. User visits /pricing, selects monthly ($9.95) or annual ($99.50/yr).

  3. 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 the PLANS config. d. Check for existing Stripe customer on the user_subscriptions record. e. Create Stripe customer if none exists (with supabase_user_id in 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).
  4. After payment, the ITW webhook at /api/stripe/webhook handles:

    checkout.session.completed: a. Extract supabase_user_id from session metadata. b. Retrieve the full subscription from Stripe to get the price ID. c. Look up the pricing_tier_id from the pricing_tiers table by Stripe price ID. d. Upsert user_subscriptions with: 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 in user_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. Update user_subscriptions status to "past_due". b. Send payment failed email.


D. SermonWise (sermonwise.ai)

  1. SermonWise shares the CWA codebase (hostname rewrite at /sermons).
  2. The checkout uses the SAME /api/stripe/checkout endpoint as CWA with price=sermon_pro (or sermon_pro_annual).
  3. The success URL is /sermons/thank-you (hostname-aware redirect back to sermonwise.ai).
  4. The webhook handling is in the CWA webhook (see step 13, "SermonWise Pro" section):
    • Updates profiles.subscription_tier (not premium_churches).
    • Uses user_id from session metadata to identify the Supabase Auth user.

Trial Handling

ProductTrialDurationNotes
CWA Chat plans (Starter/Pro/Suite)Yes14 daysMonthly AND annual
CWA Voice plansNo--Per-minute Cartesia costs
CWA Bundle plansNo--Include voice
PewSearch Premium/Pro WebsiteNo--
ITW PremiumNo--
SermonWise ProNo--
AI Starter KitN/A--One-time purchase

Trial mechanics:

  • subscription_data.trial_period_days = 14 on 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_failed fires.

Annual vs Monthly

PropertyMonthlyAnnualSavings
CWA Chat$14.95-$59.95$149.50-$599.50/yr~17% (pay for 10 months)
CWA Voice/Bundle$39.95-$99.95Not available--
PewSearch$9.95-$19.95Not 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

PropertyDB UpdateEmailAdmin Access
CWApremium_churches status=active, plan, channel; churches is_premium=true; auto-provision chatbot + voice stubWelcome email with magic link/admin/[token]
PewSearchpremium_churches status=active, plan; churches is_premium=true; Pro Website defaultsWelcome email with magic link/admin/[token]
ITWuser_subscriptions upsert with tier + Stripe IDsNone (user is already logged in)/profile
SermonWiseprofiles subscription_tier=sermon_proNone (user is already logged in)sermonwise.ai dashboard
ShareWiseAIsocial_subscriptions tier, status, Stripe IDsNone (user is already logged in)sharewiseai.com app

Webhook Deduplication

All webhook handlers implement idempotency:

  • CWA: Checks if stripe_subscription_id already 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.