Skip to main content

Before you start

Three things must be true, and all three are on PolyAI’s side. The integrator needs: a key ID and a secret from PolyAI, a backend endpoint that can sign a JWT, and a few lines of JavaScript on the page.

Step 1 - Register the callback

onContextRequired takes a handler receiving { sessionId, contextId } and returning a signed JWT string, or a promise of one.
The handler is read at request time, not snapshotted at init, so registering inside onReady is comfortably in time. The effective deadline is SESSION_START.
Return-value traps. Only a non-empty string is treated as a token. undefined , null , '' , a number, or an object like { token: '...' } are all treated as declines — you get a console.warn and the conversation proceeds without context.
Other behaviours worth knowing:
  • One handler slot, not a list. Registering twice silently overwrites - last call wins, no warning. There is no way to unregister; WebchatAPI.off() does not apply here.
  • destroy() clears the handler. Re-initialising the widget means re-registering.
  • contextId can be undefined. The SDK reads it defensively off the payload. Signing sub = undefined will fail the vault’s binding check - guard for it.
  • A synchronous throw is caught. The handler is invoked inside a promise chain, so a raw throw becomes a decline rather than stranding the widget.

Step 2 - Sign the token on your backend

HS256, signed with the project’s key. Never in the browser.

The trap that costs the most time.

The secret is used as a UTF-8 string, not decoded from hex — pass it to your JWT library exactly as issued. If you decode it to bytes first (Buffer.from(secret, 'hex') or equivalent), every token you mint will fail signature verification with an opaque 401.

Token contract

The token is signed, not encrypted - ctx is readable by anyone who sees the token. It proves origin, not confidentiality.

Step 3 - Refresh mid-conversation

Re-runs the same callback against the live conversation and submits a fresh token. The vault set is idempotent - full replacement, last write wins - and the conversation is not interrupted.
It returns undefined, not a promise. Fire-and-forget, with no way to await it or learn the outcome. It silently no-ops in three separate places: no handler registered, no session or no context_id yet, or a context request already in flight. If your handler isn’t invoked, you cannot tell which of the three happened from the host page.

The timing budget

Three timeouts interact, and the tightest one is the one that matters.
A signing backend slower than 4 seconds always declines, no matter how long the token’s TTL is. Budget your endpoint against the 4s cap, not the 30s TTL.

Failure behaviour

Verified context is best-effort and fails open. The conversation always starts.
No success or failure signal ever reaches the host page. There is no onContextSet / onContextFailedcallback — the full context-related public surface is onContextRequired and refreshVerifiedContext. If the vault rejects your token, the widget logs it, resolves internally as failed, and the agent joins without context. Your page is not told. Budget for widget-console and network-tab debugging, and design the agent to degrade gracefully.

Verified Context Injection

What Verified Context Injection is, when to use it, and how it works end to end.
Last modified on August 21, 2026