Skip to content

Portals: troubleshooting and FAQ ​

Most portal problems are one of four things: the person is not allowed (access list, a deny rule, a network restriction), a feature is switched off (in the portal, or on the agent), a change has not reached the published version of the agent, or a limit was reached. This page goes symptom by symptom.

Enterprise module

Portals are part of the enterprise edition. See Licensing.

Four tools to reach for first ​

ToolWhereAnswers
View asPortal → AccessCould this person or group open the portal, and which agents could they run? With an optional address, to test a network restriction.
The reference codeOn the person's refusal screenPaste it into IAM center → Access check → Look up a denied request to see the action, the people and the rules that decided.
Open as testPortal → … → Open as testWhat does the portal look like and do, right now, even if it is switched off or does not list you?
Change historyPortal → HistoryWho changed which setting, when and from where, with their reason.

Getting in ​

A person sees "No access" ​

The screen says "Your account is not allowed to use this portal" and shows a reference code. Work through, in order:

  1. Is the person on the access list, directly or through a group? Use View as → Person. Selected with nobody chosen means nobody.
  2. Does a deny rule apply? Access → Policies that also apply lists the policies that mention this portal; Check a person runs the access check. Allow rules never add people.
  3. Is a network restriction set, and is their address listed? Run View as with their address, or ask them to open the page from an allowed network. A network refusal normally shows "Not available from here" instead.
  4. Is the account disabled? A disabled account is refused whatever the rules say.
  5. Paste the reference code into IAM center → Access check → Look up a denied request.

See Portal access boundaries for how the engine decides.

An owner or admin cannot open the portal ​

The access list admits only the people on it, and a workspace owner is no exception. Use Open as test (it works even when the portal is switched off), or add yourself to the list. The network restriction applies to test sessions too.

"Portal unavailable" ​

The portal is disabled. Enable it from the … menu. Only people who would otherwise be allowed in see this screen; people who are not allowed see "No access", so a refusal never reveals whether a portal is switched off.

"Portal not found" ​

The address does not match any portal in the person's workspace. Check the address (a changed address breaks old links) and that the person signed in to the right account; Sign in with another account signs them out.

"Not available from here" ​

The network restriction refused their address. Check the ranges, remember IPv4 and IPv6 are separate, and use Check another address with the person's address. If everybody is refused after you turned the restriction on, the address Console sees is probably your reverse proxy's: set TRUSTED_PROXIES to the proxy's address. A request whose address cannot be read is refused when a list is set.

"Temporarily unavailable" ​

Console could not decide whether the person may open the portal (the policy engine did not answer). The portal fails closed instead of guessing, and the person's account is fine. Try again is the right action; if it persists, an operator should check the policy engine and the server logs.

The terms keep coming back ​

Terms are asked again when the text changes (any edit raises the version), when the number of days you set has passed, or when the acceptance was made in a test session (those are kept apart and never count for the real portal). Another portal has its own terms. Accepting twice records once.

In the chat ​

A button is missing ​

MissingCheck
Copy, regenerate, edit, rename, pin, select, rating, export, bellChat features tab: is the switch on? A portal created before the tab existed has none on until you save it. Rating comments need Allow a written comment; tool calls in exports need Include tool calls.
Regenerate on an old answerOnly the newest answer can be regenerated.
Edit on an assistant messageOnly a person's own messages can be edited.
The paperclipFiles are off for that agent in its published version, or no bucket that works is chosen. Fix the agent's Files setting and publish again. Each agent decides for itself.

"Too many requests. Wait a moment and try again." ​

A per-minute limit was reached: messages (including regenerations, edits and answers to requests) default to 30 per minute, uploads to 20. Check Limits; a value you set lower than the platform maximum may be too tight. The Retry-After header says how long to wait.

"There are too many open conversations. Delete an old one first." ​

The portal answered 429 TOO_MANY_CONVERSATIONS ("You have reached the conversation limit for this portal. Delete older conversations to start new ones."), which the chat shows in its own words. The person holds the maximum number of conversations (200 by default, or what you set). They should delete older ones, singly or with Select. If this is too tight, raise Conversations per person (up to the platform maximum).

The message box is blocked, or "Waiting for you" ​

