Consenti

Server-Side Enforcement

Client-side blocking (Consenti's widget not loading a script until consent is granted) covers the browser. It doesn't cover a server-side tag manager, an edge-injected pixel, or a backend event pipeline forwarding events to an ad platform — those need to ask Consenti's API directly before firing. GET /consent/:visitorId/verifyis built for exactly this: it's fetch-based, has no server-side SDK dependency, and works identically from a Node backend, a Cloudflare Worker, or a Vercel Edge Function.

The endpoint

bash
GET /consenti/api/v1/consent/:visitorId/verify

Returns { valid: boolean, reasons: string[], ... }. valid is false whenever the profile has changed since the visitor decided, the consent has expired, or — when consentSigningKeyis configured — the stored signature doesn't match (hmac_invalid). Treat any of these the same way you'd treat "no valid consent": don't fire the tag.

⚠️This route requires the visitor's own ownership cookie (the same one the widget sets on submit) to prove the caller is the actual visitor, not someone probing another visitor's id. Server-side callers acting on a visitor's behalf need to forward that cookie — see the edge-worker example below, which does this via the incoming request's own Cookie header.

Pattern: gate a tag before firing it

gate-tag.ts
typescript
async function isConsentValid(visitorId: string, cookieHeader: string): Promise<boolean> {
  const res = await fetch(
    `https://your-domain.com/consenti/api/v1/consent/${visitorId}/verify`,
    { headers: { Cookie: cookieHeader } },
  )
  if (!res.ok) return false
  const { valid } = await res.json() as { valid: boolean }
  return valid
}

// Before forwarding an event to an ad platform's server-side API:
if (await isConsentValid(visitorId, req.headers.get('cookie') ?? '')) {
  await adPlatform.sendServerEvent(payload)
}

Edge-worker example

Both Cloudflare Workers and Vercel Edge Functions run on the standard Fetch API (the same Request/Response globals Consenti itself is built on), so the pattern above needs no adaptation — only the surrounding handler shape differs:

export default {
  async fetch(req: Request): Promise<Response> {
    const visitorId = new URL(req.url).searchParams.get('vid')
    if (!visitorId) return new Response('Missing vid', { status: 400 })

    const verifyRes = await fetch(
      `https://your-domain.com/consenti/api/v1/consent/${visitorId}/verify`,
      { headers: { Cookie: req.headers.get('cookie') ?? '' } },
    )
    const { valid } = await verifyRes.json() as { valid: boolean }

    if (!valid) {
      return new Response(null, { status: 204 }) // no-op: consent not valid, don't fire the pixel
    }

    // Forward to the actual pixel/tag endpoint only when valid
    return fetch('https://ad-platform.example.com/pixel', { method: 'POST', body: req.body })
  },
}

Pattern: gate a backend event pipeline

The same check works as a guard clause anywhere in a server-side event pipeline — before writing to a data warehouse row, before publishing to a Kafka topic, before calling a CDP's server-side API. Cache the verify result for the lifetime of a single request/batch rather than calling it per-event if you're processing many events for the same visitor at once.

Frequently asked questions