Authentication
Knowing who is calling
Authentication is how your Parlant server knows who is calling it. Every request carries a credential. Parlant checks it, works out what kind of caller sent it, and only then decides what that caller may do. You set it up once on the server, and your backend hands each signed-in user a small token. Parlant enforces the rest, so a customer can only ever reach their own conversations.
- The four kinds of caller and what each one may do
- How to turn authentication on in code or through the environment
- How to wire Parlant to the login system you already have
- What 401, 403, and 429 mean when a request is refused
A Parlant server that is reachable from a browser needs to know who is calling. Parlant answers that with one model: every request carries a credential, Parlant verifies it and works out what kind of caller it belongs to, and then decides separately what that caller may do.
You configure it once on the server. Your backend mints a small token for signed-in users. Everything else is enforced for you.
Until you configure it, the server runs in development mode and allows every request without credentials. That is right for building on your laptop and wrong for anything with a public address, so this page is part of going live.
The four kinds of caller
Every request resolves to exactly one principal, and the principal decides what the request may touch.
| Caller | Proves it with | May do |
|---|---|---|
| Anonymous | Nothing | Read agent info and start a guest conversation. |
| Guest | A guest token Parlant minted | Read and post to the sessions that guest created. |
| Customer | A JWT, the signed login token your backend created for them | Read and post to that customer's own sessions. |
| Admin | A static API key | Everything: customers, agents, any session, diagnostics. |
Two consequences shape how you build on this. First, Parlant is not an identity provider. It never logs anyone in; it verifies a token that your system issued after your own login check. Second, ownership is enforced on every session and event endpoint. A customer who knows another customer's session id gets a 403, not the conversation.
Guests get a stable identity without an account. When a browser with no credentials creates a session, Parlant creates it under a shared guest customer and returns a signed guest token in the X-Parlant-Guest-Token response header. The browser stores it and sends it as a bearer token from then on.
Each guest token carries a private instance id, so one guest can never read another guest's conversation. Guest tokens last a week. A guest who keeps coming back has theirs renewed: once a token is past half its lifetime, any response hands back a fresh one for the same guest in the same X-Parlant-Guest-Token header, and the browser stores it in place of the old one. Renewal stops when the guest's identity reaches its maximum age, 28 days by default, counted from their first visit. After that, or after a week away, the token is rejected with 401 and the browser starts over as a new guest.
The admin key is the credential for your backend and operator tools. It bypasses ownership checks and rate limits. It is also the only credential accepted on WebSocket endpoints such as /logs, where it is passed as a ?token= query parameter because browsers cannot set WebSocket headers. Treat it like a database password and never ship it to a browser.
Parlant keeps no token state. Customer tokens are issued, refreshed, and expired by your systems. An expired token is rejected with 401, and the next request with a fresh one succeeds. Revocation lives in your identity system, through short lifetimes or key rotation, not in a Parlant block list.
A customer token must also point at a real customer. A persona is a test customer that stands in for a real one. If a token's sub names a persona, Parlant rejects it with 401. This stops a real user whose id matches a persona from attaching their live session to a test customer.
Authorization explains how personas are reached instead.
Turn it on
AuthConfig is the object that holds every credential the server verifies: the admin key, the secret for customer tokens, and the secret for guest tokens. Pass it as the server's auth:
import parlant.sdk as p
async with p.Server(
auth=p.AuthConfig(
admin_api_key="a-long-random-admin-key", # for your backend and operator tools
customer_jwt_secret="the-secret-your-backend-signs-with", # verifies the tokens your login issues
guest_token_secret="another-long-random-secret", # signs the tokens anonymous visitors get
),
) as server:
...
Each field in full:
| Field | What it does |
|---|---|
admin_api_key | The static admin credential. Required. |
customer_jwt_secret | Shared secret for verifying customer JWTs your backend signs with HS256, HS384, or HS512. |
customer_jwks_url | The alternative: your identity provider's JWKS endpoint, for RS256 or ES256 tokens it already issues. Mutually exclusive with the shared secret. |
guest_token_secret | Signs guest tokens. If unset, Parlant generates a temporary one at startup, so a restart logs every guest out. Set it. |
guest_identity_max_age_seconds | How long a returning guest keeps one identity, and with it their conversations, through token renewals. Four weeks by default. |
auto_provision_customers | If true, a valid customer token for an unknown customer creates that customer on first contact. Off by default. |
Leaving both customer_jwt_secret and customer_jwks_url unset disables customer authentication and leaves guests and admin only. AuthConfig also accepts an expected audience and issuer for customer tokens, a switch that requires every customer token to carry an expiry, and a set of trusted proxy addresses for rate limiting behind a load balancer.
In production, set the same configuration through the environment instead of code:
export PARLANT_ENV=production
export PARLANT_ADMIN_API_KEY="a-long-random-admin-key"
export PARLANT_CUSTOMER_JWT_SECRET="the-secret-your-backend-signs-with"
export PARLANT_GUEST_TOKEN_SECRET="another-long-random-secret"
export PARLANT_GUEST_IDENTITY_MAX_AGE_DAYS=28 # optional; this is the default
With PARLANT_ENV=production, the server refuses to start if the admin key is missing. A misconfigured deployment fails fast instead of running open.
Wire it to your login
The pattern is the same whatever your stack. Your backend already knows who is logged in. It vouches for that identity by minting a short-lived JWT that Parlant verifies. Three steps, and your coding agent can do each of them.
1. Make the customer known. A JWT carries named fields called claims. Its sub claim is the subject: the id of the user it was issued to. Parlant reads that claim as the customer id, so it needs a matching customer record. There are three ways to provide one.
Create customers yourself with your own ids, from a signup webhook or a one-time backfill:
await server.create_customer(id="user_1234", name="Dana Cohen")
Or turn on auto_provision_customers, and the first valid token for an unknown sub creates the customer, using the token's name claim if there is one. Or plug your user database in as the customer store, so Parlant reads profiles live and nothing is copied; the Customers page shows how.
2. Mint the token in your backend. After your login flow establishes the user, sign a short-lived JWT with the shared secret, the same value you gave customer_jwt_secret.
The token needs the customer id in sub and an expiry in exp. A name claim is optional and is used when Parlant creates the customer for you. Any JWT library works; this is PyJWT:
import time
import jwt
def make_parlant_token(user_id: str, display_name: str) -> str:
return jwt.encode(
{
"sub": user_id, # the Parlant customer id
"name": display_name, # used only when auto-provisioning
"exp": int(time.time()) + 15 * 60, # short-lived; the frontend refreshes it
},
PARLANT_CUSTOMER_JWT_SECRET,
algorithm="HS256",
)
Expose it through a small endpoint the frontend can call, such as GET /parlant-token, protected by your normal session cookie. When the token expires, the frontend asks you for a new one.
If you already run an identity provider that issues RS256 or ES256 tokens, skip the minting. Point customer_jwks_url at its JWKS endpoint and pass its access token, as long as the sub claim maps to a Parlant customer id.
3. Call Parlant from the browser with the token as a bearer credential:
const session = await fetch(`${PARLANT_URL}/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${parlantToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ agent_id: AGENT_ID }),
}).then((r) => r.json());
Note that the body carries no customer_id. Parlant takes the customer from the token, and a customer_id in the body that contradicts it is rejected with 403. The official chat widget does all of this for you when you give it a function that fetches the token; see Frontend integration.
For anonymous visitors, no backend work is needed. The browser creates a session with no credentials, stores the guest token from the response header, and sends it on later requests. Whenever a later response carries that header too, it's a renewal for the same guest: store it in place of the old one. If you serve the browser from a different origin, the default CORS setup already exposes that header; keep it exposed if you replace the CORS configuration.
When a request is refused
| Status | Meaning |
|---|---|
| 401 | The credential failed verification: bad signature, expired, an unknown customer, or a customer that is a persona. |
| 403 | Valid identity, but this operation or session is not allowed for it. A request with no credentials also gets 403 for an existing session, so a browser that has lost its guest token should start a new session rather than retry. See Authorization. |
| 429 | Rate limit exceeded. The default limiter is per client IP, and admins are exempt. |
Replace the authenticator
Use a custom authenticator when the built-in token settings cannot verify your existing credentials. A common case is an opaque token that must be checked with your identity provider.
Authentication is one of four ports in the policy pipeline. A port is a component with a fixed interface that you can swap while keeping everything else; Authorization names the other three.
An authenticator implements resolve, which reads the request and returns a principal, the object Parlant uses to represent the caller. It also implements resolve_websocket, which does the same for a WebSocket connection.
The one below accepts an opaque token from your single sign-on and validates it by introspection, meaning it asks the SSO server whether the token is valid and whose it is:
import parlant.sdk as p
class MySSOAuthenticator(p.Authenticator):
async def resolve(self, request):
token = extract_bearer(request)
if token is None:
return p.AnonymousPrincipal() # no credential: an anonymous caller
claims = await my_sso.introspect(token)
return p.CustomerPrincipal(customer_id=claims["uid"], attributes=claims) # a customer, plus what SSO knows
async def resolve_websocket(self, websocket):
...
async with p.Server(
auth=p.CompositeAuthorizationPolicy(authenticator=MySSOAuthenticator()),
) as server:
...
This is an adapter outline. extract_bearer and my_sso.introspect belong to your application. Ask your coding agent to reject expired or inactive tokens before creating a principal, and to implement resolve_websocket if your integration uses WebSockets.
Whatever your authenticator learns about the caller beyond their identity, such as roles or an organization id, belongs in Principal.attributes. The other three ports, which decide what a caller may do, are covered in Authorization.