DokumentasjonPersonvernMarkdown
Privacy: access, erasure, consent and bots
Tools for GDPR-style requests and consent, and for keeping bots out of the visitor list.
Right of access: export
Everything Snakkbar stores about one visitor, as JSON (VisitorExport): the visitor row
(profile, attributes, live state, device, location), page views, conversations with every message,
their translations and attachments (with signed links valid for 10 minutes), and callback requests
made in those conversations, and the visitor's feedback (thumbs on answers, conversation ratings).
Internal tokens and assistant bookkeeping are left out, and so are internal notes between staff
(kind: "note") with files attached to them, and the staff-only rating transcript lines.
| Who | Request |
|---|---|
| Admin UI | Visitor detail → Personvern → Eksporter data, or the visitor panel of a conversation |
| Admin operator (API) | GET /operator/visitors/:id/export · SDK operatorClient.visitors.export(id) |
| Your backend | GET /server/visitors/:externalId/export (secret API key; identified visitors) |
Right to erasure
Slett alle data om denne besøkende (admin, with a typed confirmation) or the API deletes, in one transaction:
- the visitor: profile, attributes, live state (current page, device, location), page views;
- their conversations with all messages, translations, read markers and participants;
- their images, also from storage (unsent uploads included);
- callback requests made in those conversations;
- knowledge suggestions drawn from those conversations that were not approved (approved ones are team knowledge and stay, without the link to the conversation);
- outbox events and webhook deliveries that mention the visitor or their conversations, and rate-limit counters keyed by the visitor.
Then the assistant runtime (eve) is asked to clear its copy of each conversation (best effort; an
expired session follows eve's own retention), staff views refresh, and a visitor.deleted
event goes out ({ visitorId, externalId, by }), so webhooks can propagate the
erasure to your CRM. This cannot be undone.
| Who | Request |
|---|---|
| Admin UI | Visitor detail → Personvern, or a conversation's visitor panel |
| Admin operator (API) | DELETE /operator/visitors/:id → 204 (404 if unknown) · SDK operatorClient.visitors.remove(id) |
| Your backend | DELETE /server/visitors/:externalId → 204, also when unknown or already erased |
Erasing does not touch the browser: an anonymous visitor's stored token simply stops working, and the SDK creates a new visitor next time. Data also leaves Snakkbar by itself after the tenant's retention period (Settings → Slett samtaler etter).
Consent
Tenant setting Vent på samtykke før sporing (TenantSettings.consent, "none" by default,
or "required"; also in PublicConfig.consent). What it means, precisely:
| Consent not given (pending or denied) | Granted | |
|---|---|---|
| Live tracking (heartbeats, page views, cursor while watched) | Off. Nothing is sent | On (if live visitors is on) |
| Creating a visitor | Not for tracking or for setUser | As needed |
| Opening the chat (a user action) | Creates a visitor for the chat; the conversation works fully. Pages are still not tracked | Same |
setUser / data-user-* | Held in the page; sent only once the visitor opens the chat (or consent is granted) | Sent |
| Storage on the device | No visitor token until the visitor opens the chat; then the chat's own keys (visitor token, open conversation). A choice, once made, is remembered | Same |
With "none", tracking runs by default, but an explicit denial always wins: a site that reports
denied is never tracked, whatever the tenant setting.
Withdrawing (consent(false)) stops tracking at once, closes the chat, and forgets the
anonymous visitor on this device (snakkbar:<tenant>:visitor and the open conversation). The
conversations stay on the server until retention or erasure. Granting again later tracks as a new
visitor. For identified visitors (token from your backend) the token is yours and is kept; tracking
stops.
The choice is remembered per tenant in localStorage (snakkbar:<tenant>:consent =
granted/denied). Consent is enforced in the browser (the SDK and widget); the server cannot
know a visitor's consent.
Telling Snakkbar
| Where | How |
|---|---|
| Script tag | data-consent="granted", "denied" or "pending" (wait for a call, even when the tenant does not require consent). granted/denied are remembered like a call |
| Global API | Snakkbar.consent(true | false) (queued until the widget has mounted) |
mountWidget | mountWidget({ …, consent: "granted" | "denied" | "pending" }), later controller.consent(true | false) |
| SDK | createVisitorClient({ …, consent }), client.setConsent(granted), client.getConsent() → "granted" | "denied" | "pending". startTracking() waits while consent is needed and starts by itself on setConsent(true) |
A pending from the script tag or option never overrides a choice the visitor made earlier.
Wiring a consent manager
Most consent managers (CMPs) call you back when the visitor decides and again when they change their mind. Map the category that covers analytics/statistics to Snakkbar:
<script src="https://<host>/widget.js" data-tenant="your-tenant" data-consent="pending" defer></script>
<script>
// Generic: call this from your CMP's "consent given/changed" callback.
function snakkbarConsent(granted) {
if (window.Snakkbar && window.Snakkbar.consent) window.Snakkbar.consent(granted);
else {
// The widget has not loaded yet: it reads the attribute when it starts.
var tag = document.querySelector("script[data-tenant][src*='widget']");
if (tag) tag.dataset.consent = granted ? "granted" : "denied";
}
}
// Cookiebot
window.addEventListener("CookiebotOnConsentReady", function () {
snakkbarConsent(Boolean(window.Cookiebot && Cookiebot.consent.statistics));
});
// Google Consent Mode (gtag) users can call snakkbarConsent from the same place they call
// gtag("consent", "update", { analytics_storage: "granted" | "denied" }).
</script>Headless with the SDK:
const client = createVisitorClient({ host, tenantId, consent: cmp.hasConsent("statistics") ? "granted" : "pending" });
const stop = client.startTracking();
cmp.onChange((categories) => client.setConsent(categories.includes("statistics")));Bot protection
Obvious bots never become visitors. In the browser, the SDK does not start live tracking (and so
creates no visitor) in automated browsers (navigator.webdriver) and for crawler/HTTP-library user
agents (Googlebot, Bingbot, GPTBot, HeadlessChrome, curl, python-requests, link previewers, …).
On the server, POST /visitors refuses the same user agents with 403 forbidden. The check only
looks for clear signals, so unknown browsers and apps pass. The per-IP limit on creating visitors
stays (120 per 10 minutes). The list is packages/sdk/src/bots.ts (isBotUserAgent,
isAutomatedBrowser, exported from the SDK).
Note for test automation: headless Playwright/Puppeteer counts as a bot. Set a normal user agent
and hide navigator.webdriver (see packages/widget/dev/visual.mjs) to test tracking.
Cloudflare Turnstile (optional). Set both environment variables on the deployment:
| Variable | |
|---|---|
TURNSTILE_SITE_KEY | Public site key; sent to browsers in PublicConfig.turnstileSiteKey |
TURNSTILE_SECRET_KEY | Secret key; the server verifies tokens with Cloudflare |
With both set, POST /visitors requires a token (turnstileToken in the body): 403 challenge_required without one, 403 challenge_failed for a rejected one. The SDK handles it: it
loads https://challenges.cloudflare.com/turnstile/v0/api.js only then, solves the challenge
invisibly (appearance: "interaction-only": a small box appears at the bottom left only if
Cloudflare needs a click), and sends the token. Creating a visitor happens once per device, so
returning visitors are not challenged again. If Cloudflare cannot be reached, visitors are let
through (logged) rather than locked out. Sites with a Content Security Policy must allow
https://challenges.cloudflare.com in script-src and frame-src. Use Cloudflare's widget mode
Managed or Invisible. Clients without a browser (the SDK in Node) cannot solve the challenge;
use identified visitors (POST /server/visitor-tokens) there, which never need it.