GDPR Art. 7 requires that consent be demonstrable — 'freely given, specific, informed, and unambiguous.' Sending marketing email to all registered users with no separate opt-in step fails the 'specific' and 'unambiguous' requirements for EU residents. CASL Section 10 requires either express or implied consent, and implied consent has strict time limits (24 months) and relationship conditions. Beyond legal exposure, no-opt-in flows produce high spam complaint rates because users who didn't choose to receive marketing email treat it as spam — complaint rates above 0.1% cause major providers to throttle or block the sending domain.
High because sending marketing email without an opt-in record violates GDPR Art. 7 for EU recipients and CASL S10 for Canadian recipients, and high resulting spam complaint rates cause domain-wide deliverability damage.
Implement a double opt-in flow: record a pending subscription, send a confirmation email, and only activate the subscription on confirmation click.
// Step 1: record pending + send confirmation
await db.pendingSubscription.create({
data: { email, confirmToken: crypto.randomUUID(), expiresAt: addHours(new Date(), 48) },
})
await sendEmail({
to: email,
subject: 'Confirm your subscription',
html: `<a href="https://example.com/api/newsletter/confirm?token=${token}">Confirm</a>`,
})
// Step 2: activate on confirmation click
await db.subscriber.upsert({
where: { email: pending.email },
create: { email: pending.email, confirmedAt: new Date(), optInSource: 'double-opt-in' },
update: { confirmedAt: new Date() },
})
At minimum for single opt-in: record consentedAt, optInSource, and ipAddress for every subscriber at the point of signup. Without this audit trail, you cannot demonstrate consent under GDPR.
ID: email-sms-compliance.consent.opt-in-confirmation
Severity: high
What to look for: Enumerate every relevant item. Double opt-in (also called confirmed opt-in) is the practice of sending a confirmation email after signup and only adding the subscriber to the marketing list after they click the confirmation link. While not legally required in the US under CAN-SPAM, it is required or strongly recommended for GDPR-compliant marketing email to EU residents and is a best practice that reduces spam complaints and improves deliverability. Check whether the email signup flow sends a confirmation email. Check whether the confirmation click is recorded and used as the activation condition. If single opt-in is used, check whether there is a clear audit trail: timestamp, IP address, and form source recorded at the point of signup.
Pass criteria: At least 1 of the following conditions is met. Either (a) a double opt-in flow is implemented — user receives a confirmation email, clicks a link, and is only added to the marketing list after confirmation — or (b) single opt-in is used with a complete audit record: consent timestamp, source, form location, and IP address recorded for every subscriber.
Fail criteria: Marketing emails sent to anyone who created an account with no separate opt-in. Single opt-in with no audit record (no timestamp, no source, no IP). No confirmation email sent for newsletter subscriptions.
Skip (N/A) when: The application sends no marketing email.
Detail on fail: Example: "All registered users automatically receive the weekly newsletter. No opt-in step exists — subscription is automatic at account creation." or "Newsletter opt-in checkbox present at signup but no confirmation email sent and no consent timestamp recorded.".
Remediation: Implement double opt-in for newsletter subscriptions:
// app/api/newsletter/subscribe/route.ts — step 1: record pending subscription
export async function POST(req: Request) {
const { email } = await req.json()
const token = crypto.randomUUID()
await db.pendingSubscription.create({
data: {
email,
confirmToken: token,
expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000), // 48 hours
},
})
await sendEmail({
to: email,
subject: 'Confirm your subscription to YourProduct updates',
html: `
<p>Click the link below to confirm your subscription:</p>
<a href="https://example.com/api/newsletter/confirm?token=${token}">
Confirm subscription
</a>
<p>This link expires in 48 hours. If you didn't request this, ignore this email.</p>
`,
})
return Response.json({ ok: true })
}
// app/api/newsletter/confirm/route.ts — step 2: activate on confirmation click
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const token = searchParams.get('token')
const pending = await db.pendingSubscription.findUnique({
where: { confirmToken: token! },
})
if (!pending || pending.expiresAt < new Date()) {
return Response.redirect('https://example.com/subscribe?error=expired')
}
await db.subscriber.upsert({
where: { email: pending.email },
create: {
email: pending.email,
confirmedAt: new Date(),
optInSource: 'double-opt-in',
},
update: { confirmedAt: new Date() },
})
await db.pendingSubscription.delete({ where: { confirmToken: token! } })
return Response.redirect('https://example.com/subscribe?success=1')
}