Are you an LLM? You can read better optimized documentation at /docs/custom-widget/host-page-otp-login.md for this page in Markdown format
OTP login in the widget → your host page reacts
A customer types a one-time passcode in the chat and, a second later, your page shows their account dashboard — as if they had gone through your normal sign-in form. This guide wires that up using the widget's command bus and a signature-verified handshake.
This page is for developers who have already embedded the Perfox widget and run their own OTP / verification logic as MCP tools, and who need the host page to react when verification completes.
Whose URL is whose
Throughout this guide, example.com is your website + backend and example-api.perfox.ai is your Perfox API host. Swap in your own. Anything on example.com is code you write; anything on example-api.perfox.ai is a Perfox endpoint you call.
Two mechanisms combine: the auth-context broker unlocks per-customer data for the agent (so it can answer "what's my balance?" — see Authentication), and the UI-command bus drives your host page. This how-to is about the second. Because login is sensitive, we use the server-driven, signature-verified path.
The three players
Keep these straight and the rest is simple:
| Player | Who it is | Role |
|---|---|---|
| Perfox platform | example-api.perfox.ai | Signs each command, delivers it to the browser, and answers "did I mint this?" |
| Your server | example.com backend + your MCP tools | Verifies the OTP, tells Perfox to drive the command, checks the signature, owns the session. |
| The browser | your page + the widget | Thin: receives the command and echoes it to your server. |
The whole thing is one round-trip: your server initiates → Perfox delivers → the browser echoes back → your server confirms with Perfox → your server completes.
Prerequisites
- The widget is embedded on your page.
- You run the OTP logic yourself as MCP tools — Perfox ships no
send_otp/verify_otp; they're your business logic. - You have a backend on your site (
example.com) that can make outbound HTTPS calls.
Step 1 — Allow the event (once, in Studio)
There's no JSON to paste — it's a checkbox and a text field:
- In Studio, go to Build → Agents and open your agent.
- Click the Web Chat trigger node on the canvas and switch to its Install tab.
- Find the section "Client-side UI commands (co-pilot)".
- Tick "Let the agent drive the host page (navigate, highlight, prefill…)".
- In the "Allowed events" field that appears, type your event name — for this guide,
user_authenticated. (It's a comma-separated list if you have several.) - Publish the agent.
That's the whole setup. Use a custom event name like user_authenticated for "login just completed" — custom names are allowed and your page decides what they do. (The built-in trigger_login means the opposite: ask the page to start its login flow.)
This one toggle gates BOTH producers
The checkbox copy says "the agent", but the same switch and Allowed events list authorize both the agent's drive_ui and your server's POST /events. Turn it on even though you're driving from your server. With the switch off, or an event that isn't listed, the call is rejected with 403.
Step 2 — Handle the event on your page
Register a handler with the widget SDK. Don't act on it directly — echo it to your own backend to verify first (Step 4). Note the endpoint below is yours; name it anything:
js
Perfox('on:ui', 'user_authenticated', async ({ event, payload, meta }) => {
// POST to YOUR OWN backend — this is not a Perfox URL. Name the route whatever you like.
const res = await fetch('https://example.com/api/complete-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, payload, nonce: meta.nonce, sig: meta.sig }),
});
if ((await res.json()).ok) {
if (payload.redirect_after) window.location.assign(payload.redirect_after);
else location.reload();
}
});Where the Perfox URL, conversation_id, and token come from
You don't hard-code them. On every MCP tool call, Perfox adds three headers to the request it sends your MCP tool — read them off that incoming request:
| Header | What it is |
|---|---|
X-Sa-Conversation-Id | The conversation id. |
X-Sa-Auth-Callback | The ready-to-use Perfox URL https://example-api.perfox.ai/conversations/<id>/auth-context. |
X-Sa-Auth-Callback-Token | A fresh 1-hour bearer — use it as Authorization: Bearer …. |
js
// inside YOUR verify_otp MCP tool handler
const conversationId = req.headers['x-sa-conversation-id'];
const authContextUrl = req.headers['x-sa-auth-callback']; // https://example-api.perfox.ai/conversations/<id>/auth-context
const callbackToken = req.headers['x-sa-auth-callback-token'];The same Perfox host (example-api.perfox.ai) serves /events and /events/verify. Driving a command out of band (no live tool call)? Authenticate with a long-lived Perfox API key (sk_…, created on the Developer page) instead of the callback token.
Step 3 — On OTP success, your server tells Perfox
From your MCP tool / backend, make two POSTs to Perfox for that conversation_id, with Authorization: Bearer {X-Sa-Auth-Callback-Token} (or an sk_ key).
(a) Unlock data for the agent — mark the conversation authenticated so the agent can serve per-customer data (Authentication):
http
POST https://example-api.perfox.ai/conversations/{conversation_id}/auth-context
Authorization: Bearer {X-Sa-Auth-Callback-Token}
Content-Type: application/json
{ "authenticated": true, "trust": "verified",
"user_context": { "external_id": "u_123", "name": "Asha" } }authenticated is required and must be a real boolean. If you also send a forward_payload for your tools to receive on later calls, send its expires_at alongside it — an expiry belongs to the credential it ships with, so a rotation that omits it stores an unbounded credential. Later callbacks are partial updates: omitting forward_payload keeps the one you already gave us rather than clearing it.
(b) Tell your page — drive the UI command. handoff_code is a one-time value you generate and remember against the just-logged-in user (it's yours, not a Perfox field):
http
POST https://example-api.perfox.ai/conversations/{conversation_id}/events
Authorization: Bearer {X-Sa-Auth-Callback-Token}
Content-Type: application/json
{ "event": "user_authenticated",
"payload": { "redirect_after": "/account", "handoff_code": "oc_a1b2c3" } }Perfox signs the command, pushes it to the browser, and answers 202 { accepted: true, correlation_id }.
Step 4 — Verify on your server, then log the user in
This is the part worth getting right. Your page handler fires (Step 2), but the browser can't trust the command on its own: the widget shares the window with your page, so it holds no verification secret — meta.verified is always false in the browser (expected, not an error) — and any script on the page could call window.Perfox(...).
So your page echoed the command to your backend. Now your backend asks Perfox to confirm it:
http
POST https://example-api.perfox.ai/conversations/{conversation_id}/events/verify
Authorization: Bearer sk_xxxxxxxxxxxx
Content-Type: application/json
{ "event": "user_authenticated", "nonce": "…",
"payload": { "redirect_after": "/account", "handoff_code": "oc_a1b2c3" },
"sig": "…" }
→ { "valid": true }valid: true means Perfox itself minted this exact command for this conversation, untampered and unexpired. Only then does your backend complete login — it matches the handoff_code to the login it started in Step 3 (or resolves the user by conversation_id), sets your session cookie, and returns { ok: true }; the page (Step 2) redirects to payload.redirect_after.
Worked example — Priya logs in at Acme Diagnostics
Setup: Acme Diagnostics has the Perfox widget on their patient portal homepage. A visitor lands while logged out. The MCP tool verify_otp is registered and connected to the agent.
Action:
- Priya opens the widget and types "I want to check my results." The agent asks for her phone number and sends her an OTP via Acme's SMS provider.
- Priya types the code into the chat. The agent calls Acme's
verify_otptool. - Inside
verify_otp, Acme's server readsX-Sa-Conversation-Idand the callback headers from the incoming request. It confirms the OTP is correct, then makes two POSTs toexample-api.perfox.ai— one to mark the conversation as authenticated, one to fire theuser_authenticatedevent with{ "redirect_after": "/my-results", "handoff_code": "oc_xyz789" }. - Perfox signs the command and delivers it to the browser. The
on:uihandler Acme registered echoes it tohttps://acmediagnostics.com/api/complete-login. - Acme's route calls
/events/verify, gets{ "valid": true }, matcheshandoff_codeto Priya's pending login, sets a session cookie, and returns{ "ok": true }.
Result: Priya's page navigates to /my-results. She sees her lab reports without ever leaving the chat window or clicking a separate "Log in" button.
What just happened: Perfox acted as a secure message relay between the in-chat event and the host page. The signature check on your server is what turned a browser event — which anyone could forge — into a trustworthy signal that this exact conversation just completed a verified login.
Security checklist
- Never put a real session token or secret in
payload— it reaches the browser unsigned. Carry the one-timehandoff_codeyour backend exchanges, or resolve the user byconversation_id. - Always verify the signature on your server before logging anyone in — it's the anti-forgery guard.
- The event must be in Allowed events, or Perfox returns
403. - If signing is ever unavailable, frames arrive unsigned and
/events/verifyreturns503 signing_not_configured— treat that as "not verified" and don't log anyone in. - Perfox endpoint guards: unknown conversation →
404, more than 10 commands a second →429, an optionalidempotency_keydedups retries, an optionaltarget: { tab_id }addresses one tab (the widget reports itstab_idon thereadyevent).
Simpler variant — let the agent drive it
For a low-stakes UI nudge you can skip the Step 3(b) call and have the agent emit the command via the drive_ui builtin right after verify_otp succeeds (you write that instruction in the agent's Persona). It produces the same signed command and the same page handler fires. For real authentication, still verify on your server as in Step 4.
The command bus itself — the envelope, both producers, and the full event registry — is documented in UI-command bus.
You can now accept an OTP inside the chat and use the result to log the visitor into your own session — without a separate login page. From here:
- UI-command bus — learn every event type and producer, and how the signed envelope works end-to-end.
- Authentication — understand the auth-context broker (Step 3a) and how the agent uses a verified identity to answer per-customer questions.