Skip to Content
Compliance

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:

  1. Records a manual_cancellation_requested outcome on the session row in a single database transaction.
  2. Stamps a deterministic manual_cancellation_request_id so double-clicks and retries collapse to one request.
  3. Creates a merchant dashboard task with the subscription ID, customer ID, and the reason the automated path was blocked.
  4. Enqueues a customer confirmation email and a merchant webhook through a durable outbox.
  5. 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.

StateWhat happened on StripeCustomer copyCounts as a cancel in your analytics?
cancel_scheduledcancel_at_period_end: true”Subscription will end on <date>.”Yes
cancel_already_scheduledAlready scheduled before the session”Subscription will end on <date>.”Yes (counted on the original event)
manual_cancellation_requestedNothing”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 (test or live).
  • 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_canceltrue if the customer used the persistent Cancel Now CTA, false if they reached cancel through the standard waterfall.
  • For manual requests: the deterministic manual_cancellation_request_id, the manual_cancellation_request_at timestamp, and the merchant_manual_cancellation_notified_at timestamp.

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, and merchant_manual_cancellation_notified_at in 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

Last updated on