Consent
Consent in Qinto decides which apps receive a visitor’s data. Your site asks the visitor once, with your own banner or a consent platform, and sends the answer to Qinto with one call. Qinto stores it on the visitor’s profile and applies it on every event, in the browser and on the server, for every app installed on the site. You do not send consent to each destination, and you do not change your tracking code when you add or remove an app.
Consent is not a switch on whether Qinto accepts an event. Every track, identify and alias call is accepted and
processed as Actions describes. Consent changes who the event is delivered to.
How consent works
Section titled “How consent works”Five categories and a choice per app
Section titled “Five categories and a choice per app”A consent state has two parts:
| Part | What it holds |
|---|---|
categories |
One boolean per category: necessary, functional, analytics, marketing and salesOfData. necessary is always true. |
apps |
An optional boolean per app slug, such as { "facebook": false }. |
Every app declares one category in its manifest, the category a visitor must have granted before the app receives anything. The developer picks it when they build the app, and the app’s listing in the App store shows it. See Manifest.
An app receives a visitor’s data when all of these hold:
- Its category is
necessary, or the visitor granted its category. - The visitor did not set the app to
falseinapps.
The per-app choice only narrows. apps: { facebook: false } stops Meta even when marketing is granted. Setting an
app to true does not grant a category the visitor refused. Use it for a banner that lists vendors, or for a
preference page where a visitor can refuse one destination. The gate reads it only where it reads the categories: with
consent mode on, for apps outside necessary. A necessary app runs whatever apps says.
A visitor who has not saved anything has granted nothing. With consent mode on, only necessary apps receive their
data until the first save.
Consent mode
Section titled “Consent mode”Consent gating is per site, and it is off until you turn it on. Open your site in the Blotout platform and go to Settings → Privacy & Security. Under Consent, turn on Enable consent mode. Qinto redeploys the site, and from then on each event reaches only the apps whose category the visitor allowed.
With consent mode off, every app receives every event, and Qinto still stores the consent you send. Turn it on only
when your site asks visitors for consent. On a site that never asks, no visitor ever grants a category, and consent
mode would silence every app outside necessary.
What consent gates
Section titled “What consent gates”| Surface | Gated by consent |
|---|---|
| Event delivery to an app, in the browser and on the server, including the connect hook | Yes |
| The app’s browser code | Yes. The tag downloads every app’s code, and runs an app only when the gate allows it. A grant made on the page starts the app at once, without a reload. |
| Messages an app pushes to the page, and click ids or cookies an app captures | Yes |
| An app’s transform hook | No. A transform is the site’s own ingest step, such as masking a field before Qinto stores it, and it runs for every visitor. |
| The consent change itself | No. Every installed app receives a consent change, so a destination can stop or delete on a revocation. |
Calls from your servers over /v1/* and the server SDK |
Yes. The stored consent applies to a server call for the same person. |
| What the identity graph stores | No. Purchases, traits and identifiers are stored as before. See ID graph. |
Where consent lives
Section titled “Where consent lives”Qinto stores consent on the visitor profile, with a 90-day history of changes, and the current state converges on the person profile. A choice made on the phone holds on the laptop once both devices present the same identifier, and a server call that names the person by email reads the same state. ID graph explains the two profiles.
Saves merge. A save that carries only { analytics: true } changes analytics and leaves the other categories as
they were, so a banner can send one toggle at a time. A save equal to the stored state writes nothing.
Global Privacy Control
Section titled “Global Privacy Control”Global Privacy Control (GPC) is a browser signal that says the visitor does not want their data sold or shared. It is a separate setting from consent mode. On Settings → Privacy & Security, under Global Privacy Control, turn on Honor Global Privacy Control.
When it is on and a visitor’s browser sends GPC, apps in the marketing and salesOfData categories receive nothing
from that visitor for that page session. The rule holds in every region, with consent mode on or off, and it wins over
a stored opt-in. Apps in necessary, functional and analytics run as before.
GPC is a signal of the page session, and Qinto stores nothing from it. The stored consent, getConsent(), an app’s
ctx.consent and the state an app.onConsent handler receives do not change. Only app.consent in a browser app,
which reports the consent that applies on the page, reads marketing and salesOfData as denied. A call from your
servers carries no browser signal, so GPC does not gate it.
Save consent
Section titled “Save consent”Send consent before the first event of the page. A visitor who has not saved anything has granted nothing, so an event
sent before the save reaches only necessary apps. The promise settles when Qinto stored the choice.
Every example has a tab per surface. Pick one tab and the whole page follows.
Call qinto.consent with the categories the visitor allowed. Put the call in your banner’s accept and save handlers,
and on every page load for a visitor who already decided.
The tag records PageView for you as soon as it loads. A consent call your page makes before the tag loads, for
example from a cookie your banner wrote, is queued ahead of that PageView, and the server applies the two in that
order. Browser apps apply the grant only when the server’s reply arrives, so they do not see that first PageView.
When your banner learns the choice asynchronously, or the first page view must reach browser apps as well, turn off
Page load under Settings → Capturing data and send PageView yourself after the save settles, as the
Shopify example does.
<script> qinto.consent({ categories: { functional: true, analytics: true, marketing: false, salesOfData: false } })</script>The promise settles once Qinto stored the choice. Browser apps apply the new state when the reply arrives, so
await the call before you send an event that the newly allowed apps must see.
<script> const onAccept = async () => { await qinto.consent({ categories: { analytics: true, marketing: true } }) qinto.track('ConsentAccepted') }</script>@blotout/qinto-sdk-browser exports consent next to
track. The package does not record the page load, so send consent before your own track('PageView').
import { consent, init, track } from '@blotout/qinto-sdk-browser'
init({ destination: 'https://q.example.com' })
await consent({ categories: { functional: true, analytics: true, marketing: false, salesOfData: false } })await track('PageView')The five category names are exported as CONSENT_CATEGORIES.
@blotout/qinto-sdk-server saves consent for a person your
backend knows, for example from a preference center or an unsubscribe link. The first argument names the person by an
anon_… id or any identifier of your own. It needs an edge token with the Save consent permission from your site’s
Developer tools → Edge tokens page.
import { createClient } from '@blotout/qinto-sdk-server'
const qinto = createClient({ destination: 'https://q.example.com', token: process.env.QINTO_EDGE_TOKEN })
await qinto.consent('crm-user-8412', { categories: { marketing: false } })An identifier Qinto has never seen creates a profile and binds the identifier to it, as identify does, so the choice
is in place when that person first visits the site.
Any language can call POST /v1/consent on your site’s Qinto host, with an edge token that has the Save consent
permission as a bearer token and a JSON body. This is what the server SDK sends.
curl -X POST https://q.example.com/v1/consent \ -H 'Authorization: Bearer qwat_…' \ -H 'Content-Type: application/json' \ -d '{ "identifier": "crm-user-8412", "categories": { "analytics": true, "marketing": false }, "apps": { "facebook": false } }'| Field | Required | What it is |
|---|---|---|
identifier |
Yes | The person: an anon_… id or an identifier of your own. An identifier Qinto has never seen creates a profile and binds the identifier to it. |
categories |
No | The categories to change. necessary accepts only true. |
apps |
No | The apps to change, by slug. |
observed |
No | occurredAt and the other fields Actions lists, for a choice that was not made just now. |
The response is { "ok": true, "id": "act_…" }. A body with "necessary": false or an unknown category returns 400
with invalid_body.
Refuse one app
Section titled “Refuse one app”The apps map takes the same place in every call. With consent mode on, an app set to false receives nothing even
when the visitor granted its category. It does not stop a necessary app, and with consent mode off Qinto stores the
choice and applies nothing.
await qinto.consent({ apps: { facebook: false } })The key is the app’s slug, the fixed name of the app such as facebook. Find an app
slug on the Actions page lists where to read it.
Read consent
Section titled “Read consent”qinto.getConsent() returns the state stored for this visitor, as the server last sent it. It sends nothing. Qinto
sends the state when the page connects, answers each consent call with the state it stored, and pushes every later
change, including one made in another tab or on another device.
<script> qinto.getConsent().then(({ categories }) => { if (!categories.marketing) { showBanner() } })</script>import { getConsent } from '@blotout/qinto-sdk-browser'
const { categories, apps } = await getConsent()getConsent takes an anon_… id and needs an edge token with the Read consent permission.
import { createClient } from '@blotout/qinto-sdk-server'
const qinto = createClient({ destination: 'https://q.example.com', token: process.env.QINTO_EDGE_TOKEN })
const { categories, apps } = await qinto.getConsent('anon_01J…')curl 'https://q.example.com/v1/consent?identifier=anon_01J…' \ -H 'Authorization: Bearer qwat_…'The response is the full state, with every category present:
{ "categories": { "necessary": true, "functional": true, "analytics": true, "marketing": false, "salesOfData": false }, "apps": { "facebook": false }}The read accepts an anon_… id only. An identifier of your own returns 400 with invalid_params.
Forward Shopify’s consent
Section titled “Forward Shopify’s consent”Shopify stores a visitor’s choice in its own Customer Privacy API, whether the choice came from Shopify’s banner or a consent app. Your theme reads that API and forwards the result to Qinto. Shopify asks the visitor and keeps the record. Qinto applies it per app. Nothing writes back to Shopify.
Shopify’s four permissions map to four Qinto categories:
| Shopify | Qinto category |
|---|---|
preferencesProcessingAllowed() |
functional |
analyticsProcessingAllowed() |
analytics |
marketingAllowed() |
marketing |
saleOfDataAllowed() |
salesOfData |
Each Shopify method already combines the visitor’s choice with the store’s settings and the visitor’s region. In a
region where Shopify does not require consent, the methods return true before the visitor has decided, and Qinto
receives a grant. In a region where it does, they return false until the visitor accepts.
Shopify hands you its API in a callback, so the choice is not known when the Qinto tag loads, and the PageView the
tag records would go out before the save. First turn off Page load under Settings → Capturing data for the
site. Then add this to theme.liquid after the Blotout snippet. It saves the current state on every page load, sends
PageView once the save has settled, and saves again each time the visitor changes their mind. Shopify fires
visitorConsentCollected on document whenever the banner or a consent app calls setTrackingConsent.
<script> const consentFromShopify = (privacy) => { return { categories: { functional: privacy.preferencesProcessingAllowed(), analytics: privacy.analyticsProcessingAllowed(), marketing: privacy.marketingAllowed(), salesOfData: privacy.saleOfDataAllowed() } } }
window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.2' }], async (error) => { if (error) { return }
const privacy = window.Shopify.customerPrivacy document.addEventListener('visitorConsentCollected', () => { qinto.consent(consentFromShopify(privacy)) })
await qinto.consent(consentFromShopify(privacy)) qinto.track('PageView') })</script>A save equal to the stored state writes nothing, so the per-page save costs one message and no storage for a visitor
who did not change their mind. If loadFeatures fails, the page sends no PageView. If you would rather count the
view, send one in the error branch. It is then delivered under the consent Qinto already stores for the visitor,
which is nothing for a first visit and the last saved choice for a returning one.
If your store runs the Qinto tag in a custom pixel instead of the theme, the pixel receives the same four values as
booleans on init.customerPrivacy, and api.customerPrivacy.subscribe('visitorConsentCollected', …) reports a
change. A pixel runs in a sandbox and sends its events itself, so it waits for the save before each event:
const consentFromShopify = (privacy) => { return { categories: { functional: privacy.preferencesProcessingAllowed, analytics: privacy.analyticsProcessingAllowed, marketing: privacy.marketingAllowed, salesOfData: privacy.saleOfDataAllowed } }}
let consented = qinto.consent(consentFromShopify(init.customerPrivacy))
api.customerPrivacy.subscribe('visitorConsentCollected', (event) => { consented = consented.then(() => qinto.consent(consentFromShopify(event.customerPrivacy)))})
analytics.subscribe('page_viewed', async () => { await consented qinto.track('PageView')})Two Shopify settings matter for the pixel. In its Customer privacy section, set Permission to
Not required and Data sale to Data collected does not qualify as data sale. With Permission set to
Required, Shopify does not run the pixel until the visitor grants it, and with data sale marked as a sale, Shopify
does not run it for a visitor who opted out. In both cases Qinto never learns the choice. Qinto gates per app instead,
so the pixel runs for every visitor and the salesOfData category carries the opt-out to the apps. Whether your store
may collect events before consent depends on its markets and your legal advice. A store that must not collect anything
before consent keeps Permission on Required, and Qinto then sees nothing from a visitor who did not consent.
Then turn on consent mode for the site, as Consent mode describes.