GDPR and Contact Forms: Consent, Retention and What You Must Log
A practitioner’s guide to the GDPR contact form: why consent checkboxes are usually wrong, which legal basis to use, retention periods that hold up, and what
Last year we audited the lead capture setup on eleven client sites before migrating them. Nine had a checkbox that said “I agree to the privacy policy”. Seven of those were required fields. Every single one was legally useless, and two were actively creating a problem the client didn’t know they had.
Here’s the thing nobody tells you in the listicles: a GDPR contact form in most cases should not have a consent checkbox at all. Consent is one of six legal bases, it’s the most fragile one, and reaching for it by default is how you end up unable to reply to your own enquiries. If someone fills in a form that says “contact me about a quote”, you don’t need their permission to contact them about a quote.
What you do need is a legal basis you can name, a retention period you actually enforce, and a record you can produce when a regulator or a client’s procurement team asks. Most sites have none of the three.
- Contact forms normally run on Article 6(1)(b) (pre-contractual steps) or 6(1)(f) (legitimate interests), not consent. A required “agree to privacy policy” box is not valid consent and adds friction for nothing.
- Consent is genuinely required for marketing email opt-in and for non-essential scripts on the form page. That means reCAPTCHA loading before a cookie decision is a real compliance gap, not a theoretical one.
- Article 7(1) says you must be able to demonstrate consent, so record the exact wording version, timestamp, form URL and method, not just a boolean
1. - GDPR sets no retention number. Pick one per data category, write it in the privacy notice, then enforce it with a scheduled job. An unenforced policy is worse than no policy.
- The hard part of erasure is fan-out: one submission lands in an inbox, an ESP, a CRM, Slack and a backup. Map the destinations before you promise anyone a 30 day deletion.
Your contact form probably doesn’t need a consent checkbox
Consent under GDPR has to be freely given, specific, informed, unambiguous and as easy to withdraw as it was to give. Tie your enquiry handling to it and you get an absurd outcome: the moment someone withdraws consent, you have to stop processing, which includes replying to their question. You’ve built a form that legally cannot be answered.
Use the basis that matches reality:
- Article 6(1)(b), steps prior to entering a contract. Quote requests, “book a call”, project enquiries. The person asked you to do something. That’s the basis.
- Article 6(1)(f), legitimate interests. General enquiries, support questions, press contacts. Document a short legitimate interests assessment: purpose, necessity, balancing test. Half a page is fine. Nobody needs twelve.
- Article 6(1)(a), consent. Marketing. Newsletter signup. Adding an enquirer to a nurture sequence they didn’t ask for. That’s it.
The “I accept the privacy policy” box is a category error. A privacy notice is information you provide under Article 13. It isn’t a contract and it isn’t consent. Making it a required field just means every user clicks it without reading, which is precisely the behaviour that invalidates consent anyway. Delete the box. Replace it with a one-line notice and a link.
The one exception worth keeping
If you operate in a sector with an explicit written-record culture (recruitment, health, financial advice) a tick box that logs an acknowledgement can be useful evidence even when it isn’t the legal basis. Label it honestly: “I have read the privacy notice”, not “I consent”. Don’t block submission on it unless your compliance team insists.

