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