Consent
One consent to your own cookie policy, asked by your banner or by the chat, and the three arcy.js calls that keep both sides on the same answer.
ARCY stores nothing on a visitor's device until that visitor has agreed to your cookie policy. There is one consent, not two: your own policy carries the cookie template that describes what arcy.js writes, so a visitor who accepts your banner has accepted ARCY too. The widget only needs to hear about it.
There are two places a visitor can say yes, and both count:
- Your cookie banner. Its Accept handler calls
arcy.consent(true), its Reject handler callsarcy.consent(false). This is the usual path. - The chat itself. A visitor who reaches for the chat without a grant on record is asked right there, in your voice, with a link to your privacy policy and your cookie policy.
The two are not alternatives you pick between. There is nothing to configure per environment. Every environment behaves the same way, and the only thing you give ARCY is two URLs on the Consent card in Settings > App.
What the chat asks, and when
The ask appears the first time a visitor does something that needs storage: pressing Send, tapping a starter question, opening the History, Flows or Settings drawer, or pressing the image button. It never appears on opening the panel, reading the greeting or shrinking the panel to the corner. Looking is free; using is what needs an answer.
It reads as your site speaking ("We use cookies..."), links your privacy policy and your cookie policy, and has one button: Accept. There is no Decline. Closing the panel means "not now": nothing is written, and the ask comes back the next time the visitor tries to use the chat.
Until you fill both URLs on the Consent card, the ask links ARCY's own pages instead: arcyai.com/privacy and arcyai.com/cookie-policy. They are accurate, and they are ours rather than yours, so the card warns you until both fields are filled. The same fallback applies to the "[your app name] is AI and can make mistakes · Privacy Policy" line under the chat bar.
What a refusal means
arcy.consent(false) from your banner records a refusal for six months. A refusal does one
thing: it stops ARCY from speaking first. A flow whose conditions match
will not open on its own and raise the ask for a visitor who said no. The visitor can still
reach for the chat, and when they do, the chat asks again. That second ask is deliberate. A
visitor who rejected the banner and then types a question twenty seconds later now wants a
service your policy describes, and asking at that moment is your second chance at the same
consent.
A visitor with no answer at all is different: a matching flow may raise the ask on its own, because nobody has said no yet.
Calling arcy.consent(false) after a grant is a withdrawal. Collection stops, every
cookie and key ARCY wrote on the device is deleted, and the refusal is recorded. A visitor
who accepts later starts as a new visitor with no history. Withdrawal is forward-looking:
nothing already stored on our side is deleted by it, and erasure is a separate request.
There is no consent control inside the widget's own Settings sheet. Your banner is where a visitor changes their mind, because it is your policy they are changing their mind about.
The three calls
All three need init() to have run first. Calling any of them before init() logs a
console warning and does nothing.
consent(granted)
consent(granted: boolean): voidRecords the visitor's answer. true is a grant: ARCY can now write its anonymous id cookie
and start its session. false is a refusal, or a withdrawal if a grant was on record.
Calling it with the same answer that is already recorded changes nothing.
consentState()
consentState(): "granted" | "refused" | nullThe current answer, read from the visitor's own device. null means the visitor has never
answered, or answered an older version of the disclosure and needs to be asked again. Use it
to decide whether your banner should show itself at all.
onConsent(callback)
onConsent(callback: (granted: boolean) => void): () => voidFires on every change, from any source: your banner calling consent(), a visitor pressing
Accept inside the chat, or a withdrawal. The callback receives true or false. It
returns a function that unsubscribes. This is the bridge that lets an Accept inside the chat
reach your own analytics, your CMP or Google Consent Mode, so that both sides hold one
answer.
Wiring it
A plain banner
The HTML install path loads arcy.js asynchronously, so read the state once the widget is ready rather than on the first line of your script.
<div id="cookie-banner" hidden>
We use cookies. <a href="/cookie-policy">Read more</a>
<button id="cookie-accept">Accept</button>
<button id="cookie-reject">Reject</button>
</div>
<script>
// The loader from Settings > Installation runs above this.
arcy.on("ready", function () {
var banner = document.getElementById("cookie-banner")
if (arcy.consentState() === null) banner.hidden = false
document.getElementById("cookie-accept").onclick = function () {
arcy.consent(true)
banner.hidden = true
}
document.getElementById("cookie-reject").onclick = function () {
arcy.consent(false)
banner.hidden = true
}
// A visitor who accepts inside the chat should not see the banner again.
arcy.onConsent(function (granted) {
if (granted) banner.hidden = true
})
})
</script>Google Consent Mode
If Google Analytics or Google Ads runs behind Consent Mode, update it whenever ARCY's answer changes. A visitor who accepts inside the chat then unlocks your analytics too, and a withdrawal locks it again.
arcy.onConsent((granted) => {
gtag("consent", "update", {
analytics_storage: granted ? "granted" : "denied",
})
})Keep your banner's own gtag('consent', 'update', ...) call as it is. Both paths end in the
same place.
A consent management platform
Every CMP exposes an accept and a reject callback and a way to read its own state. The names differ; the shape is always the same. Two lines carry the CMP's answer into ARCY, and one callback carries an Accept inside the chat back to the CMP. Check the CMP's state before writing to it so the two do not call each other in a loop.
cmp.onAccept(() => arcy.consent(true))
cmp.onReject(() => arcy.consent(false))
arcy.onConsent((granted) => {
if (granted && !cmp.hasConsent()) cmp.acceptAll()
})React
One component owns init(), the banner and the subscription, so nothing reads the state
before the widget is ready.
"use client"
import { useEffect, useState } from "react"
import arcy from "arcy.js"
export function ARCY() {
const [showBanner, setShowBanner] = useState(false)
useEffect(() => {
let unsubscribe = () => {}
arcy.init("arcy_pk_...").then(() => {
setShowBanner(arcy.consentState() === null)
unsubscribe = arcy.onConsent((granted) => {
if (granted) setShowBanner(false)
})
})
return () => unsubscribe()
}, [])
if (!showBanner) return null
const answer = (granted: boolean) => {
arcy.consent(granted)
setShowBanner(false)
}
return (
<div role="dialog">
We use cookies. <a href="/cookie-policy">Read more</a>
<button onClick={() => answer(true)}>Accept</button>
<button onClick={() => answer(false)}>Reject</button>
</div>
)
}Render <ARCY /> once, in your root layout, next to where you already call init().
What ARCY writes after a grant
Two first-party cookies on your registrable domain, arcy.consent.<Token> (the answer, the
only thing written before a grant) and arcy.anon.<Token> (the anonymous id, written only
after one), plus a handful of localStorage and sessionStorage keys. The full list, with
lifetimes and copy-paste language for your own policy, is in the
cookie policy template.
Where the answer shows up
A session's Activity page shows whether the grant came from your banner or from the chat, so you can answer your own counsel without guessing.
See also
- App: the Consent card with the two URL fields
- Reference: the full arcy.js API
- Cookie policy disclosure: what to paste into your own policy