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 | Creates a separate widget conversation session |
| Chooses a stable external user ID and display name | Stores that asserted identity with the widget session |
| 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.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.0.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 exclusively from the
trusted server-side session. It must never accept
external_user_id or full_name 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"
}
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.The current API accepts an external user ID of up to 128 characters and a full name of up to 191 characters. Both values are required.
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
only the user's ID 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 to the
tenant's identity-token API.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.
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": "" }
call the Guanta tenant API using the server-only credential
send widget_public_id, stable external_user_id, and full_name
if Guanta does not return 201:
fail the authenticated widget initialization
return the one-time identity_token with no-store cache headers
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 initializes one widget session; it is not a permanent login token.
This example performs full-page navigation after login and logout, which naturally creates a fresh widget instance.
external_user_id and full_name 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, and full_name are present
and 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 |
| Identity persists after logout or account switch | Destroy or reload the existing widget iframe and start a new widget session |
public/index.php: login/logout UI and standard widget
embedpublic/widget-identity.php: same-origin browser
endpoint that calls Guanta from the backendsrc/app.php: CSV authentication, PHP session,
configuration, and Guanta API clientdata/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 the demo CSV and
password hashesValidate 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.