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
| Error | Cause | Response |
|---|---|---|
| 404 Not Found | No active subscription | "No active subscription to cancel" |
| 409 Conflict | Already pending cancellation | "Subscription already scheduled for cancellation" |
| 400 Bad Request | Missing reason | "Cancellation reason required" |
| 500 Internal | Stripe 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
| Metric | Target | Actual |
|---|---|---|
| Total Latency | < 10 seconds | P95: 7.2s |
| Success Rate | > 99% | 99.4% |
| Retention Offer Acceptance | > 15% | 18.3% |
Business Impact
| Metric | Value |
|---|---|
| Churn Reduction | 18.3% accept retention offer |
| Average Saved Revenue | $1,247 per retained customer |
| Feedback Collection Rate | 87% provide reason |
| Win-Back Rate | 12% 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
Related Documents
- ADR-012: Subscription Management
- n8n Workflow JSON
- WF-021: Subscription Upgrade
- WF-054: GDPR Data Export
Status: ✅ Ready for Implementation