This framework-free PHP application demonstrates how a customer application can give a standard Guanta chat widget the verified identity of a logged-in user.
The important part of this example is the server-to-server identity exchange. The application continues to own login, passwords, sessions, user records, and authorization. Guanta receives only the trusted user identifier and display name that the application backend chooses to assert.
The included CSV login is intentionally small and is not a production authentication system. It exists only so the complete integration can be run and inspected without a framework or database.
The same guide is also available as a standalone HTML document.
The question “Who am I?” makes the identity boundary easy to see, but an AI response must not be used as an application authorization decision.
The customer application remains the identity provider for its own users:
| Customer application | Guanta |
|---|---|
| Authenticates the user | Trusts an assertion made by the authenticated customer backend |
| Stores passwords, SSO identities, or other login credentials | Never receives the user's password or password hash |
| Maintains the application session and creates an opaque ID for each successful login | Creates and securely resumes a separate widget conversation session |
| Chooses a stable external user ID, display name, and per-login context ID | Binds the conversation to that exact asserted identity and login context |
| Decides what the user may do in the customer application | Makes the trusted identity available to the configured Guanta agent |
Guanta authentication does not replace the customer's login system. It connects an already-authenticated customer session to a Guanta widget session.
Each customer integration is isolated inside a Guanta tenant:
Guanta tenant
└── Agent
└── Widget interface
├── Public widget ID
├── Allowed customer origins
└── Widget identity credentials
├── Customer production backend
└── Customer staging backend
GUANTA_BASE_URL
selects this tenant runtime.wrt_... value stored by the loader in
first-party sessionStorage. It cannot resume an
authenticated conversation without a fresh matching identity token.sessionStorage. Guanta
restores an open authenticated widget only after confirming that the
same conversation resumed.A credential created in one tenant cannot authenticate against another tenant. A credential for one widget interface cannot issue a usable identity token for a different widget.
It is normal to create multiple credentials for the same widget—for example, separate credentials for staging and production, or for two independently deployed customer backends. This makes rotation and revocation safer.
The token is created only when the widget is about to initialize. Creating it when the page first renders could allow it to expire before the visitor opens the widget.
The environment-variable names below are conventions used by this example. A customer may use different names in their own application.
| Example variable | Scope | Secret? | Source |
|---|---|---|---|
GUANTA_BASE_URL |
Tenant and environment | No | The customer's Guanta tenant URL |
GUANTA_WIDGET_PUBLIC_ID |
Widget interface | No | The widget loader snippet in the interface Playground |
GUANTA_WIDGET_IDENTITY_CREDENTIAL |
Widget interface and customer backend | Yes | Created in the interface's Widget Identity Credentials section and shown once |
GUANTA_WIDGET_LOADER_INTEGRITY |
Published widget-loader version | No | The widget loader snippet in the interface Playground |
In the Guanta tenant UI:
The widget's allowed origins must include the customer application's exact origin, including scheme and port where applicable.
Copy the environment template:
cp .env.example .envConfigure the application and Guanta values:
APP_URL=http://127.0.0.1:8088
APP_SESSION_SECURE=false
GUANTA_BASE_URL=https://your-tenant.example
GUANTA_WIDGET_PUBLIC_ID=your-widget-public-id
GUANTA_WIDGET_IDENTITY_CREDENTIAL=wic_your-credential-id.your-secret
GUANTA_WIDGET_LOADER_INTEGRITY=sha384-your-published-integrity-value
GUANTA_WIDGET_IDENTITY_CREDENTIAL is sensitive. Keep it
only in the ignored .env file for local development or in
the deployment platform's secret store. Never place it in HTML,
JavaScript, source control, screenshots, logs, analytics, or browser
storage.
Start the local application:
./scripts/run.shThen open http://127.0.0.1:8088.
The PHP built-in server is for local demonstration only. Use a supported web server and PHP runtime for an internet-facing deployment.
The committed CSV contains password hashes rather than plaintext passwords. The three local demo identities are:
| External ID | Full name | Password availability | |
|---|---|---|---|
1 |
jtorras@guanta.ai |
Jordi T Guanta | Distributed separately |
2 |
jordi@torras.ai |
Jordi Torras | Distributed separately |
3 |
user@example.com |
George Towers | B_timu_bicu_fila_4315 |
The public user@example.com credentials are prefilled in
the login form so a customer can test the authenticated flow
immediately. All demo credentials are for this example only and must not
be reused for real accounts.
For a real integration, replace the CSV authentication with the application's existing session, SSO, OAuth, OpenID Connect, Laravel, Symfony, or other authentication mechanism. The Guanta exchange begins only after that system has authenticated the request.
The page uses the ordinary Guanta widget loader with one additional attribute pointing to a customer-owned endpoint:
<script
src="https://your-tenant.example/widget/embed-loader.v1.1.0.js"
integrity="sha384-your-published-integrity-value"
crossorigin="anonymous"
data-public-id="your-widget-public-id"
data-identity-token-url="/widget-identity.php"
defer>
</script>data-identity-token-url must resolve to the customer
page's own origin. The loader calls it with POST when the
widget initializes and includes the customer application's normal
same-origin session cookie.
The endpoint must determine the current user and login context
exclusively from the trusted server-side session. It must never accept
external_user_id, full_name, or
login_context_id from browser input.
When no application user is logged in, the endpoint returns anonymous state:
{"identity_token":""}When a user is logged in, it returns the one-time token received from Guanta:
{"identity_token":"wit_<token-id>.<one-time-secret>"}Responses containing identity information must use
Cache-Control: no-store and should not be logged.
The customer backend creates a token by calling the tenant-specific endpoint:
POST /api/v1/widget-identity-tokens HTTP/1.1
Host: your-tenant.example
Authorization: Bearer wic_<credential-id>.<secret>
Accept: application/json
Content-Type: application/json
{
"widget_public_id": "your-widget-public-id",
"external_user_id": "stable-customer-user-id",
"full_name": "Customer User",
"login_context_id": "opaque-id-for-this-login-session"
}
Successful response:
HTTP/1.1 201 Created
Cache-Control: no-store, private
Content-Type: application/json
{
"identity_token": "wit_<token-id>.<one-time-secret>",
"widget_public_id": "your-widget-public-id",
"expires_at": "2026-08-14T12:00:00+00:00"
}
Field guidance:
widget_public_id must match the widget interface to
which the credential belongs.external_user_id should be the application's stable,
non-secret identifier for the user. Prefer an immutable database ID or
UUID over an email address that may change.full_name is the display name made available to the
agent.login_context_id should be an opaque, unpredictable
value generated on successful login, retained only in the customer's
server-side session, and replaced on every later login. It enables
secure conversation continuity and should not be a raw session cookie or
session identifier.The API accepts an external user ID and login context ID of up to 128 characters and a full name of up to 191 characters. The login context is optional for backward compatibility, but authenticated conversations without it are intentionally non-resumable.
Typical error responses:
| Status | Meaning |
|---|---|
401 |
The identity credential is missing or invalid |
403 |
The credential is not authorized for the widget, or the interface is unavailable |
422 |
A required identity field is missing or invalid |
429 |
The credential exceeded the identity-token rate limit |
Treat any unsuccessful response as an authenticated initialization failure. Do not silently downgrade a logged-in visitor to anonymous, because that can hide configuration or availability problems.
public/index.php authenticates a demo user and stores
the user's ID plus a newly generated opaque login context in the PHP
session.public/widget-identity.php when
the widget opens.widget-identity.php reads the current user from the PHP
session.src/app.php sends the server-to-server request with the
user assertion and login context to the tenant's identity-token
API.sessionStorage, so
conversation and open/closed state survive refreshes and full-page links
within the same browser tab.The example's identity endpoint additionally requires a
POST request marked with
X-Requested-With: GuantaWidget. This is a defense-in-depth
check, not a replacement for session security, same-origin controls, or
CSRF protections appropriate to the customer's application.
Keeping the customer logged in is necessary, but it is not sufficient to restore a widget conversation. The same customer user may log out and later log in again, so Guanta must be able to distinguish one login session from the next. Otherwise, an old browser-held resume credential could incorrectly reopen a conversation from an earlier login.
The following pieces work together:
| Component | Owner | Requirement |
|---|---|---|
| Authenticated application session | Customer backend | Identifies the current user on every request to the same-origin identity endpoint |
| Stable external user ID | Customer backend | Remains the same for the customer account; it is not sufficient by itself to resume a conversation |
Per-login login_context_id |
Customer backend | Generated after each successful login, stored in the server-side session, kept unchanged during navigation, and replaced after logout or the next login |
| Fresh identity token | Customer backend and Guanta API | Created from the current user and login context for every widget initialization; it remains short-lived and single-use |
| Loader version 1.1 or later | Customer page | Included with the same Guanta tenant URL, widget public ID, and same-origin identity endpoint on every page |
| Conversation resume token | Guanta loader | Stored automatically as an opaque wrt_... value in
first-party sessionStorage; customer code should not
create, inspect, transmit, or persist it elsewhere |
| Stable widget scope | Customer page and Guanta | The tenant, widget public ID, and allowed page origin must continue to match |
| Browser tab storage | Browser | sessionStorage must remain available; it normally
survives reloads and same-tab navigation but is separate in a new
tab |
During ordinary full-page navigation:
sessionStorage.login_context_id from its server-side session and requests
a new one-time identity token.The customer application does not need to save messages, copy a Guanta session ID between pages, or expose any conversation identifier in HTML. It should only preserve its own authenticated session and per-login context. Loader version 1.1 handles the Guanta resume credential automatically.
If the identity, login context, widget, origin, expiry, or stored token does not match, Guanta does not restore the earlier authenticated conversation. Login, logout, account switching, and subsequent login therefore begin with a closed, fresh widget conversation.
The demo keeps presentation in the non-public html/
directory and application logic in src/. The three page
templates share one layout and use small partial templates for
authenticated and anonymous states. The public PHP files remain thin
entry points.
Templates contain simple placeholders rather than executable PHP:
<h2>Welcome, {{full_name}}</h2>src/template.php provides the deliberately small
replace_tags() helper. Ordinary replacement values are
HTML-escaped by default, so names, email addresses, validation messages,
and other data cannot inject markup. Complete HTML fragments may be
inserted only through the helper's separate trusted-fragment argument;
this example uses that path exclusively for partials that were
themselves rendered from files under html/.
The renderer rejects unknown placeholders, invalid placeholder names,
non-scalar ordinary values, and template paths outside the
html/ directory. Templates contain no PHP. This is
intentionally not a general-purpose template engine; production
applications should normally use their framework's established
templating system and its escaping rules.
The server-side logic is framework-independent:
receive POST from the widget loader
load the current user from the authenticated application session
if there is no current user:
return { "identity_token": "" }
load the opaque per-login context ID from the same server-side session
call the Guanta tenant API using the server-only credential
send widget_public_id, stable external_user_id, full_name, and login_context_id
if Guanta does not return 201:
fail the authenticated widget initialization
return the one-time identity_token with no-store cache headers
the standard loader handles the opaque conversation resume token automatically
In a production framework, place the Guanta API call in a server-side service and protect the same-origin identity endpoint with the application's normal session middleware. Never let the browser submit or override the asserted user ID.
An identity token is still short-lived and single-use. Conversation continuity comes from a separate Guanta resume token that is valid only alongside a fresh identity token asserting the same user and login context.
resetConversation() method.The example's Home, Account, and Orders links perform genuine full-page navigation so conversation resumption is visible and easy to test.
external_user_id, full_name, and the
opaque login_context_id are asserted by this version of the
API.The trusted identity is context for the Guanta agent. It does not grant permissions inside the customer application, and agent output must not be treated as proof of authentication or authorization.
Use separate credentials for separate customer environments or independently operated backends. Give each credential a descriptive name and an expiry where appropriate.
For rotation without interruption:
Regenerating an existing credential invalidates its previous secret immediately. Revoking a credential also prevents unconsumed tokens issued by that credential from being accepted.
APP_SESSION_SECURE=true for this example when
served over HTTPS..env files, credentials, identity tokens, and
authenticated endpoint responses out of source control and logs.| Symptom | What to check |
|---|---|
401 Invalid widget identity credential |
Confirm the complete credential was copied once, is active, has not expired, and belongs to this tenant |
403 Credential is not authorized for this widget |
Confirm the credential and GUANTA_WIDGET_PUBLIC_ID came
from the same widget interface |
422 response |
Confirm widget_public_id,
external_user_id, full_name, and any supplied
login_context_id are within their limits |
429 response |
Check for repeated initialization loops and the configured rate limit |
Example returns 502 from
/widget-identity.php |
Inspect the customer backend's connectivity and Guanta API response without logging credentials or tokens |
| Widget remains anonymous after login | Confirm the customer session cookie reaches the same-origin identity endpoint and that the endpoint returns a token |
| Token is expired or already used | Mint tokens only when the widget opens and never cache or reuse them |
| Widget does not load | Check the loader URL, SRI value, allowed origin, Content Security Policy, and widget public ID |
| Conversation does not survive page navigation | Confirm loader version 1.1 is deployed, the login context stays
stable, and sessionStorage is available |
| Conversation survives but the widget does not reopen | Confirm Guanta reports a successful resume and that browser
sessionStorage is available in the current tab |
| Earlier conversation returns after logout/login | Confirm the backend generates a new login_context_id on
every login and clears it on logout |
public/index.php: Home entry point and login/logout
UIpublic/account.php: separate full-page account
routepublic/orders.php: separate full-page orders routepublic/widget-identity.php: same-origin browser
endpoint that calls Guanta from the backendsrc/app.php: CSV authentication, PHP session, per-login
context, configuration, and Guanta API clientsrc/page.php: page actions, template selection, and
standard widget configuration for the three demo pagessrc/template.php: escaped-by-default
replace_tags() helper and template-path validationhtml/layout.html: shared document shell and
navigationhtml/pages/: Home, Account, and Orders page
templateshtml/partials/: authenticated, anonymous, setup, error,
and widget-script fragmentsdata/users.csv: demo identities with PHP password
hashesscripts/run.sh: local PHP development serverscripts/build-docs.sh: reproducibly renders the PNG
diagram and standalone HTML guidedocs/authentication-flow.mmd: Mermaid source for the
authentication sequence diagrampublic/docs/authentication-flow.png: rendered diagram
used by the README and HTML guidepublic/docs/guanta-logo.svg: local copy of the official
Guanta logotype used by the demo and guidepublic/authentication-guide.html: generated standalone
HTML version of this guide.env.example: non-secret configuration templatetests/check.php: validation for demo users, login
contexts, templates, escaping, and rendering safetyValidate the demo users and password hashes:
php tests/check.phpRebuild the documentation artifacts after editing this README or the Mermaid source:
./scripts/build-docs.shFor an end-to-end check:
If Guanta rejects a correctly formed request or the required tenant values are unavailable, contact your Guanta representative with the tenant name, widget interface name, timestamp, and HTTP status. Do not include credentials or identity-token values.