GDPR Compliant Contact Form Implementation: A Technical Guide

To build a GDPR-compliant contact form, add a mandatory consent checkbox with specific purpose language, set a defined retention period (e.g., 30 days) with automatic deletion, and implement endpoints for data access and erasure requests. Below you will find working HTML, JavaScript, and Python code for each requirement, from consent capture through automated data cleanup.

Core GDPR Requirements for Contact Forms

The GDPR establishes specific requirements for processing personal data through contact forms:

You need a valid lawful basis for processing (typically consent or legitimate interest). Data must be used only for stated purposes (purpose limitation) and you should collect only what you need (data minimization). Delete data when no longer needed (storage limitation). Users can request their data (right to access) and request deletion (right to erasure).

For a basic contact form, consent is the most straightforward lawful basis, but you must implement it correctly.

Implementation Strategy

Form HTML Structure

A GDPR-compliant contact form starts with proper HTML markup:

<form id="contact-form" action="/api/contact" method="POST">
  <div class="form-group">
    <label for="name">Name</label>
    <input 
      type="text" 
      id="name" 
      name="name" 
      required 
      minlength="2" 
      maxlength="100"
    >
  </div>

  <div class="form-group">
    <label for="email">Email Address</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      required
      maxlength="254"
    >
  </div>

  <div class="form-group">
    <label for="message">Message</label>
    <textarea 
      id="message" 
      name="message" 
      required 
      maxlength="5000"
      rows="5"
    ></textarea>
  </div>

  <div class="consent-group">
    <input 
      type="checkbox" 
      id="consent" 
      name="consent" 
      required
    >
    <label for="consent">
      I consent to my data being processed for responding to this inquiry. 
      My data will be stored for 30 days and then deleted.
    </label>
  </div>

  <button type="submit">Send Message</button>
</form>

Key points: required fields use HTML5 validation, field lengths are constrained, and the consent checkbox is mandatory with clear language.

Client-Side Validation

Add JavaScript for enhanced validation and consent confirmation:

document.getElementById('contact-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  
  const formData = new FormData(e.target);
  const consent = formData.get('consent');
  
  if (!consent) {
    alert('Please provide your consent to process your data.');
    return;
  }

  // Collect consent timestamp
  const submissionData = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
    consent_given: true,
    consent_timestamp: new Date().toISOString(),
    ip_address: await getClientIP(),
    user_agent: navigator.userAgent
  };

  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(submissionData)
    });

    if (response.ok) {
      window.location.href = '/thank-you/';
    }
  } catch (error) {
    console.error('Submission failed:', error);
  }
});

async function getClientIP() {
  try {
    const response = await fetch('https://api.ipify.org?format=json');
    const data = await response.json();
    return data.ip;
  } catch {
    return null;
  }
}

Server-Side Processing

Your backend must handle data securely and maintain compliance:

from datetime import datetime, timedelta
import json
import sqlite3

class ContactFormHandler:
    def __init__(self, db_path='contact_submissions.db'):
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        conn = sqlite3.connect(self.db_path)
        conn.execute('''
            CREATE TABLE IF NOT EXISTS submissions (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT,
                message TEXT,
                consent_given BOOLEAN,
                consent_timestamp TEXT,
                ip_address TEXT,
                user_agent TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                deleted_at TIMESTAMP
            )
        ''')
        conn.commit()
        conn.close()
    
    def process_submission(self, data):
        # Validate consent
        if not data.get('consent_given'):
            raise ValueError('Consent is required')
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        # Store submission with 30-day retention
        cursor.execute('''
            INSERT INTO submissions 
            (name, email, message, consent_given, consent_timestamp, ip_address, user_agent, deleted_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', '+30 days'))
        ''', (
            data['name'],
            data['email'],
            data['message'],
            data['consent_given'],
            data['consent_timestamp'],
            data.get('ip_address'),
            data.get('user_agent')
        ))
        
        conn.commit()
        conn.close()
        
        return {'status': 'success'}
    
    def handle_deletion_request(self, email):
        """Handle GDPR right to erasure"""
        conn = sqlite3.connect(self.db_path)
        
        # Find and anonymize matching records
        cursor = conn.execute(
            "SELECT id FROM submissions WHERE email = ?",
            (email,)
        )
        records = cursor.fetchall()
        
        for record in records:
            conn.execute(
                "UPDATE submissions SET name = '[deleted]', message = '[deleted]' WHERE id = ?",
                (record[0],)
            )
        
        conn.commit()
        conn.close()
        
        return {'status': 'erased', 'records_affected': len(records)}
    
    def cleanup_expired(self):
        """Remove data past retention period"""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.execute(
            "DELETE FROM submissions WHERE deleted_at < datetime('now')"
        )
        deleted_count = cursor.rowcount
        conn.commit()
        conn.close()
        
        return {'deleted': deleted_count}

This implementation includes automatic cleanup of data past the 30-day retention period and handles deletion requests by anonymizing records rather than immediate removal (preserving audit trails).

If you want to use contact form data for marketing, you need explicit separate consent:

<div class="consent-group">
  <input type="checkbox" id="consent_response" name="consent_response" required>
  <label for="consent_response">
    I consent to receiving a response to my inquiry.
  </label>
</div>

<div class="consent-group">
  <input type="checkbox" id="consent_newsletter" name="consent_newsletter">
  <label for="consent_newsletter">
    I would like to receive newsletters and updates (optional).
  </label>
</div>

Maintain detailed consent logs:

def log_consent_event(email, consent_type, granted, ip_address):
    conn = sqlite3.connect('consent_log.db')
    conn.execute('''
        INSERT INTO consent_log (email, consent_type, granted, ip_address, timestamp)
        VALUES (?, ?, ?, ?, ?)
    ''', (email, consent_type, granted, ip_address, datetime.now()))
    conn.commit()
    conn.close()

This creates an auditable record showing exactly when and how consent was obtained.

Data Subject Rights Implementation

GDPR grants users specific rights you must support:

Right to Access

def get_user_data(email):
    conn = sqlite3.connect('contact_submissions.db')
    cursor = conn.execute(
        "SELECT * FROM submissions WHERE email = ?",
        (email,)
    )
    records = cursor.fetchall()
    conn.close()
    
    return {
        'email': email,
        'submissions': records,
        'consent_history': get_consent_history(email)
    }

Right to Erasure

Your deletion handler should:

  1. Verify the requester’s identity
  2. Remove or anonymize all personal data
  3. Confirm completion within 30 days
  4. Notify any third parties who received the data

Security Considerations

Beyond GDPR compliance, secure your contact form against abuse:

from functools import wraps
import time

def rate_limit(max_requests=5, window_seconds=3600):
    requests_log = {}
    
    def decorator(func):
        @wraps(func)
        def wrapper(ip_address, *args, **kwargs):
            now = time.time()
            if ip_address in requests_log:
                requests_log[ip_address] = [
                    t for t in requests_log[ip_address] 
                    if now - t < window_seconds
                ]
            
            if len(requests_log.get(ip_address, [])) >= max_requests:
                raise ValueError('Rate limit exceeded')
            
            requests_log.setdefault(ip_address, []).append(now)
            return func(ip_address, *args, **kwargs)
        return wrapper
    return decorator

Compliance Checklist

Before deploying your contact form, verify:

The implementation above covers the core technical requirements; consult legal counsel for jurisdiction-specific details such as those under the UK GDPR or CCPA.


Built by theluckystrike — More at zovo.one