Compliance and audit trail
Cancel-flow tools fail compliance reviews when they pretend that “we recorded the request” is the same as “we canceled the subscription.” Unchurn keeps the two states separate. This page explains how that works, what gets logged, and where to find the audit trail.
The rules below cover FTC Click-to-Cancel, the EU Consumer Rights Directive, France and Germany’s national rules, and California (CCPA) and New York (GBS §527-A). The legal landscape moves; check the current rules with your counsel before you make claims to customers.
The two cancel paths
Every cancel flow ends in one of two paths. Both are immediate from the customer’s point of view. They differ in what happens on Stripe.
Automated cancel
The customer clicks Cancel Now (or accepts the cancel option after declining offers). Unchurn calls subscriptions.update({ cancel_at_period_end: true }) on Stripe in the same request. Stripe records the cancellation. The customer keeps access through the period they paid for.
The session row stamps outcome='canceled' or outcome='cancel_scheduled' and the merchant onComplete callback fires.
Manual cancellation request
Some subscription shapes are unsafe for an automated Stripe write — multi-item subs, attached schedules, paused-at-Stripe state, past-due invoices. For those, the cancel CTA does not call Stripe. Instead, Unchurn:
- Records a
manual_cancellation_requestedoutcome on the session row in a single database transaction. - Stamps a deterministic
manual_cancellation_request_idso double-clicks and retries collapse to one request. - Creates a merchant dashboard task with the subscription ID, customer ID, and the reason the automated path was blocked.
- Enqueues a customer confirmation email and a merchant webhook through a durable outbox.
- Shows the customer “Your cancellation request has been received” — never “your subscription is canceled.”
You complete the Stripe-side cancellation when you are ready. Until then, the customer’s intent is captured and dated, and your team has a task with everything they need to finish the work in Stripe.
The full list of subscription shapes that route to manual cancel lives on the Supported subscriptions reference page.
What “received, not completed” means
Unchurn does not market manual-request fallback as automated legal completion. Honest copy beats clever copy here.
| State | What happened on Stripe | Customer copy | Counts as a cancel in your analytics? |
|---|---|---|---|
cancel_scheduled | cancel_at_period_end: true | ”Subscription will end on <date>.” | Yes |
cancel_already_scheduled | Already scheduled before the session | ”Subscription will end on <date>.” | Yes (counted on the original event) |
manual_cancellation_requested | Nothing | ”Your cancellation request has been received.” | No — counted as a manual request |
The third row is the one most cancel-flow tools get wrong. Unchurn keeps it separate so your numbers stay honest and your compliance review stays clean.
Direct Cancel Access
When direct_cancel_access is on for a session, the cancel CTA appears one click from the cancel-flow open. No surveys, no offers, no “are you sure?” interstitial.
The flag is on by default for everyone. It becomes mandatory — and cannot be disabled — when the customer’s location resolves to one of:
- California (US-CA)
- New York (US-NY)
- Any EU member state
- France
- Germany
The location resolver runs once at session creation. It reads (in order of priority) merchant-supplied customer attributes, Stripe Tax customer location, the Stripe Customer address, payment-method billing details, and finally browser/IP geolocation as a fallback. Card issuer country is stored for audit but never used to infer state-specific labels.
When direct access is on, the Cancel Now button sits on every screen of the flow. One click takes the customer straight to either the automated cancel path or the manual cancellation request path, depending on the subscription shape. The widget never inserts retention friction — survey, offer, confirm — between that click and the result screen.
You can force direct access on for every customer by setting forceCompliance: true in your flow config. Useful if you would rather give every customer the easiest cancel UX rather than tune the resolver.
What gets logged
Every cancel-flow session writes one row to the sessions table. The row carries:
- The session ID, subscription ID, and merchant ID.
- The Stripe mode (
testorlive). - Every offer evaluation — eligible, ineligible, the reason — at session-create and again at mutation-time.
- The outcome:
canceled,cancel_scheduled,cancel_already_scheduled,saved(offer accepted),manual_cancellation_requested,dismissed,error. - The compliance jurisdictions the resolver picked, plus every signal it considered (including conflicts).
clicked_to_cancel—trueif the customer used the persistent Cancel Now CTA,falseif they reached cancel through the standard waterfall.- For manual requests: the deterministic
manual_cancellation_request_id, themanual_cancellation_request_attimestamp, and themerchant_manual_cancellation_notified_attimestamp.
Each step the customer views, accepts, or declines also gets a row in the events table. That gives you a per-step funnel — how many sessions saw the discount offer, how many accepted, how many declined and went to the next step.
The deterministic IDs prevent double-counting on retries. A network blip that causes the widget to retry the same /complete call commits exactly one Stripe mutation and writes exactly one outcome.
Where the audit trail lives
- Sessions table. One row per cancel-flow session. Surfaced in your dashboard at Cancellations with a 30-day rolling window.
- Merchant dashboard tasks. Manual cancellation requests show up as tasks tied to the subscription ID. Each task carries the reason the automated path was blocked and the timestamp.
- Customer confirmation email. Sent through the notification outbox on every manual cancellation request. Delivery is durable — first attempt inline, retried by a background worker until it succeeds.
- Audit timestamps. The session row records
manual_cancellation_request_id,manual_cancellation_request_at, andmerchant_manual_cancellation_notified_atin a single transaction. These are queryable via your dashboard. Outbound webhook delivery to your server is on the roadmap.
What Unchurn does not promise
- Automated legal completion for blocked shapes. Manual cancellation requests are tracked as received, not completed. Your team finishes the Stripe-side cancellation within your own legal SLA.
- Statement of regulatory compliance. Unchurn provides the audit trail, the routing policy, and the UI access. Your counsel decides which regulations apply to your business.
- Backfilled compliance labels. The location resolver runs once at session creation. Mutation-time changes do not retroactively change the labels on a session that already started.
Cross-references
- Supported subscriptions — the full list of subscription shapes that route to manual cancel.
- Unusual subscriptions — the rationale behind the routing rules.
- Webhook reference — the manual-cancellation webhook payload shape.