When a consumer cancels an annual subscription mid-year and receives no information about whether a prorated refund is owed, the business is exposed under EU Consumer Rights Directive 2011/83/EU Article 14 (right of withdrawal) and FTC Negative Option Rule (2025) requirements that material terms include refund rights. A 'no refund' policy is legally permissible in most jurisdictions, but only if clearly disclosed before purchase — not just in the Terms of Service. Annual subscriptions that silently absorb unused months generate disproportionate chargebacks because consumers assert their right to a refund through their bank when the product refuses.
Medium because an undisclosed or absent refund policy for annual subscriptions creates chargeback liability and EU withdrawal-right violations, but does not itself constitute the unauthorized-charge pattern triggering highest-severity FTC enforcement.
Document the refund policy on the pricing page (not only in the Terms of Service) and implement a mechanism to process prorated refunds if the policy offers them. In app/api/subscription/cancel/route.ts:
if (requestRefund) {
const totalDays = subscription.current_period_end - subscription.current_period_start
const unusedDays = subscription.current_period_end - Math.floor(Date.now() / 1000)
const refundRatio = unusedDays / totalDays
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice as string)
const refundAmount = Math.round((invoice.amount_paid ?? 0) * refundRatio)
await stripe.refunds.create({
payment_intent: invoice.payment_intent as string,
amount: refundAmount,
})
}
Add a one-line disclosure to the pricing page beneath the annual plan: '30-day money-back guarantee' or 'No refunds — cancel to stop future charges.' The specific policy is less important than its presence before purchase.
ID: subscription-compliance.cancellation.pro-rated-refund
Severity: medium
What to look for: Find where the application documents its refund policy. Check: (1) the pricing page for a refund mention, (2) the cancellation flow UI for refund terms shown during cancellation, (3) the Terms of Service for refund policy language, (4) the FAQ or help documentation. For the refund policy to be adequate, it must state: whether refunds are offered for the unused subscription period, the conditions under which a refund applies (e.g., within 30 days of billing, or for annual plans only), and the processing time. Check the Stripe account configuration for refund handling — does the API have any stripe.refunds.create calls for subscription cancellations? Check whether annual subscribers who cancel mid-year receive a prorated refund or no refund. If no refunds are offered, that policy must be clearly disclosed before purchase. Count all instances found and enumerate each.
Pass criteria: The refund policy is documented and accessible (in ToS, pricing FAQ, or cancellation flow). The policy clearly states whether a refund applies for unused subscription time and under what conditions. If no refunds are offered, this is explicitly stated before purchase. If refunds are offered, the application has a mechanism to process them. At least 1 implementation must be confirmed.
Fail criteria: No refund policy exists anywhere in the product or ToS. The refund policy is vague ("refunds at our discretion") without conditions. Annual subscribers cancel and receive no communication about whether they are owed a refund. The cancellation flow does not mention the refund policy.
Skip (N/A) when: The application has no subscription or recurring billing.
Detail on fail: Example: "No refund policy found in ToS, pricing page, or cancellation flow. Annual subscriptions are available but refund policy for annual mid-period cancellation is not stated anywhere." or "Terms of Service says 'all purchases are final' but this is not disclosed on the pricing page before purchase.".
Remediation: Document and implement a clear refund policy. For most SaaS, a 30-day money-back guarantee on the first charge is both fair and reduces chargeback risk:
// If offering pro-rated refunds via API:
// app/api/subscription/cancel/route.ts
export async function POST(req: Request) {
const { subscriptionId, requestRefund } = await req.json()
// Cancel subscription
const subscription = await stripe.subscriptions.cancel(subscriptionId)
if (requestRefund) {
// Calculate unused days
const totalDays = (subscription.current_period_end - subscription.current_period_start)
const unusedDays = subscription.current_period_end - Math.floor(Date.now() / 1000)
const refundRatio = unusedDays / totalDays
// Get last invoice amount
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice as string)
const refundAmount = Math.round((invoice.amount_paid ?? 0) * refundRatio)
await stripe.refunds.create({
payment_intent: invoice.payment_intent as string,
amount: refundAmount,
})
}
return Response.json({ cancelled: true })
}
Disclose the refund policy on the pricing page ("30-day money-back guarantee" or "No refunds — cancel to stop future charges") before the user subscribes, not only in the ToS.