What has to appear at the point of collection
Article 13 requires the information to be given when the data is obtained. A link in the footer is technically compliant and practically weak. A short disclosure directly under the submit button takes about 40 words and links out for the detail.
<form action="https://webforms.to/submit/YOUR_ID" method="POST">
<label for="cf-name">Name</label>
<input id="cf-name" name="name" type="text" autocomplete="name" required>
<label for="cf-email">Email</label>
<input id="cf-email" name="email" type="email" autocomplete="email" required>
<label for="cf-message">How can we help?</label>
<textarea id="cf-message" name="message" rows="5" required></textarea>
<!-- Separate, optional, unticked. This is the only real consent on the page. -->
<label class="form-check">
<input type="checkbox" name="marketing_consent" value="yes">
Email me occasional articles and product updates. Unsubscribe any time.
</label>
<!-- Versioned so you can prove what the user actually saw -->
<input type="hidden" name="consenttextversion" value="2026-01-a">
<input type="hidden" name="form_url" value=""> <!-- populated by JS on load -->
<button type="submit">Send enquiry</button>
<p class="small text-muted">
We use your details to reply to this enquiry and keep it on file for 12 months.
We never sell data. <a href="/privacy">Privacy notice</a> ·
<a href="/privacy#rights">Your rights</a>
</p>
</form>
Note what is absent: no phone field, no company size dropdown, no “how did you hear about us”. Article 5(1)(c) is data minimisation, and every field you add is a field you now have to justify, secure, retain and delete. If you genuinely need the phone number to run the business, keep it and say why. If it’s for a sales team’s curiosity, cut it. Conversion goes up too, which is a pleasant coincidence.
The consent you do need, and how to record it
Article 7(1) puts the burden on you: you must be able to demonstrate that the person consented. A column in your database containing 1 proves nothing. Six months from now you will not remember whether the checkbox was pre-ticked, what the label said, or whether it was bundled with the terms.
Store a record, not a flag:
<?php
// Minimal consent record. Store alongside the submission, never overwrite it.
$consent = [
'subject_email' => $email,
'purpose' => 'marketing_email',
'given' => isset($POST['marketingconsent']),
'wordingversion' => sanitizetextfield($POST['consenttextversion'] ?? ''),
'wordingsnapshot'=> getoption('consenttext202601a'), // exact string shown
'method' => 'webformcheckbox_unticked',
'sourceurl' => escurlraw($POST['form_url'] ?? ''),
'timestamp_utc' => gmdate('c'),
// IP is optional. Only keep it if you can justify it, and truncate the last octet.
'iptruncated' => pregreplace('/\.\d+$/', '.0', $SERVER['REMOTEADDR']),
];
Withdrawal gets the same treatment: append a new record with given => false rather than flipping the original. You want an audit trail, and an append-only consent log is the cheapest one to build. If you run newsletter signups, the same record structure covers double opt-in confirmation. Our notes on newsletter forms that convert without annoying people cover the UX side of that.
Form data retention: pick a number and enforce it in code
There is no statutory retention period for enquiry data. Regulators expect you to define one, justify it and stick to it. That last part is where nearly everyone fails. The policy lives in a PDF; the data lives in a database with no cron job attached to it.
Our default schedule, which has survived several client security reviews:
- Unconverted enquiries: 12 months from last contact, then hard delete. Long enough for a slow buying cycle, short enough to be defensible.
- Quoted but lost: 24 months. Proposals get revived.
- Converted to client: moves to the contract basis. Financial records retained per local tax law (6 years in the UK, commonly 7 to 10 elsewhere in the EU). The enquiry record itself still ages out.
- Marketing consent records: kept for the life of the relationship plus 24 months after withdrawal, as evidence that the withdrawal was honoured.
- Spam and failed validations: 30 days maximum. There is no reason to keep these at all beyond debugging.
Then wire it up. This is a WordPress example, but the shape is identical anywhere:
<?php
add_action('init', function () {
if (!wpnextscheduled('swpurgeenquiries')) {
wpscheduleevent(time(), 'daily', 'swpurgeenquiries');
}
});
addaction('swpurge_enquiries', function () {
global $wpdb;
$table = $wpdb->prefix . 'enquiries';
// Hard DELETE, not a status change. "Archived" rows are still personal data.
$deleted = $wpdb->query(
"DELETE FROM {$table}
WHERE status = 'open'
AND lastcontactat < (NOW() - INTERVAL 12 MONTH)"
);
// Log the count and the rule, never the deleted content itself.
errorlog(sprintf('[retention] rule=enquiry12m deleted=%d', (int) $deleted));
});
Soft deletes are the trap. A row with deleted_at set is still personal data, still discoverable in a subject access request, still in scope for a breach. If your ORM only soft deletes, add a second job that purges tombstones after 30 days.
What you must log, and what you must stop logging
Accountability under Article 5(2) means you can’t just be compliant, you have to show it. Four records do the heavy lifting:
- Record of Processing Activities (Article 30). A spreadsheet. Purpose, categories of data, categories of recipients, retention period, transfers, security measures. The under-250-employees exemption almost never applies to an agency because processing enquiries is regular, not occasional. Build it anyway: procurement teams ask for it constantly.
- Consent records. As above, append-only, with wording snapshots.
- Rights request log. Date received, identity check method, date responded. You have one month to respond to an access or erasure request, extendable by two for complex cases, and you must tell the person about the extension within the first month.
- Breach register. Every incident, including ones you decide not to report. Article 33 gives you 72 hours to notify the supervisory authority from the point of awareness. The register is where you document the decision not to.
Now the deletions. Stop putting personal data in places that log by default. Form submissions must be POST, never GET, because query strings land in nginx access logs, browser history, Referer headers and any analytics script on the page. Turn off full request body logging in your app framework. Check whether your error monitoring captures POST payloads by default, because Sentry and friends often do until you scrub them. Truncate IPs at collection rather than at analysis time.
The fan-out problem: one submission, six copies
This is where deletion promises die. A single contact form submission on a typical build ends up in: the recipient’s email inbox, that inbox’s Sent items after a reply, the form API’s dashboard, a CRM, a Slack channel, a Google Sheet somebody set up in 2023, and last night’s database backup. Erasure means all of them.
Two things make this survivable. First, reduce destinations. Every integration you add is another controller relationship, another data processing agreement and another delete endpoint you have to call. We wrote about the trade-offs in lead capture architecture for agencies. The short version: a single source of truth with downstream notifications beats five parallel writes.
Second, keep a destination map per form. Literally a list: form ID, every system it writes to, the deletion method for each, and who owns the credentials. When the erasure request lands you follow the list instead of reconstructing the architecture from memory at 6pm on a Friday.
Backups are the standard question. Regulatory guidance, the ICO’s in particular, accepts that you cannot surgically edit a backup archive. You put the data beyond use, exclude it from any restore, and let it expire on the normal backup rotation. Tell the person that’s what you’ve done, and make sure your rotation is measured in weeks, not years. If you’re keeping 36 monthly snapshots of a WordPress database, your backup policy is now a privacy liability.
Processors and transfers
Anyone who touches the data on your behalf is a processor and needs an Article 28 agreement: your form API, your ESP, your CRM, your host. Check the sub-processor list, not just the headline DPA. For US-based services, the EU-US Data Privacy Framework is the usual mechanism and it survived its first annulment challenge at the EU General Court in 2025, but the sensible engineering position is to assume any adequacy mechanism can be struck down again and to know what your fallback is. EU-hosted endpoints remove the question entirely. If you’re building static or JAMstack sites, WebForms gives you a form endpoint with a signed DPA and no backend of your own to secure, which is one fewer system holding a copy of the data.
Captchas, analytics and the scripts on your form page
Consent for cookies and similar technologies comes from ePrivacy, not GDPR, and it bites harder. Google reCAPTCHA v3 runs on page load, reads browser signals and sets cookies before the user has done anything. If it fires before your cookie banner gets an answer, that’s a non-essential technology loaded without consent. It’s also one of the easiest things for a regulator or a competitor’s lawyer to screenshot.
Options, ranked by how we actually deploy them:
- Honeypot field plus a time-to-submit check. Zero third parties, zero cookies, zero consent questions. Reject submissions completed in under 3 seconds. This stops the overwhelming majority of bot spam on ordinary contact forms.
- Server-side rate limiting by IP and by email domain. Cheap and effective.
- Cloudflare Turnstile when you need a real challenge. Less data collection than reCAPTCHA, though it’s still a processor you must disclose.
- reCAPTCHA only if the client mandates it, and then gated behind consent, loaded on interaction rather than on page load.
The UK’s Data (Use and Access) Act 2025 carved out some low-risk analytics from cookie consent, which helps British sites slightly. It changed nothing about the fundamentals of contact forms, and it doesn’t apply to your EU visitors. Don’t let anyone tell you the rules got relaxed.
Frequently Asked Questions
Do I legally need a consent checkbox on a contact form?
Almost never. Replying to an enquiry runs on Article 6(1)(b) or 6(1)(f), so a required consent box adds friction without adding compliance. The only checkbox that belongs there is a separate, optional, unticked marketing opt-in.
How long can I keep contact form submissions?
GDPR gives no fixed period, so you choose one and justify it. For unconverted enquiries, 12 months from last contact is a widely defensible default; converted leads move onto contract and tax retention rules instead. Whatever you choose, publish it in your privacy notice and enforce it with a scheduled deletion job rather than manual cleanup.
Is storing the submitter’s IP address a GDPR problem?
IP addresses are personal data, so you need a purpose and a retention period for them like anything else. Anti-fraud and abuse prevention is a valid legitimate interest, but “we might need it someday” is not. Truncating the last octet at the point of collection keeps most of the security value with much less risk.
Does GDPR apply if my company is outside the EU?
Yes, if you’re offering goods or services to people in the EU or monitoring their behaviour. A contact form on a site that actively markets to European customers is squarely in scope under Article 3(2). Non-EU controllers in scope may also need to appoint an EU representative under Article 27.
What do I do when someone asks me to delete their enquiry?
Verify identity proportionately (replying from the original address is usually enough), then work through your destination map: database, inbox, form API dashboard, CRM, Slack and any spreadsheet. Respond within one month, confirm what was deleted, and explain that backup copies are excluded from restores and will expire on the normal rotation. Log the whole thing in your rights request register.
Where to start tomorrow
Open the contact page on your biggest client’s site. If there’s a required “I agree to the privacy policy” checkbox, remove it and write one sentence under the submit button naming the purpose and the retention period. That’s the highest-value 20 minutes available to you.
Then go find the deletion job. If there isn’t one, your form data retention policy is fiction, and fiction is exactly what fails an audit. Write the cron, run it in dry-run mode for a week, check what it would have deleted, then let it run for real.


