Skip to content
Devix Open Source

Guide

The proof-of-consent log

Every decision — and every change of mind — becomes a row.

use Devix\CookieConsent\Models\ConsentLog;

ConsentLog::query()->where('consent_id', $id)->orderBy('revision')->get();
Column What
consent_id The widget's id for this visitor's decision — the link to the cookie in their browser.
categories, services, refused What was allowed, and what was actively refused.
version Your policy version at the time.
via accept-all, reject-all, custom, gpc, dnt, api, implied.
regime gdpr, pdpl, ccpa, lgpd…
language The language the notice was shown in.
notice_hash A hash of the exact wording, so the proof survives a redesign.
revision 1 for the first answer, then 2, 3…
ip_hash HMAC of the IP with your APP_KEY — never the address itself.
user_agent, url, decided_at

Nothing here identifies a person on its own. That is deliberate: a consent log that becomes a tracking database is its own problem.

The endpoint

POST /consent accepts the record the widget sends. It writes the row and answers 201; a payload that is not a decision gets 422. It never sets the consent cookie — the browser owns that, and a server that could grant consent is a server that can be made to.

Rename it, move it, or wrap it in middleware:

'route' => [
    'enabled' => true,
    'uri' => 'privacy/consent',
    'name' => 'consent.store',
    'middleware' => ['web', 'throttle:30,1'],
],

Set 'log' => false to keep the widget and drop the database entirely.

Your own handling

use Devix\CookieConsent\Events\ConsentRecorded;

class SyncConsentToCrm
{
    public function handle(ConsentRecorded $event): void
    {
        if ($event->decision->allows('marketing')) {
            $this->crm->allowEmail($event->decision->id);
        }
    }
}

Housekeeping

Consent records are personal data too. Trim them on your own schedule:

Schedule::call(fn () => ConsentLog::query()->where('decided_at', '<', now()->subYears(3))->delete())->monthly();
Updated 12 Sep 2026