Skip to content

Developer documentation

One script tag holds trackers, shows the right notice and records each decision. Everything else is optional.

SDK version 1.0.0. 9.9 KB gzipped.

Install

Add the script as the first script in your page's head, with your site key from the dashboard's Install page. Scripts parsed before it can't be held, so order matters.

Every page, in <head>

<head>
  <script
    src="https://d34dtur9cre43o.cloudfront.net/sdk/v1/plain-consent.js"
    data-site="pk_live_YOUR_SITE_KEY"
  ></script>
  <!-- everything else after -->
</head>

Using a framework? The same tag works in every one, and it's the only way to hold trackers written into your HTML. See Frameworks for reading consent from your own code.

The script reads these attributes from its own tag:

data-siteType: string
Your public site key. Required.
data-apiType: URL
API base for receipts and events. Defaults to the script's origin + /api/v1.
data-config-urlType: URL
Where to fetch the published config. Defaults to {api}/config/{site key}.
data-debugType: flag
Allows ?plain_country= and ?plain_region= to override location for testing. Always on for localhost.

Frameworks

Plain Theory works with React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular and any other stack that renders HTML. Add the script tag from Install, then read and change consent from your components with the JavaScript API. Nothing to install from npm.

React: read consent in a component
import { useEffect, useState } from "react";

// true once the visitor allows analytics
export function useAnalyticsAllowed() {
  const [allowed, setAllowed] = useState(false);
  useEffect(() => {
    const update = (e) => setAllowed(!!e.detail.categories.analytics);
    window.addEventListener("plainconsent:change", update);
    window.PlainConsent = window.PlainConsent || [];
    window.PlainConsent.push((pc) => setAllowed(!!pc.get().categories.analytics));
    return () => window.removeEventListener("plainconsent:change", update);
  }, []);
  return allowed;
}

Categories are essential (always allowed), functional, analytics and marketing. A script that has already run can't be unloaded, so a later withdrawal takes effect from the next page view.

Coming soon: typed packages. Packages with hooks, components and types for React, Vue, Svelte and Angular aren't published yet. Until they are, the script tag and the JavaScript API do everything they will.

WordPress

Works with WordPress: paste the script into your theme header or a header-code plugin. Banner design, regions and languages stay in the dashboard.

  1. Paste the tag from Install right after <head> in your theme's header.php. Use a child theme so theme updates don't remove it.
  2. Or use a header-code plugin such as WPCode: add a snippet, choose Site wide header and give it the highest priority, so it loads before other scripts.
  3. To hold a script another plugin adds, mark it as described in Block trackers. Known trackers are held automatically.

Block trackers

Change the type of any script that needs consent to text/plain and name its category in data-consent. It stays inert until the visitor agrees to that category, then runs exactly once.

Static blocking
<script type="text/plain" data-consent="analytics"
        src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>

<script type="text/plain" data-consent="marketing">
  fbq('init', '000000000000');
</script>

Scripts and iframes added later are held automatically when their src matches a known tracker or one on your Trackers page. Google Analytics, Hotjar, Microsoft Clarity, Meta Pixel, LinkedIn Insight, TikTok Pixel and Google Ads are recognised before your config even loads. Categories are functional, analytics and marketing; essential is never blocked.

Regions

The config response carries the visitor's country in x-plain-country (and state in x-plain-region) from the CloudFront edge. The script picks a notice from it:

EU, EEA, UK, CHType: gdpr
Opt-in. Nothing non-essential runs until the visitor accepts.
US + CAType: ccpa
Opt-out. Trackers start on; reject switches them off. Global Privacy Control turns off marketing.
INType: dpdpa
Opt-in, with your DPO's contact details in the preferences panel.
Everywhere elseType: generic
Your default notice. Falls back here if a region is turned off.

Test locally with ?plain_country=IN or ?plain_country=US&plain_region=CA.

JavaScript API

window.PlainConsent is available as soon as the script runs. To call it before then, push a function onto an array; the queue runs when the script loads.

Reading and changing consent
window.PlainConsent = window.PlainConsent || [];
PlainConsent.push((pc) => {
  pc.on("ready", (state) => console.log(state.framework, state.categories));
});

// later, from your own settings page
PlainConsent.set({ analytics: true, marketing: false });
PlainConsent.open();   // show the preferences panel
PlainConsent.revoke(); // forget the choice and ask again
get()Type: ConsentState
{ framework, categories, decided, visitorId } for this visitor.
acceptAll()Type: void
Grant every category and log an accept_all receipt.
rejectAll()Type: void
Keep only essential and log a reject_all receipt.
set(partial)Type: void
Merge category changes and log a custom receipt.
open()Type: void
Show the preferences panel.
revoke()Type: void
Clear the stored choice, log a revoke receipt and show the banner again.
on(event, fn)Type: () => void
Listen for 'ready' or 'change'. Returns an unsubscribe function.

Every change also fires a plainconsent:change event on window with the state in event.detail, for code that can't hold a reference to the API.

Headless mode

Turn on headless mode in the Banner page and the script renders nothing. Blocking, storage, Google Consent Mode and receipts all keep working; you draw the interface and call the API.

Your own banner
PlainConsent.push((pc) => {
  pc.on("ready", ({ decided }) => {
    if (!decided) myBanner.show();
  });
  myBanner.onAccept = () => pc.acceptAll();
  myBanner.onReject = () => pc.rejectAll();
});

Tracker scanner

The Trackers page scans up to 10 pages of your site, starting from the homepage, and lists the third-party scripts they load, matched against known trackers and sorted into categories. Review the list and add what you want held. The scanner only fetches public http(s) addresses and refuses private and internal networks.

Consent analytics

The Overview page shows opt-in, opt-out and partial consent rates and banner bounce rate, by day, country, notice, device and browser. Impressions come from a view event when the banner shows; a bounce is sent when someone leaves without choosing. Every chart has a table view.

REST API

The script calls these endpoints for you. They allow cross-origin requests, and every error has the same shape: { "error": { "code", "message", "fields?" } }.

GET /api/v1/config/:siteKeyType: 200 JSON
The published config. Location in x-plain-country and x-plain-region.
POST /api/v1/consentType: 201 JSON
Record a decision. Returns { id, hash, seq } for the new receipt.
POST /api/v1/eventType: 204
Banner impression or bounce: { siteKey, kind: 'view' | 'bounce' }.
GET /api/v1/receipts/:visitorIdType: 200 JSON
A visitor's own history, ?siteKey= required. Choices and hashes only.
POST /api/v1/consent
{
  "siteKey": "pk_live_YOUR_SITE_KEY",
  "visitorId": "3f9c0a1e7b2d4c6e8f00112233445566",
  "action": "custom",
  "framework": "dpdpa",
  "categories": { "essential": true, "functional": true, "analytics": false, "marketing": false },
  "configVersion": 4
}

Consent requests must come from your site's domain or a subdomain of it, and are rate limited per anonymised address. The server adds the time, country, device, browser and a truncated, salted hash of the IP address before writing the receipt.