Once the token route and Provider are wired and the agent can act, four things are worth knowing. None of them are extra setup. They're behaviours the runtime already has, and the shape of a larger integration.
Multiple surfaces
Declare a useSurface on each route, next to the state that route already owns, not one global surface for the whole app. It's active only while its component is mounted, so as the user navigates the active surface swaps automatically and the agent always knows where they are and what's on screen:
// app/tasks/page.tsx
useSurface({ label: "Tasks board", data: { remaining } });
// app/settings/page.tsx
useSurface({ label: "Settings", data: { plan } });You name only the human label; the SDK derives a stable internal id from it, so there's nothing to keep unique across routes. Hang each route's elements and actions off the same component and they come and go with the surface. What should live on a route and what should live in a persistent layout is the first question on Patterns.
Session lifecycle
A voice session winds down on its own when no one's interacting, so an abandoned tab doesn't burn minutes: after about 45 seconds of no interaction it goes idle, and if nothing happens for roughly 120 seconds more, the session ends. This is automatic and not yours to manage; click the button again to start a fresh session. Don't build keep-alive logic around it. (Those are the defaults; the exact thresholds are set on our side, not a prop you configure.)
Minutes & metering
Voice is metered by the minute of live session, per coworker, against a prepaid balance you manage in the portal under Settings → Plan & Billing. You meter nothing in your own code to keep our balance — but if you want to re-bill or cap your own users, every session's minutes reach your backend through the sessions API and signed webhooks; see Usage, quotas & webhooks. There is no per-request accounting to reconcile: idle time past the cut above isn't billed because the session is already over.
- Running low: the account owner gets a low-balance email, and the portal shows a banner on the dashboard. Sessions keep starting.
- At zero: new sessions are refused at the token route. Your
getTokenreceives anout_of_creditreason and the button's ring says Out of credit to the user (every ring status). A session that is already running when the balance reaches zero is ended by the runtime at that point. Nothing in your app breaks; the button simply won't connect until the balance is topped up, which takes effect immediately. - Daily limit: each coworker has a daily session limit set by its plan; past it the ring says Line busy until the day rolls over. Ask us if you need more.
If you want your own copy in front of the user instead of the ring's, branch on the reason your route relays. The Backends page lists every value.
Testing your integration
useAction, useSurface, and useElement are ordinary React hooks, and the handlers you pass them are ordinary functions. Unit-test those handlers directly, with no agent in the loop. The hooks themselves no-op without a Provider, so a hook test only needs to assert what your app does, not the wire. To confirm the agent actually perceives your annotations end to end, use the Watch panel; its Catalogue tab is the ground truth for what's exposed.