Install
One script tag. Serve it yourself.
No build step, no framework, no dependency. Put 3way.bundle.js on your own
origin, drop this at the bottom of the page, and the widget mounts itself.
<script src="/js/3way.bundle.js" data-3way-api="https://api.yourstore.com" data-3way-store="Yourstore Support"></script>
Measured This is the literal embed on the live demo — one tag, 18 tools registered, policy fetched at mount.
Where to host it
Your consent gate should not load from someone else's server.
Stripe can tell you to load js.stripe.com because Stripe is the
counterparty — you are trusting them by definition. Here you are the
counterparty, and this widget enforces your gate. A third-party script tag that
someone else can change is a script that can be changed to remove the gate.
So the recommendation is the boring one: download the file and serve it from your own origin. Your gate, your origin, your control.
To try it without hosting anything
There is a versioned copy here. It is fine for a prototype and it is pinned with Subresource Integrity, so a swapped file fails to load rather than running:
<script src="https://3way.dev/v1/3way.bundle.js"
integrity="sha384-YEUIr0xy6ZSCat7Ir09uqNyt1FiVAsqHGTuDu+SeEHG6h/q9R3bYs+Oi4gVNi/LJ"
crossorigin="anonymous"
data-3way-api="https://api.yourstore.com"></script>
Versioned on purpose. An unversioned URL means any change silently reaches every page that ever embedded it.
Why the policy isn't in the tag
Because a page must not be able to disagree with its own gate.
The widget needs your returns policy — the prose a person reads and the rules the gate enforces. Those are objects, and an attribute is a string, so the only honest route to one tag is to serve them from your API.
That is where they belonged anyway. Your server is already the authoritative gate's source of truth for the same rules. Keeping one copy is what stops a page showing someone one policy while the action enforces another.
If the config cannot be fetched, nothing mounts and the console says why. A widget that quietly fell back to a default policy would be displaying terms that are not yours.
| Attribute | |
|---|---|
data-3way-api | Required. Where your API lives. Its absence is how a page opts out of auto-mounting. |
data-3way-store | How your agent is named in the transcript. Defaults to “Store”. |
data-3way-user | Display name for the signed-in person. Nothing security-relevant reads it. |
data-3way-stance | Which personality preset to use. Overrides the server default. |
data-3way-tenant | Which store a shared backend should answer as. |
What your API answers
One endpoint.
GET /api/config, returning the policy the widget will show and the
rules its gate will read. Public by design — a returns policy is published anyway.
Everything else is the conventions: the tool names, the origins, and the refusal shape. Read them →
{
policy: {
prose: "…what a person reads",
rules: { requiresHumanDirect: [ … ] }
},
stances: { … },
storeAgentName: "Yourstore Support"
}
Full control
Or mount it yourself.
A tag with no data-3way-api is inert, so nothing auto-mounts and
ThreeWay.mount() takes every option directly — useful when the policy is
already in the page, or when you need a custom mount point.