# Introduction > How to deploy Centinel Analytica on your site and customize protection settings. Source: https://docs.centinelanalytica.com/ ## Before you begin * Create your account in the [Centinel Analytica dashboard](https://dash.centinelanalytica.com). * You'll need: * your **site key** (public, used by the browser script) * your **secret key** (server-only, used for `/validate`) ## How it works Centinel has three parts: * **Server validation** (required): your backend, or a platform integration, calls the validation API for each protected request and acts on the decision (`allow`, `block`, or `redirect`). The validator issues the `_centinel` session cookie, and your response sets it. * **Policy rules**: rules select bot detection and actions for matching requests. The dashboard's default rule enforces nothing, because the validator skips it. Add and verify an explicit matching rule before you rely on a policy action. * **Browser script** (optional): collects browser and network signals that server-side data alone cannot see, and mirrors the `_centinel` cookie. Detection works without it, with a smaller signal set. ## Next steps 1. Copy your keys from the dashboard. Review the default policy. Add and order path-specific rules where you need different behavior. Go to: [Dashboard](https://docs.centinelanalytica.com/install/dashboard.md) and [Policy Rules](https://docs.centinelanalytica.com/admin/policy.md) 2. Wire up validation: use a platform integration, or call `/validate` from your backend. Go to: [Platform integrations](https://docs.centinelanalytica.com/platforms.md) or [Validation](https://docs.centinelanalytica.com/install/validation.md) 3. Add the browser script for stronger detection on pages visitors load. Go to: [Scripts](https://docs.centinelanalytica.com/install/scripts.md) New to the decisions and block reasons in the dashboard? The [Glossary](https://docs.centinelanalytica.com/concepts/glossary.md) defines them. ## Getting help Contact support with: * the URL you're protecting * a sample `/validate` request payload (redact secrets) * the decision you received and any `crawler` metadata (if enabled) --- # Interstitial Challenge > How Centinel verifies uncertain visitors with a brief in-browser challenge before granting access. Source: https://docs.centinelanalytica.com/concepts/interstitial-challenge ## Overview The interstitial is for traffic the validator cannot confidently allow or block from request data alone. It gives an uncertain visitor a way to prove that they are human. When `/validate` returns `decision: "redirect"` with a Centinel interstitial, your backend must serve the returned HTML. If you use a platform integration, verify that it applies the validator's `status_code`, `headers`, and `cookies`. The page runs a challenge in the visitor's browser. If the visitor clears it, they can continue to the protected content. If not, the session remains unverified. A `redirect` does not always mean an interstitial. A custom block page can also use that decision. Do not infer challenge intent from `decision` alone. Serve the response HTML and status that the validator returns. On rare occasions a `redirect` arrives with no `response_html` at all, because rendering or session encryption failed. Deny those requests rather than passing them through: the validator picked that visitor for a challenge, so an allow hands them the protected content. ## What a visitor sees By default, a blank page titled "Please wait" with nothing to read and nothing to do. The challenge is entirely non-interactive: there is no CAPTCHA and no puzzle. When it finishes, the browser reloads the same URL and your backend serves the real content. You can author your own challenge page in the dashboard and select it per policy rule. ### If the challenge can't run Two screens can appear. While the browser is retrying a failed upload, the visitor sees **Still verifying** with a spinner and "We're having trouble reaching our servers. Retrying automatically." After the retry budget is spent, or when session storage is unavailable, they see **Still unable to verify** with "We couldn't verify your connection after several attempts. Please refresh the page or try again in a few minutes." and a reference code for support. There is no fallback for clients that never run JavaScript. They keep receiving the interstitial with whatever status your backend applies, which is why serving it with a 200 is a mistake. ## End-to-end flow ```mermaid sequenceDiagram participant B as Browser participant A as Your backend participant C as Centinel B->>A: GET protected page A->>C: POST /validate C-->>A: Serve interstitial A-->>B: Execute script B->>C: Send telemetry B->>A: Reload page in client A->>C: POST /validate C-->>A: Allow or block A-->>B: Enforce decision ``` 1. The visitor requests a protected URL. Your backend calls `POST /validate`. 2. The validator cannot confidently allow or block from request data alone. For an interstitial, it returns `decision: "redirect"` with base64 HTML in `response_html`. 3. Your backend returns the decoded interstitial HTML with `status_code` as the HTTP status, which is 403 by default. Apply the returned `cookies` and `headers` as well. The `Content-Security-Policy` header carries the nonce the challenge script needs, so dropping it stops the challenge from running. 4. The browser renders the interstitial and runs Centinel's challenge logic. The visitor does nothing. 5. Centinel runs the full validation pipeline after completion. The signals and fatal signals determine the result. A failed upload or retry can leave the session unverified. 6. The script reloads the same URL. Centinel never returns a redirect target, and `redirect_url` is never populated. ## See also * [Validation API](https://docs.centinelanalytica.com/api/validation.md): Where the \`redirect\` decision originates. * [Wire the validation API](https://docs.centinelanalytica.com/install/validation.md): Backend handling for redirect (and other decisions). * [Scripts](https://docs.centinelanalytica.com/install/scripts.md): The browser collector script and the signals it adds. * [Preserve the Referrer](https://docs.centinelanalytica.com/install/preserve-referrer.md): Keep the original traffic source across the interstitial reload. --- # Glossary > Plain-English definitions for the terms you'll see in the dashboard, decisions, and block reasons. Source: https://docs.centinelanalytica.com/concepts/glossary Terms used in the dashboard and in `/validate` responses. Linked pages cover anything that needs more than a paragraph. ## Decisions ### Allow An `allow` normally lets your backend serve the original content. It can also contain validator-owned `response_html`, such as managed `robots.txt`. In that case, serve the returned body, status, and headers. A request with no policy action continues to configured bot detection. When monitor mode applies, a would-be block returns as `allow` with `block_reasons`. A `redirect` remains a `redirect`. ### Block The validator denied the request. Serve Centinel's block page from `response_html` with the returned `status_code`, which is 403 unless a policy rule or a tenant setting changed it. ### Redirect The validator can't decide from request data alone, so it returns an [interstitial challenge](https://docs.centinelanalytica.com/concepts/interstitial-challenge.md). Serve `response_html` with the returned `status_code`, which is 403 by default. The browser runs the challenge and the next request continues. The dashboard labels this decision **Challenged**. A `redirect` does not always mean a challenge. If you have configured a custom block page, a real block arrives as `redirect` carrying that page. Read `status_code` rather than inferring intent from the decision string. ### Monitor A rule setting, shown in the dashboard as **Detect & log only**. `monitor: true` applies to the whole request. Terminal blocks return as `allow`, but challenges still return as `redirect`. `monitor: false` does not disable monitor mode. See [Policy Rules](https://docs.centinelanalytica.com/admin/policy.md#what-monitor-really-covers). ### Rate limit A rule setting that caps requests per scope. Over-budget requests are blocked with a `rate_limit_exceeded` reason and a `retry_after` value, delivered in-band on a 200 rather than as an HTTP 429. ## Visitors ### Crawler (identified) Request's User-Agent matches a known crawler signature, like Googlebot or GPTBot. Identification alone does not prove the request really came from that crawler. See the [crawler object](https://docs.centinelanalytica.com/api/crawlers.md) for the full schema. ### Crawler (verified) An identified crawler whose source IP falls inside the operator's published IP ranges, which Centinel ingests and refreshes. A crawler whose operator publishes no ranges can only ever be identified by name, never verified. ### Crawler (allowed) A crawler you've placed on your allowlist. It passes through without scoring. The allowlist is a built-in [policy rule](https://docs.centinelanalytica.com/admin/policy.md#crawler-allowlist-priority) that runs last, so any rule you write can override it. An identified crawler that is not on your allowlist is blocked by a matching built-in rule. ### Stealth crawler A bot that tries to look like an ordinary browser. The validator catches it by collecting browser signals and spotting inconsistencies. For real browsers controlled by automation tools (Selenium, Playwright, Puppeteer), see [Automated browser](#automated-browser). ## How a check works ### Challenge A short, non-interactive test that the browser runs in the background. There is no CAPTCHA and nothing for the visitor to solve. After it completes, Centinel runs the full validation pipeline. A failed or absent upload can leave the session unverified. ### Interstitial The HTML page Centinel returns for a challenge. The browser runs the challenge and reloads the same URL. A custom block page can also use `decision: "redirect"`, so do not identify an interstitial from the decision alone. See [Interstitial Challenge](https://docs.centinelanalytica.com/concepts/interstitial-challenge.md). ### Session A run of requests associated with the same `_centinel` cookie. While the server-side session and cached verdict remain available, later validations reuse that verdict. If either item is missing or expired, Centinel can evaluate and challenge the visitor again. ### Fingerprint A composite signature of how the browser behaves at the network and JavaScript level. The validator compares it against what the User-Agent claims. ## Block-reason categories ### Header anomaly HTTP headers are missing, contradictory, or shaped like a script rather than a browser. Real browsers send a consistent set of headers; bots and minimal HTTP libraries often don't. ### Fingerprint mismatch The request doesn't match what its User-Agent claims. This covers the TLS, TCP, and HTTP/2 layers, header order and client hints, and disagreements between the page and a Web Worker. For example, a request says it's Chrome but the network signature looks like Python or Go. ### Bot identity The request's User-Agent identifies it as a known bot or scraper HTTP client (such as `python-requests` or `curl`), and is not on your allowlist. An identified crawler that isn't on your allowlist also lands here, as `crawler_blocked`. See [Crawler (allowed)](#crawler-allowed). Browser-automation frameworks like Selenium or Puppeteer fall under [Automated browser](#automated-browser) instead. ### Automated browser A real Chrome or Firefox driven by Selenium, Playwright, Puppeteer, or a similar tool. They run JavaScript fine, but leave traces in the browser environment that the validator picks up. ### Crawler impersonator A request claims to be a well-known crawler like Googlebot, but its IP doesn't belong to that crawler's operator. Common in scrapers trying to bypass crawler-allow rules. ### Suspicious network Traffic came from a network type associated with abuse. Common sub-types you'll see in analytics: * **VPN** — commercial VPN service. Legitimate users do use VPNs, so this is a signal weighed with the others, not an automatic block. * **Datacenter (DC)** — IP belongs to a cloud or hosting provider. Most consumer traffic doesn't originate from datacenters; bots often do. * **Proxy** — open or commercial HTTP proxy. Frequently used to mask the origin of automated traffic. * **Tor** — a Tor exit node. Anonymity network with mixed legitimate and abusive use. * **IP reputation** — the address is a known anonymizer, hosting range, abuser, or is blacklisted. * **Rate limit** — the request exceeded a rate-limit rule. Filed here even though it isn't a network signal. ### Challenge failure The session got an interstitial and never cleared it. Several things produce this: the challenge failed, the verification was invalid, or the visitor abandoned the page. There are timeout and payload variants too. One case is on your side. If your backend serves the interstitial but drops the `cookies` from the `/validate` response, the challenge cannot be won and the request fails closed with `interstitial_no_session`. ### Unknown A block reason Centinel hasn't grouped into a category yet. Rare. Read the raw `block_reasons` value to see what actually fired. ## See also * [Interstitial Challenge](https://docs.centinelanalytica.com/concepts/interstitial-challenge.md): How redirect decisions are enforced and what visitors see. * [Validation API](https://docs.centinelanalytica.com/api/validation.md): POST /validate endpoint spec: schemas, decisions, errors. * [Policy Rules](https://docs.centinelanalytica.com/admin/policy.md): Allow, block, rate-limit, or scrutinize requests with custom rules. --- # Dashboard > Find your keys, and see what a /validate response can include. Source: https://docs.centinelanalytica.com/install/dashboard ## What you'll do here 1. Find and copy your integration keys (site key + secret key). Centinel normally generates both when it creates your organisation; check the Organisation page and regenerate either key if it is missing. 2. Review your policy rules. Add and order path-specific rules where you need different behavior. See [Policy Rules](https://docs.centinelanalytica.com/admin/policy.md). > **Warning:** Keys authenticate `/validate`; they do not enforce traffic. Before relying on blocks, add and > verify a matching policy rule. Then have your backend or platform integration act on the > `/validate` response. The dashboard opens on **Insights**. **Analytics** has an endpoint tree showing which paths actually receive traffic, which is the quickest way to decide what to protect first. **Crawlers** holds the allowlist. Source-IP verification and allowlist membership determine `access_allowed`. **Threats** shows what Centinel blocked and why. Press `⌘``K` (or `Ctrl``K`) to jump to any of them. ## API keys Centinel issues two kinds of key, on two different pages under **Organisation**. ### Integration keys On the [Organisation](https://dash.centinelanalytica.com/organisation) page, under **Integration keys**: * **Site key (public)**: used in the browser script URL. Safe to expose. * **Secret key (sensitive)**: used as `x-api-key` when your backend calls `/validate`. Keep this server-side only—store it in environment variables or a secret manager. > **Never expose the secret key:** The `x-api-key` is server-only. Don't put it in client-side code, HTML, or public repositories. ### Analytics keys The **API keys** page at [Organisation → API keys](https://dash.centinelanalytica.com/organisation/tokens) issues the `sk_api_ro_` keys that the [Analytics Query API](https://docs.centinelanalytica.com/api/analytics.md) and the [MCP server](https://docs.centinelanalytica.com/api/mcp.md) accept as a bearer token. These are separate from your integration keys and cannot call `/validate`. Create one key per integration so you can revoke it on its own. ## Crawler metadata `/validate` responses can carry a `crawler` object identifying which crawler made the request. It holds `id`, `name`, and `access_allowed`, plus `category` and `rsl_category` when the catalog entry resolves. See [Crawlers](https://docs.centinelanalytica.com/api/crawlers.md) for the full schema. `/validate` includes this object only when the `validate_response_include_crawler` feature flag is enabled for your organisation. Crawler detection is always active, and you manage the allowlist on the **Crawlers** page either way. Once you have both integration keys, continue to: * [Add scripts](https://docs.centinelanalytica.com/install/scripts.md): Install the browser script using your site key, and make sure it loads wherever protected actions can be triggered. * [Validate requests](https://docs.centinelanalytica.com/install/validation.md): Call /validate from your backend using your secret key, then enforce the returned decision. --- # Scripts > Optional browser script that adds browser-side signals and establishes a session up front. Source: https://docs.centinelanalytica.com/install/scripts ## Overview The Centinel collector script is an **optional** addition to your site. It runs in the browser, sets the `_centinel` cookie, and feeds browser-side signals back to the validator. Server-only validation works without it, on a smaller signal set. The script adds the browser and network signals that no amount of server-side data can reveal, which is what catches automation driving a real browser. The `_centinel` cookie it sets gives the validator a session key before the first challenge. The interstitial challenge does not need this script. The validator injects its own copy into the interstitial it serves, so challenges work on a server-only install too. ## Add the script Drop this in your page `` and replace `SITE_KEY` with your site key (from the [Dashboard](https://docs.centinelanalytica.com/install/dashboard.md)). ```html ``` Keep the `defer`. Several signals read the DOM, so a script that runs before `` exists collects less and weakens detection. `defer` also keeps the parser from blocking on the download. ## Where to install it Add the script to any page where a protected action can be triggered: * Login forms (or any page hosting a login modal) * Signup and password-reset flows * Checkout and payment pages * Gated content: premium articles, paid downloads, members-only areas * Comment, review, and other user-generated-content submission > **Single-page apps:** Install the script once in the global layout, not per route, to avoid double-loading. It collects once per full document load and deliberately does not hook client-side navigation, so a route change does not re-run collection. That is fine: the session it established on first load carries across routes. ## Verify Load a page and check three things in your browser's dev tools: * The request to `collector.centinelanalytica.com/script.js` returns `200`. A `401` means the `site_key` is missing or wrong, and no signals are collected. * A `POST` to the collector origin carries the payload. * A first-party `_centinel` cookie is set. In code, check `window.cen.initialized` to confirm the script finished. If it is not ready yet, wait for the `CentinelInitialized` event. > **Warning:** The event fires only after the payload upload succeeds, and a failed upload is not retried. Always > pair the wait with a timeout, or a visitor on a flaky network can never complete the action you > gated behind it. ```javascript function whenCentinelReady(cb, timeoutMs = 2000) { if (window.cen && window.cen.initialized) { cb(); return; } let done = false; const run = () => { if (done) return; done = true; window.removeEventListener('CentinelInitialized', run); cb(); }; window.addEventListener('CentinelInitialized', run); setTimeout(run, timeoutMs); // never block the action on the script } whenCentinelReady(() => { login(); }); ``` ## Next steps Whether you added the script or chose to skip it, your backend needs to call `/validate` on every protected request. * [Validation](https://docs.centinelanalytica.com/install/validation.md): Wire the /validate API into your backend. --- # Validation > Wire the /validate API into your backend when no platform integration covers your stack. Source: https://docs.centinelanalytica.com/install/validation ## Overview If a [platform integration](https://docs.centinelanalytica.com/platforms.md) covers your stack, use it. This page is for custom backends and platforms not yet supported. You will call `POST /validate` for every protected request and act on the response. A `block` or `allow` can come straight from a [policy rule](https://docs.centinelanalytica.com/admin/policy.md), including the crawler allowlist, before any bot scoring runs. Either way, your backend handles the decision the same. ## When to call /validate * On the server, after you have parsed the request. * Once per request, before your application handles it. * Skip locally for paths you do not want to protect. The validator returns `allow` for those anyway, but skipping saves a round trip. Send the request to `POST https://validator.centinelanalytica.com/validate` with `content-type: application/json` and your secret key in the `x-api-key` header. Only `url` and `ip` are required. `ip` must be a single parseable IP address, and `cookie` must be the bare `_centinel` UUID rather than the whole `Cookie` header. Use a timeout of 300ms to 500ms. The validator's own budget is 500ms, so a longer client timeout turns a slow validator into a stall your visitors feel. ## Handle each decision 1. Call `POST /validate` with the request URL, method, client IP, all headers, and the `_centinel` cookie. 2. Apply every entry from `response.headers` to your outgoing response, and every entry from `response.cookies` as a Set-Cookie. 3. Send `response.status_code` as the HTTP status, falling back to `403` if it is missing. 4. For `block` or `redirect`, base64-decode `response.response_html` and return it as the response body. 5. For `allow`, return validator-owned HTML when `response_html` is present. Otherwise, pass the request to your application. ### Failing safely > **Warning:** Fail open on a 5xx or a transport error, and fail closed on a 4xx. A 400, 401, or 409 is a > deterministic rejection, not an outage. Treating one as a reason to allow the request is how > bots get through: a crafted `ip` value makes the validator answer 400, so a backend that > allows on 400 hands every attacker a bypass. Do not infer challenge intent from the decision string alone. A custom block page can arrive as `decision: "redirect"`. Serve the response HTML and status that the validator returns. ```ts type ValidateDecision = 'allow' | 'block' | 'redirect'; async function enforce(req, res, next) { let result; try { result = await callValidate({ url: req.url, method: req.method, ip: clientIpFrom(req), referrer: req.headers.referer, cookie: req.cookies['_centinel'], headers: req.headers, }); // 300-500ms timeout } catch { return next(); // transport error or timeout: fail open } if (result.status >= 500) return next(); // validator outage: fail open if (result.status >= 400) return res.status(403).send('Forbidden'); // rejected: fail closed if (result.body.success === false) return next(); const data = result.body; for (const [name, value] of Object.entries(data.headers ?? {})) res.setHeader(name, value); for (const c of data.cookies ?? []) { res.cookie(c.name, c.value, { path: c.path, domain: c.domain || undefined, secure: c.secure, sameSite: c.same_site, maxAge: c.max_age * 1000, }); } if (data.rate_limited) res.setHeader('Retry-After', String(data.retry_after)); const status = data.status_code || 403; const html = data.response_html ? Buffer.from(data.response_html, 'base64').toString('utf8') : null; if (data.decision === 'allow' && html !== null) { return res.status(status).send(html); // validator-owned response, such as robots.txt } switch (data.decision) { case 'allow': return next(); case 'block': case 'redirect': // Missing HTML can result from a rendering or session-encryption failure. return res.status(status).send(html ?? 'Access Denied'); default: return res.status(403).send('Access Denied'); } } ``` Preserve every cookie attribute the validator sends. Dropping `max_age` demotes `_centinel` to a session cookie, which breaks continuity across the interstitial challenge. ## Decoding `response_html` Both `block` and `redirect` carry `response_html` as base64-encoded UTF-8. Decode and return it as the body, with `response.status_code` as the HTTP status. That field defaults to `403` for both decisions, and a policy rule or a tenant setting can change it to any of `200`, `302`, `401`, `403`, `404`, `418`, `429`, `451`, `460`, or `503`. Hardcoding a status throws away whatever was configured in the dashboard. A `redirect` can also arrive with no `response_html` at all, which happens when interstitial rendering or session encryption failed. Deny those requests with `status_code` and a short body of your own. The validator picked that visitor for a challenge, so an allow hands them the protected content. The shipped gateway integration substitutes a block page whenever the HTML is missing. ```js // Node.js const html = Buffer.from(response.response_html, 'base64').toString('utf8'); ``` ```js // Web platforms (Cloudflare Workers, Deno, browsers) const html = new TextDecoder().decode( Uint8Array.from(atob(response.response_html), c => c.charCodeAt(0)) ); ``` ## Applying headers A response can carry a `headers` object on any decision. Set each entry on your outgoing response. A `block` can contain `Content-Type`, `X-Content-Type-Options`, and `X-Frame-Options`. An interstitial can contain `Content-Type`, `X-Content-Type-Options`, and a `Content-Security-Policy` whose nonce the challenge script needs. An `allow` can carry a validator-owned response, such as managed `robots.txt`, with its own headers. Always guard for a missing `headers` key. ```js // Express for (const [name, value] of Object.entries(response.headers ?? {})) { res.setHeader(name, value); } ``` ```js // Cloudflare Worker const headers = new Headers(); for (const [name, value] of Object.entries(response.headers ?? {})) { headers.set(name, value); } return new Response(body, { status, headers }); ``` ## Common pitfalls * **Failing open on a 4xx.** A 400, 401, or 409 means the validator rejected your request, not that it is down. Allowing those requests opens a bypass. * **Calling `/validate` from the browser.** The `x-api-key` is server-only. Anyone with the key has full tenant access. * **Hardcoding the HTTP status.** Send `response.status_code`. Hardcoding `200` on `redirect` answers real blocks with a success status, which caches may then store. * **Assuming `headers` is always present.** It can be absent on any decision. Reading it without a guard can throw. * **Trusting `X-Forwarded-For` blindly.** Pass the real client IP, but take it from a hop you control. The header is attacker-controlled, and a non-IP value makes the validator answer 400. * **Ignoring `cookies` on `allow`.** The `cookies` array can be non-empty on any decision (session cookies on a happy-path response, challenge cookies on `redirect`). Apply them all, with their attributes. * **Dropping the `_centinel` cookie.** Without it, session tracking across interstitial challenges fails, and the visitor is challenged again on every request. * **Expecting an HTTP 429.** Rate limits arrive in-band as a 200 with `rate_limited` and `retry_after` in the body. `/validate` never answers 429. ## See also * [Validation API reference](https://docs.centinelanalytica.com/api/validation.md): POST /validate endpoint spec: schemas, decisions, errors. * [Platform integrations](https://docs.centinelanalytica.com/platforms.md): Drop-in modules for Cloudflare, Fastly, Nginx, Next.js, and more. * [Policy Rules](https://docs.centinelanalytica.com/admin/policy.md): Allow, block, rate-limit, or watch requests, and set the crawler allowlist priority. --- # Preserve the Referrer > Carry the original visitor source across the interstitial so your analytics still sees where traffic came from. Source: https://docs.centinelanalytica.com/install/preserve-referrer ## Overview When an uncertain visitor passes through the [interstitial challenge](https://docs.centinelanalytica.com/concepts/interstitial-challenge.md), the browser reloads into your protected page. That reload erases the original referrer, so analytics on the page attributes the visit to your own site instead of the real source (Google, a newsletter, a partner link). Centinel preserves the original source in a session-scoped value named `_centinel_ref`, kept in the browser's `sessionStorage`. This page explains why the loss happens and how to hand the saved value to your analytics. ## Why the referrer is lost The interstitial is a real page load. To send the visitor on to your content, it calls `location.reload()`. A reload (like a redirect) does not carry the previous page's referrer forward: after it, both `document.referrer` and the `Referer` request header become the interstitial's own URL. The visit looks like a self-referral from your own domain. JavaScript cannot fix this on its own. `document.referrer` is read-only, and `Referer` is a [forbidden header](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header) that scripts may not set on a top-level navigation. The only document that still holds the true upstream referrer is the interstitial itself, before it reloads. ## The `_centinel_ref` value While it is still running, the collector script the interstitial loads captures the upstream referrer and writes it to `sessionStorage` so it survives the reload. `sessionStorage` is scoped to the tab and origin, so the value is there when your page loads but never travels to your server the way a cookie would. The entry is keyed `_centinel_ref`. Four properties decide how you read it: * **Origin only.** The entry holds the referrer's origin (`https://www.google.com`), never the full path or query. That is all an analytics tool needs for source attribution, and it stays free of personal data regardless of the referring site's `Referrer-Policy`. * **Cross-origin only.** It is written only when the visitor arrived from a different origin. A same-origin visit leaves nothing behind, so your own URL is never stored as a source. * **Readable on one origin.** Only the origin that served the interstitial can read the entry, so a page on `www.example.com` will not find a value written on `example.com`. * **Cleared on every run.** Each interstitial run removes the entry before deciding whether to store a fresh one. A second interstitial in the same tab therefore discards an origin saved earlier, so read the value on the first page view after the challenge. > **Warning:** The value is absent when there was no cross-origin referrer. Always treat it as optional: if it is missing, fall back to the browser's normal behavior. ## Restore the referrer One snippet covers every tool. Most trackers read `document.referrer` themselves and give you no way to override it, so instead of wiring each one, redefine `document.referrer` before any of them loads. ```html ``` `document.referrer` is a getter on `Document.prototype`. Defining an own property on `document` shadows that getter for the rest of the page view, so anything reading the referrer gets the original source instead of your own URL. Nothing else changes. With no stored value the snippet returns early and every tool sees the browser's own referrer, exactly as it would without this code. That is what makes it safe on the page views that never meet an interstitial, and in browsers that block `sessionStorage`. > **Warning:** Placement decides whether this works. The snippet has to run before your tag manager and before every analytics loader in ``. A tracker that has already read the referrer keeps the old value. If your tags live in Google Tag Manager, the snippet can go in a Custom HTML tag on the **Initialization - All Pages** trigger, which runs before the container's other tags. That orders only what GTM fires. A tracker installed as its own `