init({ context }) channel; there is no
second API. The rule behind them never changes:
Facts from the page ground answers. They never authorize anything. Puntego never derives authority from page text or from unsigned context.
Release preview: the@puntego/reacthelpers below (usePuntegoContext,signPuntegoAccountFacts) and the headlesssetContextare implemented and tested in the repository, but the packages are not yet published to npm.window.Puntego.init({ context })works with the hosted script today.
Two trust levels
If a signed fact and a page-reported fact disagree, the signed fact wins and
the page value is dropped. Facts written into page text, retrieved knowledge or
the visitor’s message are not account facts.
When a table, card or meter in an answer shows one of these facts, Puntego
writes the value exactly as your app sent it, in the answer’s language, and
records which fact it came from and at which trust level; the AI model only
names the fact. See values from your data.
The contract (v1)
Every field is optional. Unknown fields are ignored; a bad value is dropped on its own and the rest still count.period is one of day, week, month, year or billing_period.
accountId is only used to notice an account switch and to match a signed
token; it is never shown to the AI model. traits reach the model only when
you map them.
Limits. Text values up to 120 characters; up to 12 usage meters, 40
entitlements and 24 traits; the whole accountFacts object up to 16 KB; a
signed token up to 8 KB with a lifetime of at most 12 hours.
Privacy. Account facts describe an account, never a person. Keys named like
a credential or a direct identifier (email, phone, password, token, key,
secret, session, card, address and similar) are always refused, whatever you
configure. A value that contains an email address, phone number, card number,
IBAN or national ID is dropped whole. Puntego reads account facts for each
message and does not save them with the conversation or in workspace exports.
The guide’s answers are kept like any other answer, so a fact an answer
mentions, in its words or in a table or card shown with it, is part of that
answer.
Pass page-reported facts
With the hosted script, hand the guide a function; it is read on every message:init({ context }) replaces the function and settles an account switch
at once; on its own it changes nothing else (the panel, theme and launcher stay
as they are).
With React, pass context to the provider, or add it where your app loads the
account with usePuntegoContext:
client.setContext({ accountFacts }) whenever the facts change.
Sign facts on your server
- In the dashboard, open Settings → Account facts and choose Create
signing secret (owners and admins; it asks for two-factor
authentication). The secret starts with
afs_and is shown once. - Store it in server-only configuration, for example
PUNTEGO_ACCOUNT_FACTS_SECRET. Never ship it to the browser. - Sign the facts from your authenticated session and hand the token to the
page as
signedAccountFacts:
createPuntegoAccountFactsRoute({ appId, secret, resolve }), a POST handler
for Next.js App Router, Remix or any Fetch API runtime that answers
{ signed_account_facts, expires_at } with Cache-Control: private, no-store.
Audience. appId must be the app id of the widget on that page: your
production gp_… id, or the gp_test_… id in test mode. A token
for another app is refused.
Expiry. One hour by default, at most 12 hours; Puntego allows two minutes
of clock skew. An expired token is ignored (the message is still answered,
without the signed facts), so refresh it on your normal session cadence.
Account. The token’s subject is accountId. If facts.accountId is set it
must match. When the visitor is also signed in with a
customer identity token, signed facts about any
other account than the verified one are ignored. Otherwise, when the page’s
accountFacts.accountId names another account than the token, the conversation
is that account’s and the token’s signed facts are ignored for the message
(for example, a token not yet refreshed after an account switch).
Person. A role and a contact email belong to one person of the account,
not to the account. Sign userId (the same externalId you pass to
mintPuntegoIdentity) to bind them to that person. When the visitor is also
signed in with a customer identity token, a
token signed for another person of the account is ignored (for example, one not
yet refreshed after someone else signed in), and a token without userId still
grounds answers but its role never meets an
audience rule and its contact email is not used.
Other languages. The token is a compact JWS: header exactly
{"alg":"HS256","kid":"afk_<id>","typ":"JWT"}, where <id> is the 20
characters after afs_ in your secret; claims
{ "aud": appId, "sub": accountId, "iat", "exp", "v": 1, "facts": { … } },
plus "user": userId when you sign the person and "contact": { "email": … }
when you sign the visitor’s email (see below); signature HMAC-SHA256 over
header.payload with the whole secret string as the key. Any other header
field, algorithm or key id is refused, and so is a user that is not a
non-empty id.
The visitor’s own email. Governed actions that file something in the
visitor’s name (a support ticket, a meeting) need their email. Sign it with
contactEmail (one plain ASCII address; sign an internationalized domain in
its xn-- form) and Puntego fills that input itself when the visitor confirms;
it is never shown to the AI model, never an account fact and never part of a
prompt. A customer identity token with an
email trait does the same. Without either, those actions are not offered at
all, so the guide never asks the visitor for an email it could not use:
From a host read tool
When the facts are expensive to compute on every page, let the guide ask for them only when a question needs them: a host read tool can return{ signedAccountFacts } (a token your server signed) and the guide verifies it
exactly like the one above. Anything else a tool returns grounds answers like
page-reported facts.
Map your own keys
If your app already puts account data ininit({ context }) under its own
keys, map them in Settings → Account facts → Your keys instead of changing
code. Use a dot for a nested key: billing.plan reads { billing: { plan } }.
Keys match exactly, including case:
A typed
accountFacts value wins over a mapped key for the same fact. Mapped
keys, like accountFacts and signedAccountFacts themselves, never also reach
the model as generic page context.
Signed only. Mark a mapped key, or a built-in fact such as Plan, as
Signed only and the guide uses it only when it arrives in
signedAccountFacts. Use it for anything that must not be taken from the
browser’s word: discounts, contract terms, limits that change what you promise.
Account switches
When the account the facts describe changes (accountFacts.accountId, or the
signed token’s subject, goes from account A to account B), the guide starts a
new conversation straight away: the transcript with its tables and cards, any
answer still streaming and the conversation’s server history belong to A and are
not carried into B. A
reload into B does not resume A’s conversation either.
A page that reports no facts is not a switch; the guide keeps remembering the
last account, so A → no facts → B still starts over. Signing out is handled by
your identity lifecycle (destroy(), or the React provider’s
customerIdentityToken). Always include accountId when a visitor can belong
to more than one account.
Returning-visitor memory is not part of the conversation and is not cleared by
an account switch. It is the visitor’s own opt-in and is kept per person, not
per account: per signed customer when you use customer identity, otherwise per
browser. If people from different accounts share one browser, sign them in with
customer identity so each keeps their own memory, or leave memory off.
Troubleshooting
- The guide does not use a fact. Check the key is not refused (see
Privacy), the value is within the limits, and, for a mapped key,
that it is mapped. A fact marked Signed only needs
signedAccountFacts. - Signed facts are ignored. The token is expired, was signed for another
app id (test and production differ), names another account than the signed-in
customer or than the page’s
accountFacts.accountId, or its secret was rotated more than 24 hours ago or turned off. accountFacts was not sentin the console. The object is not plain JSON or is over 16 KB; the message is still sent without it.