The agent is waiting on an approval or a question. The card is in the conversation; answering it unlocks the box. Depending on the agent's setting, sending a message instead cancels the request. If the person cannot see a card, reload the page: open requests are restored.

An approval is refused ("APPROVAL_NOT_ALLOWED") or never appears ​

  • Refused: the person is not allowed to answer. The person who started the conversation normally can; a deny policy, or a different person trying to answer, is refused with a reference code. Check Access → Policies and see Who can approve tool calls.
  • No card appears: the agent's Approvals & questions setting decides which tools ask. Read-only built-ins are not asked about unless set. Remember the portal runs the published version: settings saved with the draft apply only after you publish again.
  • A card expired: it stayed open past the agent's limit (a day by default). The tool was denied or the request dropped, per the agent's setting.

Starters or the welcome do not show ​

  • A legacy portal shows only its plain welcome text until the Chat features tab is saved.
  • Starters come from the portal only: in Chat features, open the agent and choose Set starters. The agent itself carries none.
  • Welcome variables ({{user.firstName}}) are filled in on the server; an unknown variable is left as written.
  • A welcome picture that is not an upload of this portal is refused when you save the text.

The question is only typed in unless Send the question automatically is on and the link carries &send=1. The agent named in the link must be one the person can open, otherwise the first agent opens.

No generated titles ​

Check Conversation titles: it may be off, set to a model that no longer exists in the project of the portal's agents, or the agent runs outside Console (it has no model of its own; choose A specific model). A title is written after the first answer and never blocks the conversation; a spend limit or a timeout simply means no title. A legacy portal has titles off until its Chat features are saved.

An image is attached but ignored ​

The agent's model cannot see images (the chat warns "This assistant can't see images"). Choose a vision-capable model for the agent, or ask people to attach text.

An announcement is not showing ​

Check its schedule (Scheduled or Ended), that it is among the five active at that moment, and whether the person dismissed it. Editing the text or importance gives it a new identity, so people see it again; editing only the dates does not.

Sound or desktop alerts do nothing ​

