Serve an Agent From a Web App
Serve a Platform agent from a customer web application using External Integration credentials and external-user session tokens.
This guide shows how to embed an Amigo Platform agent in your own web application without exposing workspace API keys or external integration secrets to the browser.
The production pattern is a backend-for-frontend (BFF):
Your backend stores an External Integration
client_idandclient_secret.Your backend exchanges those credentials for a parent JWT with the
external_user_sessions:createscope.Your backend mints a short-lived external-user session JWT for the signed-in web user.
Your browser uses only that external-user JWT to call scoped conversation endpoints.
Your browser refreshes the external-user JWT through your backend when it is near expiry.
Never put the External Integration client_secret, parent access token, workspace API key, admin API key, or external-user refresh token in browser code. The browser should receive only the current external-user access token and non-secret session metadata.
Prerequisites
Before you start, collect these values:
AMIGO_PLATFORM_API_URL
Amigo
Usually https://api.platform.amigo.ai. Token exchange is mounted at /token; workspace APIs are mounted under /v1/{workspace_id}.
AMIGO_WORKSPACE_ID
Developer Console Settings page for the customer workspace
The workspace that owns the agent, service, integration, and user data. Make sure you are viewing the customer's workspace, not an internal or staging workspace, before copying the ID.
AMIGO_SERVICE_ID
Developer Console Services page
Open the service your web application will expose and copy its service ID. It should point at the desired agent and version set.
Admin or owner API key
Amigo dashboard or existing provisioning
Needed once to create the External Integration and credential. The caller must be able to create/update external integrations and create/delete API-key-backed credentials. Do not use this from browser code.
External Integration client_id and client_secret
Created below
Stored only in your backend secret manager.
Agent Forge Go v0.1.3+
Amigo Agent Forge Go CLI
Required for forge platform external-integration and nested credential commands. Verify with forge --version.
TypeScript SDK @amigo-ai/platform-sdk v0.66.0+
npm package
Required for the External Integration, token exchange, external-user session, refresh, and conversation streaming helpers used below.
Customer subject key
Your application
A stable per-user key such as your internal user ID. Amigo stores only a keyed hash of this value.
consumer_entity_id
Optional Amigo world model entity
Not required for authenticated or anonymous web conversations. Pass it only when the web user is already linked to a patient, member, customer, or other materialized entity that the agent should use as conversation context.
You also need a customer backend that can authenticate the browser user before minting an authenticated Amigo token, or apply product-specific abuse controls before minting an anonymous Amigo token.
Authenticated and Anonymous Conversations
External-user sessions support both authenticated customer accounts and anonymous guest sessions. Choose the subject type when your backend mints the external-user session:
User mode
subject_type
external_subject_key
consumer_entity_id
Authenticated customer account
user
A stable internal user ID from your application. Do not use an email address or phone number that can change.
Optional. Pass it only if that app user is already linked to an Amigo world model entity.
Anonymous guest session
anonymous
A stable anonymous session or visitor key generated by your application.
Usually omitted. Pass it only if your product intentionally links the guest session to a materialized Amigo entity.
A pre-existing Amigo world model entity is not required to start agent conversations for authenticated customer accounts. When you omit consumer_entity_id, Amigo creates and uses a stable external subject derived from your external_subject_key and the conversation is still scoped to the selected service_id.
Pass consumer_entity_id when the agent needs to operate with an existing materialized entity, such as a patient, member, customer, lead, or account record. That lets the agent use the correct world model context and keeps the conversation associated with that entity. If the web user is not linked to a materialized entity yet, omit consumer_entity_id and create or link the entity later through your normal application flow.
Anonymous sessions use the same External Integration credential and conversation flow, but your backend remains responsible for abuse controls such as rate limits, CAPTCHA or bot checks, origin checks, and session expiration before minting or refreshing guest tokens. Do not expose External Integration credentials or Amigo refresh tokens to the browser for either mode.
System Design
Use this boundary model:
Customer browser
Renders chat UI, holds the external-user access token in memory, creates conversations, sends user messages, and streams agent responses.
Customer backend
Authenticates the user, stores external integration secrets and external-user refresh tokens, exchanges parent credentials, mints and refreshes external-user tokens, applies your app's authorization checks.
Amigo Identity
Issues parent JWTs, external-user JWTs, and rotating refresh tokens.
Amigo Platform API
Accepts the external-user JWT for scoped conversation access.
Amigo agent runtime
Runs the service's agent, context graph, tools, and safety behavior.
Create the External Integration
You can provision with forge, the TypeScript SDK, or direct HTTP. The examples below use Agent Forge Go v0.1.3+ for one-time setup because it includes both platform external-integration and platform external-integration credential commands and prints the one-time secret clearly.
Check your Forge version:
Configure Forge with a workspace-scoped admin or owner credential:
Create an integration record:
Save the returned id as AMIGO_EXTERNAL_INTEGRATION_ID.
Create a credential scoped to the service you will expose:
Save these response fields in your backend secret manager:
The client_secret is returned only on create and rotate. If you lose it, rotate the credential and store the replacement.
TypeScript SDK Snippets
The public SDK guide has a complete TypeScript walkthrough: External User Text Conversations. The snippets in this guide require @amigo-ai/platform-sdk v0.66.0+.
Install the SDK:
Create the integration credential from an admin or setup job:
Mint an external-user session from your backend:
Use the child access token with a separate SDK client:
Runtime Backend Flow
Your backend exposes a route such as POST /api/amigo/session. It should:
Authenticate the web user with your normal app session.
Decide whether the user may access this Amigo service.
Choose
subject_type:userfor authenticated customer accounts.anonymousfor guest sessions.
Set
external_subject_keyto a stable app-side key. Use a durable user ID, not an email address that can change.Optionally set
consumer_entity_idif the user is already linked to an Amigo world model entity.Mint or reuse a cached parent token from
client_credentials.Mint an external-user session token, store the refresh token server-side, and return the access token metadata to the browser.
If you use @amigo-ai/platform-sdk v0.66.0+ server-side, the equivalent token resource methods are client.tokens.exchangeClientCredentials(), client.tokens.createExternalUserSession(), and client.tokens.refresh().
Browser Conversation Flow
After your backend returns an external-user token, create a conversation and send turns with that token. For streaming UI, call the turn endpoint with Accept: text/event-stream.
When rendering streamed turns:
Append user messages optimistically when the browser sends them.
Render
tokenevents only as provisional text if you want a typing effect.Commit
messageevents as the completed agent messages in the chat transcript.External-user streams never include
tool_call_started/tool_call_completedevents - tool-call detail is workspace-credential-only, and requestinginclude_tool_calls=truewith an external-user token returns 403. During tool-backed turns, expect a quiet gap aftertoken/thinkingactivity and keep the response UI pending until the finalmessageanddoneevents arrive.On page load, refresh, or reconnect, reload history with
GET /conversations/{id}and renderturns[].role/turns[].text.Do not use
/calls/{id}oragent_transcriptto render web chat history. Those fields are for call-oriented transcript views, not customer web chat.Handle deferred tool results. When a turn returns
background_pending: true, the message you just rendered is only an acknowledgement - a slow tool is still running and the real answer arrives a few seconds later. Keep the turn in a "working…" state and drain the result: send the next user turn, or poll withPOST /conversations/{id}/turns?poll=trueand an empty request body (no message) until you receive a turn withbackground_pending: false. Combiningpoll=truewith a non-empty message returns422; the SDK wrapperpollTurn()handles this for you. Do not treat the acknowledgement as final, and do not subscribe the browser to/events/streamfor this - that is a workspace-wide observer bus, not the per-chat delivery path. See Background (deferred) tool results.
For a production UI:
Keep the external-user
access_tokenin memory when possible.Refresh before
expires_inelapses, with a buffer such as 60 seconds.Refresh by calling your backend with the user's normal app session cookie; do not pass a refresh token from the browser.
Use the latest access token for every conversation request.
If refresh fails with
invalid_grant, discard the current access token, clear the server-side refresh token, and mint a new external-user session after revalidating the app user.
The sessions/connect WebSocket endpoint is for materialized world-model entities and requires an entity_id query parameter. For External Integration web chat, prefer the Conversations REST/SSE endpoints shown above because they derive the correct entity binding from the external-user JWT and enforce the token's service_id and external-user scopes.
Refresh Endpoint in Your Backend
Expose a backend route such as POST /api/amigo/session/refresh. It should verify the app session, load the current server-side refresh token for that app user/session, and call Amigo's refresh grant with workspace_id.
Return the new access token to the browser and replace the stored refresh token after every successful refresh.
JWT, TTL, and Refresh Lifecycle
There are two JWT levels in this flow.
Parent JWT
Customer backend only
grant_type=client_credentials with External Integration client_id and client_secret
expires_in=900 seconds
Delegation token that can mint external-user sessions only.
External-user JWT
Browser
grant_type=external_user_session using the parent JWT
Default 3600 seconds. ttl_seconds may be 900 to 3600.
Runtime token for the signed-in or anonymous web subject.
External-user refresh token
Customer backend only
Returned with the external-user JWT
Absolute session limit is 7 days; idle timeout is the external-user token TTL selected at mint time.
Rotates the browser session without re-exchanging parent credentials.
External-user JWT claims include:
principal_type
Always external_user.
sub
The Amigo consumer_subject_id.
workspace_id
Workspace the token is bound to.
credential_id
Parent External Integration credential ID.
session_id
External-user session ID.
consumer_subject_id
Stable Amigo subject created from your external_subject_key hash.
subject_type
user or anonymous.
consumer_entity_id
Present only when you pass a materialized Amigo entity.
service_id
The only service this token can use.
scopes
Subset of external-user scopes granted for this session.
External-user scopes are restricted to:
conversations:createconversations:turnconversations:read_ownconversations:close_ownconversations:approve_own- enables the subject of care to approve or reject a parked, approval-gated write in their own conversation (the self-approval path)
A mint request with no explicit scope parameter grants all five.
Refresh lifecycle details:
The parent JWT is not refreshed. When it expires, your backend repeats the
client_credentialsexchange.The browser must send refresh requests through your backend using its normal app authentication; it should not hold the Amigo refresh token.
The refresh grant must include
workspace_idfor external-user refresh tokens.Each successful refresh returns a new access token and a new refresh token.
The previous refresh token is rotated away. Replace the server-side stored token immediately.
A short 30-second grace window makes retrying the same refresh request idempotent if the client loses the response.
Reusing an old refresh token after the grace window is treated as token reuse and revokes the token family.
Refresh fails if the External Integration credential is revoked, expired, inactive, or no longer allows the session's
service_id.Refresh fails if the external-user session is revoked, the workspace is inactive, the session has been idle longer than its selected TTL, or the 7-day absolute session lifetime has elapsed.
Operational Guidance
Use one External Integration credential per deployed web application and environment.
Scope each credential to the smallest service allowlist possible.
Rotate External Integration credentials on a schedule and immediately after suspected exposure.
Cache the parent JWT server-side only until
expires_inminus a safety buffer.Handle
429responses and honorRetry-Afteron token and conversation calls.Log token mint and refresh failures by status and error code, but never log token values.
Treat
external_subject_keyas stable identity material. Use an internal immutable ID rather than email or phone number.For linked users, prefer passing
consumer_entity_idso the agent can use the correct world model context.For guest users, use
subject_type=anonymousand a stable anonymous session key.
Related References
Last updated
Was this helpful?

