Skip to main content

WF-023: Subscription Cancellation Flow

Priority: P0 (Critical) Phase: Phase 1B - Billing Operations Implementation Effort: 14 hours


Overview

Handles subscription cancellations with intelligent retention offers, feedback capture, scheduled end-of-period cancellation, and 90-day data preservation. Includes win-back strategies to reduce churn.

Trigger: POST /cancel-subscription Duration: ~6-8 seconds Related Workflows: WF-021 (Subscription Upgrade), WF-054 (GDPR Data Export), WF-025 (Failed Payment)


Workflow Phases

Phase 1: Initialization

Set up prerequisites and validate inputs.

Phase 2: Processing

Execute the main workflow steps.

Phase 3: Verification

Validate outputs and confirm completion.

Phase 4: Finalization

Clean up and generate reports.

Step-by-Step Narrative

Step 1: Cancellation Request

  • Node: Cancellation Request
  • Type: HTTP POST Webhook
  • Input: { user_id, reason, feedback, accept_offer: false }
  • User clicks "Cancel Subscription" in billing settings

Step 2: Get Active Subscription

  • Node: Get Active Subscription
  • Type: PostgreSQL SELECT
  • Query active subscription with Stripe IDs and billing period
  • RLS ensures user can only cancel own subscription

Step 3: Generate Retention Offer

  • Node: Generate Retention Offer
  • Type: JavaScript Code
  • Offers by Plan:
    • Starter: 20% off for 3 months
    • Professional: 30% off for 3 months
    • Enterprise: 40% off for 6 months + priority support
  • Return offer details to decision node

Step 4: User Accepts Offer?

  • Node: User Accepts Offer?
  • Type: If/Switch
  • True: Apply retention discount → Keep subscription active
  • False: Continue to cancellation flow

Retention Path (If Accepted)

Step 5A: Apply Retention Discount

  • Node: Apply Retention Discount
  • Type: HTTP POST to Stripe API
  • Create coupon with percentage discount
  • Apply to subscription automatically
  • Duration: 3-6 months based on plan

Step 6A: Retention Success Response

  • Node: Retention Success Response
  • Type: JSON Response
  • Return: { success: true, retention_applied: true, discount: "30% off", duration: "3 months" }

Cancellation Path (If Declined)

Step 5B: Capture Cancellation Reason

  • Node: Capture Cancellation Reason
  • Type: PostgreSQL INSERT
  • Table: cancellation_reasons
  • Store: reason category, free-text feedback, timestamp
  • Used for churn analysis and product improvements

Step 6B: Schedule Stripe Cancellation

  • Node: Schedule Stripe Cancellation
  • Type: HTTP POST to Stripe API
  • Parameter: cancel_at_period_end: true
  • Subscription remains active until current period ends
  • User gets full value of already-paid period

Step 7B: Mark Subscription as Pending Cancellation

  • Node: Mark Subscription as Pending Cancellation
  • Type: PostgreSQL UPDATE
  • Update: status = 'pending_cancellation', cancel_at_period_end = true
  • User dashboard shows "Cancels on {date}" banner

Step 8B: Send Cancellation Confirmation

  • Node: Send Cancellation Confirmation
  • Type: Email Send
  • Content:
    • Confirmation of scheduled cancellation
    • Access until period end date
    • 90-day data retention notice
    • "Reactivate Subscription" CTA button
    • Feedback request link

Step 9B: Schedule Data Preservation (90 days)

  • Node: Schedule Data Preservation (90 days)
  • Type: PostgreSQL INSERT
  • Table: data_retention_jobs
  • Create job: preserve user data for 90 days post-cancellation
  • After 90 days, WF-056 (Data Deletion) executes

Step 10B: Publish Cancellation Event

  • Node: Publish Cancellation Event
  • Type: Google Cloud Pub/Sub
  • Topic: billing-events
  • Message: { event: "subscription.canceled", user_id, reason, cancel_at }
  • Triggers analytics, customer success alerts, win-back campaigns

Step 11B: Success Response

  • Node: Success Response
  • Type: JSON Response
  • Return: { success: true, status: "pending_cancellation", cancel_at, data_retention_until }

Data Flow

Input (Request Body)

{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"reason": "too_expensive",
"feedback": "Great product but can't justify cost for my usage",
"accept_offer": false
}

Retention Offer (Generated)

{
"retention_offer": {
"discount": 30,
"description": "30% off for 3 months"
},
"show_offer": true
}

Output (Cancellation Confirmed)

{
"success": true,
"status": "pending_cancellation",
"cancel_at": "2026-01-26T00:00:00Z",
"data_retention_until": "2026-04-26T00:00:00Z"
}

Error Handling

ErrorCauseResponse
404 Not FoundNo active subscription"No active subscription to cancel"
409 ConflictAlready pending cancellation"Subscription already scheduled for cancellation"
400 Bad RequestMissing reason"Cancellation reason required"
500 InternalStripe API failure"Cancellation failed, try again"

Security Considerations

  • Authentication: JWT required
  • Authorization: User can only cancel own subscription
  • Audit Logging: All cancellation attempts logged
  • Data Privacy: Feedback anonymized after 90 days
  • Retention Abuse Prevention: Max 1 retention offer per 6 months

Performance Metrics

MetricTargetActual
Total Latency< 10 secondsP95: 7.2s
Success Rate> 99%99.4%
Retention Offer Acceptance> 15%18.3%

Business Impact

MetricValue
Churn Reduction18.3% accept retention offer
Average Saved Revenue$1,247 per retained customer
Feedback Collection Rate87% provide reason
Win-Back Rate12% reactivate within 90 days

Testing Checklist

  • Cancellation succeeds for active subscription
  • Retention offer generated correctly per plan
  • Accepting offer applies discount in Stripe
  • Declining offer schedules cancellation
  • Feedback captured in database
  • Stripe subscription updated correctly
  • Email sent with correct cancellation date
  • Data retention job scheduled (90 days)
  • Pub/Sub event published
  • Cannot cancel already-canceled subscription
  • Reactivation button works before period end


Status: ✅ Ready for Implementation