The portal only offers them. Browsers play sound only after the person has interacted with the page, and desktop notifications need the person to turn them on in the bell menu and allow them in the browser ("Blocked in the browser" means the site's notification permission is denied).

"PDF (print)" does not download a PDF ​

By design: Console has no PDF renderer in portals. The option opens a printable page; use the browser's print dialog and choose Save as PDF.

Settings that seem not to apply ​

You changedWhy it may not show yet
An agent's Files, Approvals & questions, Experience or usage limitThe portal runs the published version. Publish again.
LimitsThe node that handled your save applies them at once; other nodes within about 15 seconds.
Theme or languageThe page remembers the last theme and language it saw to paint the first screen; it corrects itself once the portal answers. Reload.
TermsAn accepted answer is cached for up to a minute per person; a refused one is always read afresh.
Browser-tab name, icon, descriptionWritten into the page after it loads. Link-preview crawlers cannot sign in, so they never see the preview image or description.
Embed sites or switchTake effect within seconds and end the sessions that depended on them; people sign in again with a fresh token.

Saving and settings errors ​

Message or codeMeaning and fix
Agent "x" has no published version (or is not active) and cannot be addedPublish the agent, and check that it is active.
The address "x" is already used by another portal (KEY_TAKEN)Choose another address.
Choose a language model that exists in the projects of this portal's agentsThe title model must be an llm model in the project of at least one of the portal's agents.
Write the terms people should acceptTerms are on with an empty text.
Pictures in the welcome text must be uploaded to the portalUse Add a picture; remove pictures from other addresses.
At most 5 announcements can be active at onceShorten the overlap of schedules or remove one.
… cannot be above 30 (invalid_limits)The platform maximum is lower than what you entered; an operator sets it with PORTAL_MAX_* variables.
An allow list needs at least one IP range (invalid_networks)Add a range, or choose Any network.
"x" is not a valid IP address or CIDR rangeFix the entry. IPv4 inside IPv6 is not accepted; at most 32 ranges.
Only workspace owners and admins can do this / 403 FORBIDDENPortal administration is for owners and admins only.
402 / "Enterprise license required"The workspace has no active enterprise license.

Import, export and duplicate ​

SymptomCause and fix
"This is not a portal export"The file is not a cognipeer.portal export.
"This file was exported by a newer version and cannot be read"Export again from a Console that is not newer than this one, or update this one.
Agents, groups or people listed as "do not exist here"They are left out. Agents are matched by key and project; an agent that exists but is not published is left out too. Create or publish them and run the check again.
Apply stays disabledThe report has problems (for example a key that is taken or a setting the platform refuses). Fix them in the file or choose another address.
The embed settings did not change after importing over a portalBy design: importing never changes who may sign in through an embed. Change them in the Embed tab.
A copy has no embed secret or terms acceptancesBy design: a copy is disabled, has no secrets and starts the terms at version 1.

Statistics look wrong ​

  • A ≥ in front of a number: some turn reported tokens without a price (cost is a lower bound), or more than 5,000 conversations were active in the window.
  • Zero conversations while people use it: check the window (7, 30 or 90 days, UTC days). Test sessions are excluded on purpose.
  • Active users lower than people who opened it: only people who sent a message count.

FAQ ​

Do people need a Console account? Yes. They sign in with their own account, and the access list names them or their groups. Using a portal gives no dashboard access. To serve people who are signed in elsewhere, use embedding, which signs them in through your application.

Can a portal be public or anonymous? No. Every request needs a signed-in person (or a verified embedded identity), and the policy engine decides each time.

Which version of an agent does a portal run? The published one. Publish again to roll out changes.

Can one agent be in several portals? Yes. Each portal has its own audience, look, limits and statistics.

Who pays for the model calls? The portal's agents run on your workspace's model budget like any other agent run, and usage is recorded for the agent and the person who asked. Regenerating and editing count like new questions; generated titles are small recorded model calls too.

Where are the conversations, and who can read them? In the normal agent conversation store, tagged as portal conversations. They are listed read-only in Studio sessions, so people with read access to the agent can read them; other portal users cannot see someone else's.

Can I limit a portal to my office network? Yes, with the network restriction. Set TRUSTED_PROXIES first if Console runs behind a proxy.

Can people use it on a phone? Yes. The page adapts to small screens.

Can the language follow each person? No. The language is set per portal, so everybody reads the same words. Create another portal (or duplicate one) in the other language.

How do I move a portal from staging to production? Export its settings, import the file in the other workspace (agents, groups and people are matched by key, name and email) and read the dry-run report before you apply. The new portal starts disabled.

How do I see who changed a setting? The portal's History tab, or the console audit log (service portals).

What happens to conversations when I delete a portal? They stay in the console history. The portal's History is removed with it.

Still stuck? ​

Collect the portal address, the time, the person, the reference code from their screen and the relevant History rows, then check the console audit log (service portals) and the IAM center's access check. For embedding problems, see Embedding a portal; for the policy engine, see Authorization (IAM).

Where to go next ​

Embedded portals ​

When something is refused, the person sees a short, generic message with a reference code. This page turns that code into a cause: what each screen means, every refusal code of the token exchange, the exact reasons the audit log records, the limits, the routes, and a short FAQ. For problems with the portal itself (access, networks, terms) see Troubleshooting portals and Troubleshooting authorization.

Enterprise module

Embedding is part of the enterprise portals module — see Embedding a portal.

Diagnose in three steps ​

  1. Read the screen. Match what the person sees to the table below.
  2. Take the reference code. A refused sign-in shows Reference: E-1A2B3C4D. The same code is in the auth:error event your page receives (reference) and in the audit row.
  3. Find the audit row. In Configure → Audit Log, set Service to Portals and Outcome to denied, then open the portal.embed.refused row with that reference. Its metadata holds the exact reason (see Audit reasons) and the issuer, subject, origin and secretId — who, and from where. The caller is never told more than the generic message; only the audit log is.

A refused sign-in with its reference code

What the person sees ​

ScreenMeaningLook at
Embedding is turned off — This portal cannot be shown inside another site.The frame answered its one generic refusal. Any of: wrong tenant or portal key, embedding off, portal disabled, workspace not licensed, your site is not on the list, or the address was opened as a page of its own.Allowed sites; the tenant and portal you pass.
This chat can't be shown here — This page is not allowed to embed the chat.The page that frames the chat is not the origin it declared, or the declared origin is not allowed (origin_not_allowed, ancestor_mismatch).The origin of the iframe; a page behind a different host name or port.
This chat can't be shown here — Open this chat from the page it is embedded in.The address was opened on its own, not in a frame (not_framed).Open the host page, not the frame's address.
Connecting… → Waiting for the page to sign you in…The frame asked for a token and no token arrived for 10 seconds. Your page emits auth:required (reason: "no_token").getToken missing, failing or returning nothing; cp:auth posted to the wrong origin; a CSP blocking your token request.
Sign-in failed + a reference codeThe exchange was refused. The text is generic, except for identity_not_found ("Your account is not set up for this chat. Ask your administrator for access."), network_error and rate_limited.The audit row for the reference code.
Session expired — Reconnect to keep chatting.The session ran out and no fresh token arrived in time.Sessions: your page did not answer cp:token_request.
You have been signed outYour page called logout() (or posted cp:logout).Give the chat a new token with setToken(token).
No access — Your account is not allowed to use this portal. + a referenceThe token was accepted but the policy engine refused the person.The portal's access list, People who arrive through the embed, the network restriction, and the decision in the Authorization center.
Not available from hereThe portal's network restriction refuses the person's address.The portal's network settings; TRUSTED_PROXIES behind a proxy.
Enterprise license requiredThe workspace has no active enterprise licence.Licensing.
Temporarily unavailable + Try againThe policy engine could not decide (HTTP 503). Never read as "no access".Retry; the Authorization center shows each unavailable decision.

Nobody matched and nothing is created

Refusal codes at the exchange ​

POST /api/portal/<key>/embed/session answers a refusal as { "error": { "code", "message", "reference" } }. A 429 also carries Retry-After.

CodeHTTPShown asCausesFix
token_invalid401"The sign-in was refused."Anything wrong before or in the signature: unknown workspace, portal or site; embedding off; no usable key; bad signature; wrong algorithm, issuer or audience; missing or malformed claims; a lifetime above the limit; a token from the future; a token that is too large. Also an unknown site and every other failure before the signature is checked — on purpose, so the endpoint tells a stranger nothing.The audit reason says which. Typical fixes: copy the issuer and audience from the Embed tab, use the secret as UTF-8 text, send kid with two secrets, list the site.
token_expired401"The sign-in token has expired."The token was valid and has run out (60 seconds of clock skew allowed), or its iat is older than the longest lifetime.Mint a fresh token for each call; check your server's clock.
token_replayed401"The sign-in token was already used."That jti was already exchanged — a cached token, a double mount, or a retry that reused it.Mint a new token for every exchange.
identity_not_found403"You are not set up to use this chat."Nobody matches and When nobody matches is Refuse them; the token had no email_verified: true; a member is already linked to a different subject of this issuer; a member account could not be created (no verified address, or the address is taken).Identity mapping: send email_verified: true, or match by identity, or let the portal create accounts.
identity_privileged403"This account cannot sign in this way."The token names a workspace owner or administrator, also through a group.Administrators try a portal with Open as test or the Try it page.
account_disabled403"This account is disabled."The matched account is disabled, or no longer active.Re-enable the account.
quota_exceeded403"The limit of embedded accounts has been reached."The licence's user limit is full, so the account to create does not fit.Free a seat, raise the licence, or stop creating accounts.
rate_limited429"Too many sign-in attempts. Try again in a moment."20 refused attempts a minute from the address; 600 verified tokens a minute for the portal; 20 sessions a minute for the person.Wait Retry-After; stop retrying a failing token in a loop.
exchange_unavailable503"Sign-in is temporarily unavailable. Try again in a moment."The console could not check (a database error). It never lets a token through unchecked.Retry; check the console's health.

Audit reasons: the exact cause ​

Every refused exchange writes portal.embed.refused with a reason. Verifier reasons are shown as code: message.

Before the signature is checked ​

ReasonMeaningFix
rate_limited_ipThe caller's address used up its 20 refused attempts a minute.Wait; fix the cause.
bad_addressThe workspace slug or the portal key is malformed.Check tenant and portal in mount().
no_tokenThe body had no token, an empty one, or one above 8192 characters.Send { "token": "<jwt>" }.
tenant_unknownNo workspace with that slug.Check tenant.
licence_requiredThe workspace has no active enterprise licence.Licensing.
portal_unknownNo portal with that key.Check portal.
embed_disabledThe portal does not allow embedding.Switch Allow embedding on.
portal_disabledThe portal's status is not Enabled.Enable the portal.
rate_limited_portal600 verified tokens a minute for this portal were used.Wait; reduce renewals or session churn.
origin_missingThe origin parameter is absent or not an address.Pass the page's origin.
origin_not_allowedThe declaring site is not exactly one of the Allowed sites.Add the exact origin (scheme, host and port).
no_keyThe portal has nothing to check a signature with: no readable secret (HS256), or no public key or key set (RS256 / ES256).Generate a secret, or paste a key.
malformed: no token, too_largeThe token is empty or above 8192 characters.Do not put data in the token.

In the token ​

ReasonMeaningFix
invalid_assertion: The assertion is not a signed tokenNot three dot-separated parts.Send the compact JWT only.
invalid_assertion: The assertion algorithm is not allowedThe header's alg is not the one the portal pinned (or is none).Sign with the portal's algorithm.
invalid_assertion: The assertion key id is not usablekid is not text, or longer than 256 characters.Use the key id as shown.
invalid_assertion: The assertion signature does not matchThe signature does not match the key.Use the secret as UTF-8 text; check the right secret and the right key.
invalid_assertion: The assertion's "iss" claim is not acceptable …The issuer differs from the portal's.Copy the Issuer; mind slashes and case.
invalid_assertion: The assertion's "aud" claim is not acceptable …The audience is not this portal's (or not the workspace form, when required).Use cognipeer-portal:<key> or cognipeer-portal:<slug>:<key>.
invalid_assertion: The assertion's "<claim>" claim …Another required claim (exp, iat, sub) is missing or malformed, or nbf is in the future.Send all required claims.
invalid_assertion: The assertion was issued in the futureiat is more than 60 seconds ahead.Fix your server clock.
invalid_assertion: The assertion is valid for longer than allowedexp - iat exceeds the portal's longest lifetime.Use 120–300 seconds.
invalid_assertion: The assertion has no usable subjectsub is missing, empty or longer than 512 characters.Send a stable sub.
invalid_assertion: The assertion has no usable idjti is missing, empty or longer than 256 characters.Send a fresh jti.
invalid_assertion: The assertion carries an email that is not an addressemail is present but not an address.Send a valid address or omit it.
invalid_assertion: The assertion context is larger than 4096 bytesctx is too big.Keep ctx under 4 KB.
claim_invalid: ctx must be an objectctx is not a JSON object.Send an object.
expired: The assertion has expired / … is older than allowedPast exp (beyond skew), or iat older than the longest lifetime.Mint per call; check the clock.
replayed: This assertion was already used / jti_replayedThe jti was used before.Mint per call.
untrusted_key: No shared secret is configured for HS256HS256 pinned but no secret exists.Generate a secret.
untrusted_key: The token has no key id and several secrets are configuredTwo secrets, no kid.Send kid.
untrusted_key: The token names a key id that is not configuredkid matches no secret (removed or mistyped).Use an existing key id.
untrusted_key: No public key is configured for RS256 (or ES256)RS256 / ES256 pinned but no PEM or key set.Paste a key or enter a key set address.
untrusted_key: No key of the configured key set could verify this tokenThe key set is unreachable, too large, not JSON, or has no key for the token's kid.Publish the key set on a public https address; set kid.
untrusted_key: The configured public key cannot verify this algorithmThe key's type does not fit the algorithm (an RSA key for ES256).Paste the right kind of key.

After the signature ​

ReasonMeaningFix
identity_unknown_principalNobody matched, or the account to create needs a verified address.See identity_not_found above.
identity_conflictThe member is linked to another subject of this issuer, or the address is already held.Resolve the duplicate identity.
identity_disabled, account_not_activeThe account is disabled.Re-enable it.
identity_seat_limitThe licence's user limit is full.Free a seat.
identity_privilegedAn owner or administrator.Use Open as test.
rate_limited_user20 sessions a minute for this person.Stop the loop.
bind_address_unknownTie a session to the network range is set but the caller's address is unknown.Configure TRUSTED_PROXIES, or change the binding.
internal_errorThe console could not check.Retry; check health.

Other embed events in the same log: portal.embed.exchange (accepted), portal.embed.logout, portal.embed.sessions_revoked, portal.embed.memberships_released, and portal.embed.test (a Try it sign-in).

After sign-in ​

The chat uses the portal API with the session's bearer token. What it can be refused with:

StatusMeaning
401The session is not live (ended, revoked, wrong portal, binding mismatch, site removed, embedding off, account disabled). The chat renews once and retries.
402The workspace has no enterprise licence (requiresEnterprise: true).
403The person has no access (a reference code is shown), the network restriction applies, or the request was cross-site (CROSS_SITE_REQUEST).
404The portal does not exist.
415A request body that is not JSON.
429 RATE_LIMITEDA per-person limit of the chat (below). Retry-After is set.
503The policy engine could not decide. Never read as "no access".

Common problems ​

ProblemCause and fix
Nothing appears on the pageThe script was blocked by your CSP, or portal / tenant is missing (the console shows [CognipeerPortal] "portal" and "tenant" are required). In bubble mode the frame is only created when the bubble is first opened.
The bubble appears but is unstyledA strict style-src blocked the widget's one <style> element — see Hosting, CSP and security.
"Waiting for the page to sign you in…"Your getToken is not defined, throws, or returns nothing; your CSP blocks the token request; with a plain iframe your page does not answer cp:ready.
It works locally and not in productionhttp://localhost is allowed only on development and self-hosted installations; production needs https origins.
It works on www. and not on the bare domainOrigins are exact — list each host name and port separately.
Sign-in fails for some people onlyTheir token lacks email_verified: true; their groups arrived as a string; their account is an owner or admin; they are not on the access list. The audit rows say which.
People are signed out every N minutesSession length is N minutes and your page did not answer cp:token_request, or your token endpoint failed at renewal.
token_replayed on every second loadA token is cached or reused (a CDN cached the endpoint, or the page mounts the widget twice with one token). Return Cache-Control: no-store and mint per call.
rate_limited from many people at onceEveryone shares one address behind a proxy that is not listed in TRUSTED_PROXIES.
The chat works but the agent ignores page toolsThe agent is not switched on for page tools in this portal (Chat features tab → the agent → May use the page's tools) — see Tools in your page.
A removed secret still lets people chatRemoving a secret stops new sign-ins; open sessions run on. Press Sign everyone out.

Use the Try it page ​

In the Embed tab press Open the test page (it opens /dashboard/portals/<id>/embed-test, for workspace owners and administrators only). It plays your application:

  • Create a test token — available for shared-secret signing. Enter a subject, and optionally an email (with Email is verified (email_verified)), a name and comma-separated groups. The console signs a token with the portal's own secret; it lives a few minutes and never exceeds the portal's longest lifetime.
  • Paste a token — for RS256 / ES256, where the console never holds the private key. A pasted token works once; paste a new one before each Start.
  • What the chat reported lists the events the page received and the page tools it answered (with a canned result).

The token is checked exactly like a real one — signature, claims, mapping, access. The only thing replaced is the allowed site check, by a pass that lasts ten minutes and that only owners and administrators can mint. A test sign-in is audited as portal.embed.test.

Limits ​

WhatLimit
Allowed sites20 per portal
Shared secrets2 per portal
Token8192 characters; the portal's Longest token lifetime 30 to 600 s (default 300); clock skew 60 s
sub, iss512 characters
email, name, jti320, 200 and 256 characters
groups100 entries of 200 characters
ctx4 KB of JSON, an object
Group mappings100 per portal; value 200 characters, console group name 120
Session length5 to 1440 minutes (default 60)
Session tokenat most 200 characters; stored as a hash
Page context (context)8 KB of JSON, 6 levels deep
Prefill text8000 characters via the API, 2000 via the address
Tools in your page20 per portal; name 48 characters; description 1000; schema 16 KB; wait 1 to 600 s; result 64 KB
Widget windowwidth 280 to 800, height 400 to 1000 pixels; button text 24 characters
JWKShttps address of 1000 characters; 256 KB; 5 s; cached 10 minutes
PEM8192 characters; RSA at least 2048 bits; EC P-256
Refused exchanges20 a minute per address
Verified exchanges600 a minute per portal
New sessions20 a minute per person
Chat messages (regenerate, edit, answer, continue)30 a minute per person and portal by default; a portal can set a lower value
Uploads20 a minute by default
Open event streams5 per person by default
Conversations kept200 per person by default
Conversations created20 a minute
Request body64 KB (file uploads aside)

The chat limits are the platform's ceilings (operators can change them with PORTAL_MAX_MESSAGES_PER_MINUTE, PORTAL_MAX_UPLOADS_PER_MINUTE, PORTAL_MAX_STREAMS_PER_USER and PORTAL_MAX_CONVERSATIONS_PER_USER); the portal's Limits tab can only set lower values.

Routes ​

Public routes ​

These need no cookie and no API token. The signed token is the credential.

RoutePurpose
GET /api/embed/portal.v1.jsThe widget script (also /api/embed/portal.js).
GET /api-embed/portal/<key>?tenant=<slug>&origin=<host origin>The frame page. Optional: mode, lang, theme, agent, q, send, hist, ec.
POST /api/portal/<key>/embed/session?tenant=<slug>&origin=<host origin>The token exchange.
DELETE /api/portal/<key>/embed/sessionEnd the session whose bearer token makes the request.
GET /api/portal/<key>/embed/widget?tenant=<slug>The bubble's look, for a page on the allowed list.

Exchange a token — { "token": "<jwt>" }, at most 8192 characters:

bash
curl -sS -X POST \
  "https://console.example.com/api/portal/support/embed/session?tenant=acme&origin=https%3A%2F%2Fintranet.example.com" \
  -H "Content-Type: application/json" \
  -d "{\"token\":\"$TOKEN\"}"
json
{
  "sessionToken": "cp_es_665f0c9a1b2c3d4e5f607182.k3J0hTq1m8xQ0y2Nn0oWmQ1Z4hV9Zb7sTt5r2dLwXUc",
  "expiresAt": "2026-10-12T11:04:31.000Z",
  "user": { "name": "Ada Lovelace", "email": "ada@example.com" }
}

A refusal:

json
{ "error": { "code": "identity_not_found", "message": "You are not set up to use this chat.", "reference": "E-1A2B3C4D" } }

This uses the token up and opens a real session — fine for a smoke test, but the widget does all of it for you. The response is Cache-Control: no-store.

End a session — DELETE /api/portal/<key>/embed/session with Authorization: Bearer cp_es_… answers 200 { "success": true } whether or not the token was known.

The bubble's look — GET /api/portal/<key>/embed/widget?tenant=<slug> with an Origin header of an allowed site answers 200 { "widget": { "position": "bottom-right", "launcherIcon": "sparkle", "launcherText": "Ask", "primaryColor": "#6d28d9", "openOnLoad": false, "width": 380, "height": 600 } }, echoes the origin in Access-Control-Allow-Origin, and is cached for a minute. Every other caller gets the same 404 as for a portal that does not exist.

Administration routes ​

These are dashboard routes (the rest of the portal administration API is in the Portals API reference). They authenticate with the browser session of a workspace owner or administrator — not with an API token, and not with a custom role — and answer 401 without a session, 402 without an enterprise licence and 403 to anybody else. Errors are { "error", "code", "field"? }.

RoutePurpose
GET /api/portals/<id>The portal with its embed settings (embed, secrets as key ids only) and embedInfo (tenant, audience, workspaceAudience, consoleUrl, allowInsecureLocalhost).
PATCH /api/portals/<id>Save embed settings (a partial merge) and, optionally, access.embedded.
POST /api/portals/<id>/embed/secretsGenerate a shared secret.
DELETE /api/portals/<id>/embed/secrets/<kid>Remove a shared secret.
GET /api/portals/<id>/embed/status{ "status": { "activeSessions": 3, "externalUsers": 12 } }
POST /api/portals/<id>/embed/sessions/revokeSign everyone out. Answers { "revoked": 3 }.
POST /api/portals/<id>/embed/test-tokenA short-lived test token (shared-secret signing only).
POST /api/portals/<id>/embed/test-frameThe test frame's address and its signed pass.

Save the embed settings

json
{
  "reason": "Allow the intranet",
  "embed": {
    "enabled": true,
    "allowedOrigins": ["https://intranet.example.com"],
    "signing": {
      "algorithms": ["HS256"],
      "issuer": "https://intranet.example.com",
      "maxTokenAgeSeconds": 300,
      "requireWorkspaceAudience": false
    },
    "mapping": {
      "matchBy": ["externalId", "verifiedEmail"],
      "jit": "none",
      "trustEmails": false,
      "groupMap": [{ "idpGroup": "support-agents", "consoleGroupName": "Support agents" }]
    },
    "session": { "ttlMinutes": 60, "bind": "none" },
    "widget": { "position": "bottom-right", "launcherIcon": "sparkle", "launcherText": "Ask", "primaryColor": "#6d28d9", "openOnLoad": false, "width": 380, "height": 600 },
    "clientTools": [
      {
        "name": "get_cart_total",
        "description": "Reads the total of the cart the person has open.",
        "inputSchema": { "type": "object", "properties": { "currency": { "type": "string" } }, "required": ["currency"] },
        "timeoutSeconds": 30
      }
    ]
  },
  "access": { "mode": "selected", "groupIds": ["<group id>"], "userIds": [], "embedded": "access_list" }
}

Only the fields you send change. null clears issuer, keys.pem, keys.jwksUrl, launcherText and primaryColor. hsSecrets in a patch is ignored — secrets are made and removed by their own calls. Turning enabled on requires at least one allowed site, an issuer, and a secret (HS256) or a key (RS256 / ES256). The error codes are invalid_embed, invalid_origin, invalid_signing and invalid_client_tool (HTTP 400) with the failing field.

Generate a secret — 201:

json
{
  "secret": { "kid": "sec_3f9a1c07d2", "createdAt": "2026-10-12T10:04:31.000Z", "secret": "cps_Q1x0v8N3kJ2mZ7sH4tR9yLw5uB6eD1aGpC8nXoVfT2iM" },
  "portal": { "id": "…", "key": "support", "embed": { "…": "…" } }
}

The secret is in this answer and nowhere else, ever. A third secret is refused with 409 secret_limit; removing one that does not exist is 404 secret_not_found; removing the only secret while embedding is on is 409 invalid_signing.

Create a test token — { "subject": "test-user-1", "email": "ada@example.com", "emailVerified": true, "name": "Ada", "groups": ["support-agents"] } answers { "token": "…", "expiresAt": "…" }. Create the test frame — { "origin": "https://console.example.com" } answers { "path": "/api-embed/portal/support?tenant=acme&origin=…&test=…", "signature": "…", "expiresInSeconds": 600 }.

Export, import and copies ​

A portal's settings, the Embed tab included, can be exported to a file, imported into a new portal, or duplicated. The file never holds a shared secret. A new portal made from a file or a duplicate starts with embedding off and gets a new secret when you generate one. Importing over an existing portal never changes who may sign in through the embed — the allowed sites, the signing settings, the identity mapping, the session rules, whether embedding is on, and the access rule for embedded people stay as the portal has them (the dry run lists what the file would have changed); the look of the bubble and the declared tools are applied. Change the rest in the portal's Embed tab.

FAQ ​

Can anonymous visitors use an embedded portal? No. Every embedded session belongs to an account, so access, limits, history and audit work as they do everywhere. Use the portal link, or build on the client API, for anonymous use.

Can I use my identity provider's ID token directly? Usually not: the token needs the audience cognipeer-portal:<key>, a unique jti, and a lifetime of at most 10 minutes. Have your server mint a dedicated token from the signed-in session instead — that is what the examples do.

Can several people share one sub? No. The sub is the person; two people sharing one would share an account and a history.

Do conversations survive a new session? Yes. History belongs to the account the token resolves to, not to the session, so the same person sees their conversations on every device and after every renewal.

What happens to a person I remove from my application? Stop minting tokens for them and press Sign everyone out (or disable the account); their existing sessions otherwise run until their length does. If they were created as a console member or external account, the account stays until you delete it.

Can I embed one portal on several sites? Yes — list up to 20 exact origins. Each site's sessions are tied to the site that opened them; removing a site ends only its sessions.

Does the host page see the conversation? No. The frame is a different origin. The page receives events with ids and sizes, and message text only if you set eventContent: 'text'.

Does it need a build step or a package? No. One script tag, or one iframe.

How do I see who signed in? Configure → Audit Log, service Portals: the portal.embed.exchange rows carry the issuer, subject, site, secret id, and whether the account was created.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub