• Sep 2, 2026
  • 5 min read

One Checkbox, Two Problems: Unbundling Consent on a Lead-Magnet Form

Not legal advice. This is what I changed on my own lead magnet as a sole trader in Poland, and why. Have a lawyer review anything you rely on.

My website-audit tool is the best lead magnet I have: you give it a URL, a name and an email, and a few seconds later you get a real report — Lighthouse, SEO, security headers, broken links. When I moved it onto my central lead pipeline, I read the form one more time, slowly. The single required checkbox said:

I agree to the Terms & Privacy Policy. I understand results are sent via email and I accept marketing comms (unsubscribe anytime).

Two sentences, two problems. And a third one hiding in the backend.

You couldn’t get the audit without ticking that box, and the box included “I accept marketing comms”. That is bundled consent: agreeing to marketing was a condition of receiving the thing you asked for.

Under GDPR, consent has to be freely given, and tying it to a service that doesn’t need it is the textbook example of consent that isn’t. Poland adds its own rule on top: commercial email needs prior consent, B2B included (I wrote about that in GDPR-friendly lead capture). So every “yes” that checkbox collected was a yes I couldn’t rely on.

Problem 2: a promise the code never kept

“Results are sent via email.” I went looking for the email code in the Express backend. There is none. The audit runs, the server returns a JSON payload and a tokenized, expiring report link, and the browser shows the report on screen. Nothing is ever mailed.

So the form asked people to agree to something that didn’t happen, while the thing that did happen (storing their lead and audit history) was described elsewhere. Copy written early, code that evolved, nobody re-reading the checkbox.

Problem 3: the CSV said YES for everyone

The backend keeps a local CSV as a fallback next to PocketBase. The writer looked like this:

await csvWriter.writeRecords([{
    timestamp: new Date().toISOString(),
    name, email, website: safeUrl,
    score: reportData.overall_score,
    consent: 'YES'
}]);

consent: 'YES', hard-coded. The server never received a consent value from the browser at all — the checkbox only enabled the submit button. If I had ever used that file as “people who agreed to marketing”, it would have been every single person who ran an audit.

The fix: two boxes, recorded honestly

In the modal, one required box and one optional box:

  • Required: I agree to the Terms & Privacy Policy. Your report appears here as soon as it is ready. — true, and nothing more.
  • Optional, unticked by default: Also send me occasional website tips and offers. One confirmation email first; unsubscribe anytime.

The optional box is its own piece of state, sent with the request:

const [marketing, setMarketing] = useState(false);
// …
body: JSON.stringify({ ...formData, options, turnstileToken, marketing }),

Not ticking it changes nothing about the audit. The submit button only waits for the required box.

In the backend, the CSV now stores what actually happened:

// Terms/privacy acceptance is required to run an audit; marketing is the optional box.
consent: marketing === true ? 'TERMS+MARKETING' : 'TERMS'

In the lead pipeline, the backend forwards the audit as a lead with consent_marketing: marketing === true. A tick doesn’t put anyone on a list: it starts double opt-in, so the person gets one confirmation email and nothing else until they click it. How that server-to-server forward works is the next post.

The privacy page had to follow the code

The old privacy page listed “a local CSV fallback”, “Umami for analytics when configured” and “contact you about the submitted site when you have consented”. I rewrote it against what the code does today:

  • what is collected, including which boxes you ticked;
  • the report is shown on screen; report links expire after 24 hours and the generated report files are deleted after 7 days (the server’s defaults);
  • lead and audit records: up to 24 months;
  • the email provider is involved only if you ticked the optional box;
  • the legal basis for each purpose, and the right to complain to the Polish data protection authority.

Writing it surfaced one more gap: the database had no automatic clean-up for the 24-month promise. That went on the roadmap rather than staying a sentence nobody enforces.

Checking it in a real browser

I didn’t want to trust the JSX by reading it, so I built the site and drove the modal in headless Chromium with the audit endpoint mocked (the same approach as in testing form states with Playwright):

consent boxes:
  I agree to the Terms & Privacy Policy. Your report appears here as soon as it is ready. [required=true]
  Optional: also send me occasional website tips and offers. … [required=false]
submit enabled with only the required box: true
payload marketing: false | keys: name,email,website,options,turnstileToken,marketing

Required box alone is enough to run an audit; the payload carries an explicit marketing: false. Then one real audit on production with both boxes ticked: the report appeared, the lead arrived as pending, and the confirmation email followed.

Takeaways

  • Read your checkboxes against your code. Mine promised an email the server never sent and asked for consent the server never recorded.
  • Required box = what the service needs. Optional box = marketing. Unticked by default, and the service works the same without it.
  • Record the real choice, not a constant. consent: 'YES' hard-coded is worse than no field at all, because it looks trustworthy.
  • A tick is the start of consent, not the end. Double opt-in turns it into something you can prove.
  • Update the privacy page in the same change, and treat every sentence in it as a requirement the code must meet.

Next: Server-to-server leads: a key for kinds only the backend may send