How to Prove a User Gave Consent Under GDPR
Somewhere in your app, a user clicked a button that said "Accept." In your head, that's consent. Under GDPR, it's the start of a question, not the end of one: consent to what, given when, based on what information, and can you show any of that six months from now if someone asks?
This distinction β having consent versus being able to prove it β is where a lot of otherwise well-intentioned teams get caught out. Not because they're ignoring GDPR, but because they built a UI for consent and assumed the UI was also the record.
The Legal Bar: "Demonstrate" ," Not Assume"
GDPR Article 7(1) states that where processing is based on consent, the controller must be able to demonstrate that the data subject has consented. This is an accountability obligation, and it sits entirely with you β not with the user, not with your cookie banner vendor, not with good intentions.
"Demonstrate" has a specific practical meaning: if a supervisory authority, a partner running due diligence, or the user themselves asks for proof, you need to produce a specific, verifiable record β not a general statement that your app "follows GDPR best practices."
This matters most in three situations: a regulatory audit, a data subject access request, and enterprise customers doing vendor due diligence before they'll sign a contract with you. All three want the same thing β a concrete answer, not a policy document.
What Actually Counts as Evidence
A defensible proof-of-consent record has to answer five questions, all at once, for a single event:
- Who β a user or session identifier
- What β the specific purpose (analytics, marketing, data sharing β these are distinct consents, not one bucket)
- When β a precise timestamp
- Under what terms β which version of your privacy policy or consent language was live at that moment
- How it was presented β what the interface actually looked like or said when the user acted
The fifth point trips up more teams than any other. A policy_version: "v3" field only proves something if you can independently reproduce what v3's language actually was β and most teams don't keep that. If your cookie banner copy changes in six months and someone disputes what they agreed to, "we have a policy_version field" doesn't hold up without the accompanying evidence of what that version actually presented.
What Doesn't Count (Even Though It Feels Like It Should)
A checkbox that defaults to checked. Consent under GDPR must be an unambiguous, affirmative action. Pre-ticked boxes and consent implied by continued use of the site have both been explicitly rejected by regulators and courts (this was tested directly in Planet49, a CJEU case about pre-checked cookie consent boxes).
"The banner was displayed." Server logs showing a banner rendered on a page tell you the user saw a prompt. They say nothing about how the user responded. Impression data is not consent data.
A single global flag on the user record. If your database just has gdpr_consent: true, you can't answer which specific processing purposes that covers, when it was granted, or whether it was later withdrawn. Regulators specifically look for consent broken down by purpose, not bundled into one binary switch.
Consent inferred from account creation. Signing up for a service is not, by itself, consent to marketing emails or analytics tracking. These need to be separate, explicit actions, each with their own record.
Building a Record That Actually Holds Up
The practical fix is simpler than it sounds. Every time consent is captured, changed, or withdrawn, log it as its own immutable event containing:
user_id
purpose (e.g. "marketing_emails", "analytics")
consent_given (boolean)
policy_version
timestamp
ip_hash (hashed, not raw β storing raw IPs is its own compliance risk)
Two habits make this genuinely defensible rather than just technically present:
Never overwrite a consent record β always append. If a user withdraws consent, that withdrawal is a new event with its own timestamp. The original grant record stays intact. This gives you the full history, which is what "demonstrate" actually requires β not just the current state.
Snapshot what the user saw, not just a version label. A stored copy of the actual banner or form text at the time of consent closes the gap that a version number alone leaves open. This doesn't need to be complicated β even a simple screenshot or stored HTML snapshot tied to the consent event is enough to make the record self-contained.
Why This Usually Gets Fixed Reactively, Not Proactively
Most teams don't build this properly until something forces the issue β a data subject request, an enterprise customer's security questionnaire, or a near-miss during an audit. The problem is that consent records can't be reconstructed retroactively. If you weren't logging this properly a year ago, there's no way to produce that history now. The only time to fix this is before you need it.
If you're at the stage of building this yourself, the version above β a purpose-scoped, append-only event log with a snapshot of what was shown β is genuinely sufficient for most solo and small SaaS teams. It's not complicated engineering; it's a handful of deliberate decisions made early.
If you'd rather not own the upkeep of snapshotting every banner change and keeping the export path working as your app evolves, ConsentKeep handles exactly this β proof-of-consent logging with screenshot evidence, IP hashing, and instant export, built specifically so the answer to "can you prove it?" is already sitting there when someone asks.