Public links & access gates
Every agent has a public URL at /{tenantSlug}/{agentUrl}. Whether a stranger can
open that URL and start chatting is controlled per-agent by the Allow anonymous
chat toggle on the agent's Setup tab. When it's off, visitors have to clear an
access gate — a shared password, an invite code, or both — before the chat UI loads.
Tenant admins can additionally publish stable "agent link" redirects under
/{tenantSlug}/l/{slug}.
The public agent URL
The Share tab on an agent (AgentShareTab) shows the canonical public URL and a QR
code, backed by GET /api/agents/{id}/share. That route requires an authenticated
session with edit rights on the agent (canEditAgent) — it's for the owner to copy
the link, not a public endpoint. It builds the URL from the request's
x-forwarded-host/host headers (falling back to NEXT_PUBLIC_APP_URL), so the
link reflects whatever host the app is actually served from.
The public page itself (/{tenantSlug}/{agentSlug}) resolves the tenant and agent
server-side and renders one of two things:
allowAnonymous: true— the chat UI (PublicAgentExperience) loads directly.allowAnonymous: false— a gate form renders first (GatedAgentPage), unless the visitor already has a valid access cookie from a previous visit.
The same allowAnonymous check gates the embeddable widget
at /widget/{agentId}.
Access gate mechanisms
When an agent requires access, two independent mechanisms are available and can be used together — the gate form is a single input field that tries the value as a password first, then as an invite code:
Access password
One shared password per agent, managed from the Setup tab (via InviteCodeManager)
and stored as a hash on the agent row.
PUT /api/agents/{id}/access-password— body{ "password": string }(1–200 chars), sets the password. Requires edit rights on the agent.DELETE /api/agents/{id}/access-password— removes it.
Passwords are hashed with a salted, versioned HMAC keyed by the ACCESS_GATE_SECRET
environment variable (see environment variables),
not stored in plaintext. The hash is never included in the agent payload sent to the
browser — the verification route reads it server-side on demand specifically so an
anonymous visitor can't see it in the page's data before they've authenticated.
Invite codes
Multiple, independently revocable codes per agent.
-
GET /api/agents/{id}/invite-codes— list codes for the agent. -
POST /api/agents/{id}/invite-codes— create one. Body:Field Type Notes codestring, optional 3–50 chars. Omit to auto-generate ( VIBE-XXXXXX, using an alphabet that excludesI/O/0/1for readability).expiresAtISO date string or null, optionalNo expiry if omitted/ null.maxUsesnumber ≥ 1 or null, optionalUnlimited if omitted/ null. -
PATCH /api/agents/{id}/invite-codes/{codeId}— revokes the code (soft delete; it's markedrevoked, not removed).
Both list and mutate endpoints require edit rights on the agent. Each code tracks
usedCount and up to 100 redemption records (redeemedAt + an anonymized
per-visitor session id) inline; codes stop working once revoked, past expiresAt,
or once usedCount reaches maxUses.
An invite code can be shared as a bare code for visitors to type in, or baked into
the URL as /{tenantSlug}/{agentUrl}?code={CODE} — the gate form auto-submits a
code query param on load, so the link itself grants access with no typing.
Verifying access
POST /api/public/agents/{agentId}/verify-access is the public endpoint the gate
form calls. Body: { "value": string } (1–200 chars) — the password or invite code
the visitor entered. It:
- Returns
400if the agent actually allows anonymous access (nothing to verify). - Checks
valueagainst the access password hash, if one is set. - Falls back to redeeming
valueas an invite code, scoped to the agent's tenant. - On success, sets an HMAC-signed,
httpOnly, session-lifetime cookie (va_access_{agentId}, nomaxAge, so it clears when the browser closes) and returns{ "ok": true }.
Invite-code failures come back as 403 with a code field so the UI can show a
specific reason:
code | Meaning |
|---|---|
invalid | Value didn't match the password or any invite code. |
revoked | Code exists but was revoked. |
expired | Past its expiresAt. |
max_uses_reached | Hit its maxUses limit. |
Requests from an embedded widget send an x-embed: true header, which makes the
access cookie SameSite=None instead of Lax so it survives being set from inside
a cross-origin iframe.
Invite-code redemptions are tracked per anonymous visitor using a separate
30-day va_ext session cookie, not by account — there's no login involved in
clearing a password/invite-code gate, only in editing the agent itself.
Agent links (stable redirect URLs)
Beyond an agent's own URL, a tenant admin can publish additional short links under
/{tenantSlug}/l/{slug} from Settings → Agent Links. An agent link stores a
slug → agent mapping that can be repointed to a different agent later without
changing the published URL or QR code — useful for things like a printed front-desk
QR code you want to be able to redirect independently of which agent it currently
targets.
- Gated by the tenant feature flag
AGENT_LINKS(on by default; toggle it from the tenant Features settings). The list, create, and update endpoints check this flag and 403 if it's off (fetching or deleting a link by id doesn't re-check it); the public/l/{slug}page checks it too and 404s if it's off. - Managed via
GET/POST /api/tenants/{tenantId}/agent-linksandPATCH/DELETE /api/tenants/{tenantId}/agent-links/{linkId}, all requiring tenant-admin rights (requireTenantAdmin). Fields:slug,agentId,name, optionaldescription, andisActive(deactivating a link 404s the public page without deleting it). - The slug is fixed at creation — editing a link can change which agent it points to, its name, and its description, but not the slug itself, so previously distributed URLs/QR codes keep working.
Agent links don't render the access gate form
The /{tenantSlug}/l/{slug} page checks allowAnonymous on the connected
agent, but if it's false it does not show the password/invite-code form — it
renders a static "this agent requires an invitation... contact the owner"
message instead. Only the agent's own URL (/{tenantSlug}/{agentUrl}) and the
embeddable widget render the actual gate form. If you need an invite-code or
password flow, share the agent's direct URL, not an agent link.
Choosing a setup
- Fully public (kiosk, marketing page): leave Allow anonymous chat on.
- Semi-private, self-serve (e.g. an internal tool shared with a known group): turn anonymous chat off and set an access password everyone can share.
- Per-recipient tracking or expiry (e.g. a link sent to specific customers, or
one that should stop working after a launch event): use invite codes with
expiresAtand/ormaxUsesinstead of, or alongside, a password. - A stable, repointable URL for print/signage: create an agent link, and make sure the agent it targets allows anonymous access — since the link page itself can't collect a password or code.