Newsletter Consent

NDPA Section 26 affirmative opt-in for newsletter signups — no pre-checked boxes, double opt-in compatible.

Open in StackBlitzOpen in CodeSandbox

Most newsletter signups in Nigerian apps still fail NDPA Section 26. The classic trap is a pre-checked "subscribe me" box on the signup form — that's inferred consent, which 26(7)(a) explicitly disallows. The fix is small but easy to get wrong.

Correct opt-in shape

'use client';
import { useState } from 'react';
import { sanitizeInput } from '@tantainnovative/ndpr-toolkit/core';

export function NewsletterForm() {
  const [email, setEmail] = useState('');
  const [marketing, setMarketing] = useState(false);
  const [transactional, setTransactional] = useState(false);

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    // Section 26 requires consent to be SPECIFIC to a stated purpose.
    // We collect two separate opt-ins, both default unchecked.
    await fetch('/api/newsletter', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: sanitizeInput(email),
        consents: {
          marketing,
          transactional,
        },
        version: 'newsletter-2026-05',
        timestamp: Date.now(),
      }),
    });
  }

  return (
    <form onSubmit={onSubmit} className="space-y-3">
      <label className="block">
        <span className="block text-sm font-medium">Email</span>
        <input
          type="email"
          required
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="mt-1 w-full rounded border px-3 py-2"
        />
      </label>

      {/* DEFAULT UNCHECKED. Each consent is its own line with a clear purpose. */}
      <label className="flex items-start gap-2 text-sm">
        <input type="checkbox" checked={marketing} onChange={(e) => setMarketing(e.target.checked)} />
        <span>
          <strong>Marketing emails.</strong> Send me product updates, promotions, and offers.
          You can unsubscribe anytime from the footer of any email (NDPA Section 26).
        </span>
      </label>

      <label className="flex items-start gap-2 text-sm">
        <input type="checkbox" checked={transactional} onChange={(e) => setTransactional(e.target.checked)} />
        <span>
          <strong>Account emails.</strong> Receipts, security alerts, and account changes.
          Required for service — see our <a href="/privacy">privacy policy</a>.
        </span>
      </label>

      <button type="submit" className="px-4 py-2 rounded bg-blue-600 text-white">
        Subscribe
      </button>
    </form>
  );
}

Why two checkboxes?

NDPA Section 26(7)(a) requires consent to be specific and not based on a pre-selected confirmation. A single "send me everything" box bundling marketing with transactional emails fails both tests. Splitting them lets the user separately consent to marketing (which they can withdraw) while still receiving transactional emails (necessary for the service contract under Section 25(1)(b)).

Double opt-in

Most email providers (Mailchimp, Brevo, ConvertKit, Resend) support double opt-in out of the box. NDPA doesn't require it, but it's strong evidence of the consent burden of proof under Section 26(1) and is also recommended by NDPC guidance. Keep the "confirm subscription" email template short:

Subject: Confirm your subscription to Acme NG

You requested updates from Acme NG.

Confirm: [https://acme.ng/confirm?token=…]

If you didn't sign up, ignore this email — no record will be kept.
Acme NG · Lagos, Nigeria · privacy@acme.ng

Audit trail

Persist { email, consents, version, timestamp, ipAddress, userAgent } for every signup. Section 26(1) puts the burden of proof on you to show consent was given. Use the useConsent + createAuditEntry helpers from /server if you want toolkit-managed records:

import { createAuditEntry, appendAuditEntry } from '@tantainnovative/ndpr-toolkit/server';

// in your /api/newsletter route handler:
const entry = createAuditEntry({
  subjectId: email,
  action: 'consent_given',
  consents: body.consents,
  version: body.version,
  ipAddress: req.headers.get('x-forwarded-for'),
  userAgent: req.headers.get('user-agent'),
});
await appendAuditEntry(entry, /* your storage */);

Related