Web widget
One script tag shows a consent banner or form, records each decision in your ledger, holds back trackers until people agree, and lets them change their mind at any time.
Install#
<script src="https://cdn.optproof.in/w.js" data-pk="pk_live_..." async></script>- The script is under 15 KB gzipped, loads asynchronously and never throws errors into your page.
- The UI lives in an open Shadow DOM on an
optproof-rootelement, so your CSS cannot restyle it and its CSS cannot leak into your page. - The widget fetches the property's configuration (
GET /v1/public/config) and caches it for five minutes. If the network fails it uses the last good copy.
Attributes#
| Attribute | Default | What it does |
|---|---|---|
data-pk | required | Your publishable key (pk_live_… or pk_test_…). |
data-api | https://api.optproof.in | API base URL, for staging or local development. |
data-lang | automatic | Preferred language, used only if the notice is published in it. Otherwise the visitor’s last choice, then the browser languages, then the property default. |
data-mode | from config | cookie, purpose or both. Overrides the mode set in the console. |
data-consent-mode | on | off disables Google Consent Mode signals. update sends updates only, for use with the GTM template. |
nonce | none | On strict-CSP pages, the widget copies this nonce to its fallback style element. |
The same settings work as query parameters (w.js?pk=…&api=…&lang=…&mode=…&cm=update), because Google Tag Manager cannot set attributes on injected scripts.
Layouts and modes#
Choose the layout and position in the console under the property's widget settings.
| Layout | Best for |
|---|---|
banner | A bar along the top or bottom of the page. The usual choice for cookie consent. |
modal | A centred dialog that traps focus. Esc closes it only after a choice is stored, and focus returns where it was. |
slide_in | A panel in a corner that does not cover the content. |
| Inline form block | Consent inside your own form, at the moment you collect the data. See below. |
The mode decides what the visitor sees:
cookiegroups purposes into Essential, Functional, Analytics and Marketing.purposelists every DPDP purpose with its title, description and the data items it uses.bothshows a cookie section plus the purposes that are not cookie categories.
The language switcher only offers languages your notice is published in. Urdu, Kashmiri and Sindhi render right to left.
Equal choice, no dark patterns#
The widget is built so that the easy path and the fair path are the same:
- Accept all, Reject all and Customise share one style: same size, weight and colour. A browser test checks this, including on pages with hostile CSS.
- No box is ever pre-ticked. The preference panel shows the visitor's own current choices.
- Required purposes and the Essential category show as Always on, with an explanation, and have no checkbox.
- A floating Privacy choices link reopens the panel on every page. Withdrawing everything is one click (Reject all), the same as granting.
- After saving, the visitor sees the receipt code and can download the signed receipt.
- When your notice version changes, the banner shows again: always, or only for material changes, depending on your re-prompt setting.
Inline form block#
Put the block inside your form, where you collect the data. List the purpose keys it should show:
<form action="/book" method="post"> <input name="phone" /> <div data-optproof-purposes="appointment,whatsapp_updates"></div> <button>Book</button></form>- Each optional purpose appears as an unticked checkbox. Required purposes show as Always on.
- When the form submits, the widget records consent with
ui_variant: "inline". Ticked boxes are granted. Required purposes based on consent are granted too, because submitting the form after seeing them is the affirmative act. - Unticked boxes are recorded as refused, unless the purpose was already granted. Leaving a box unticked never silently withdraws an earlier grant.
- Submitting the same choices twice is recorded once.
Hidden inputs added to your form#
| Field | Value |
|---|---|
optproof_choices | The choices, for example whatsapp_updates:1. Set before your submit handler runs. |
optproof_device | The visitor’s device_ref. Your backend can link it to the person with POST /v1/principals/link. |
optproof_receipt | The receipt code, filled in when the API answers. AJAX and single-page forms can read it; a full-page POST usually leaves first, so use optproof_device then. |
JavaScript API#
If you call the API before the script has loaded, paste this stub before the widget tag. Calls are queued and run once the widget is ready.
<script> window.optproof = window.optproof || { q: [] }; ['open', 'identify', 'withdraw', 'onChange', 'ready'].forEach(function (m) { optproof[m] = optproof[m] || function () { optproof.q.push([m].concat([].slice.call(arguments))); }; });</script>| Call | Returns | Notes |
|---|---|---|
optproof.open() | nothing | Opens the preference panel. |
optproof.identify({ phone }) or ({ email }) | Promise<boolean> | Links this device to a person so their history follows them. Exactly one identifier. Never stored in the browser. Resolves false on bad input or failure. |
optproof.has(key) | boolean | A purpose key, or a category: essential, functional, analytics, marketing. Required purposes are always true. |
optproof.onChange(cb) | unsubscribe function | Called with { purposes, categories, notice_version } on every change, and once on load for a returning visitor. |
optproof.receipt() | { code, jws } or null | The most recent receipt. |
optproof.withdraw(keys) | nothing | Withdraw a list of purpose keys, or 'all'. |
optproof.ready(cb) | nothing | Runs after the configuration has loaded, or failed to load. |
None of these throw. TypeScript types (OptProofApi, ConsentSnapshot) are exported from the widget package.
optproof.onChange(function (state) { if (state.categories.analytics) { // start your own analytics code }});Block scripts and iframes until consent#
Mark a tag with type="text/plain" and a category. Browsers neither run nor fetch these scripts.
<script type="text/plain" data-optproof-category="analytics" src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script><script type="text/plain" data-optproof-category="marketing" data-optproof-purpose="ads"> fbq('init', '...');</script><iframe data-optproof-src="https://www.youtube-nocookie.com/embed/..." data-optproof-category="functional"></iframe>- Once the category is granted (and the purpose, if
data-optproof-purposeis set), each tag is re-created as a real script, in document order. Attributes, the nonce andasyncare kept. Usedata-optproof-type="module"for module scripts. - Iframes get their
srcfromdata-optproof-src. - Tags added to the page later are picked up automatically.
essentialscripts run as soon as the configuration loads.- A script that has already run cannot be un-run when someone withdraws. Reload the page, or check
optproof.has()inside your own code.
Google Consent Mode v2#
If window.gtag or window.dataLayer exists when the widget loads, it:
- pushes a default as early as possible: everything denied except
security_storage, withwait_for_update: 500; - for a returning visitor, pushes the stored state straight away, before the configuration round trip;
- on every decision, pushes
gtag('consent', 'update', …)and a dataLayer event namedoptproof_consent_updatecarrying the categories and purposes.
| Category | Consent Mode signals |
|---|---|
essential | security_storage (always granted) |
functional | functionality_storage, personalization_storage |
analytics | analytics_storage |
marketing | ad_storage, ad_user_data, ad_personalization |
Load the widget before or alongside your Google tag, and leave the Google tag as a normal, unblocked script: Consent Mode makes it behave correctly.
Google Tag Manager#
OptProof provides a custom tag template.
- In GTM, open Templates, then Tag Templates, then New. From the menu choose Import and select the OptProof template file. Save.
- Under Tags, create a tag of type OptProof Consent (DPDP). Enter your publishable key and, if you like, regions and
wait_for_update. - Set the trigger to Consent Initialization – All Pages and publish.
The template sets the Consent Mode default and loads w.js with cm=update, so the widget only sends updates. If w.js is already on the page, untick Load the OptProof widget in the template and add data-consent-mode="update" to your own tag.
For your other tags, use GTM's built-in consent checks or trigger them on the optproof_consent_update event.
Content Security Policy#
Add these entries to your policy:
script-src https://cdn.optproof.inconnect-src https://api.optproof.in- Modern browsers need no
style-srcentry and no'unsafe-inline': the widget styles its shadow root with constructable stylesheets and never setsstyleattributes. - Older browsers fall back to a style element inside the shadow root. Put a nonce on the widget tag and
'nonce-…'instyle-src; the widget copies it. Without a nonce, those browsers show the widget unstyled unless you allow'unsafe-inline'styles. - Blocked inline scripts that you want activated on a strict-CSP page need their own nonce or hash. The nonce is carried over when the widget activates them.
- Save receipt downloads through a
blob:URL. - If you host
w.jsyourself or use a staging API, use those origins instead.
Tip
default-src 'none'; script-src 'self'; connect-src <api>; style-src 'self' and checks that no violations are reported.What the widget sends#
All calls go to /v1/public/… with your publishable key in the OptProof-Key header and an Idempotency-Key on every write. Requests use credentials: 'omit'. The page_url it sends is origin and path only, never the query string or hash, because those can carry personal data. Queued writes retry with backoff and survive page reloads; identify calls are never stored because they carry a phone number or email.
See the API reference for the exact request and response shapes.