Skip to main content
Version: v1

Lifecycle

The session lifecycle is identical to the web widget except reconnect: on mobile a dropped socket comes back as a fresh guest session, never a resume — the server binds a mobile session to the merchant, not the individual shopper, so per-subject continuity is not currently scheduled. Everything else — minimize, teardown, close codes — matches web. The single most important behavior to get right:

minimize ≠ close

minimize() is UI-only — the session and WebSocket stay alive. endSession() ends the session (sends {type:"end"}) and tears everything down. In your custom UI, wire your "collapse" control to minimize() and your "X" to endSession().

The lifecycle table

You doWhat happens on the wireBackend
startVoiceSession() / startTextSession()issue token → open WS → handshake (ready)readysession_state
minimize()nothing — UI-only collapse; WS + session stay alive, no end sentsession stays active
restore()nothing — un-collapse
app backgroundedWS may dropsocket drop noted
→ return (any time, even < 60s)reconnect → fresh guest sessionno resume, no recapnew session, bound to the merchant not the shopper
endSession()sends {type:"end", reason:"user_closed"} → real end + teardownend_session(reason="client_end")
bot ends / fatal closeSDK emits ended / a failed state, tears downend_session(...)
screen unmount / destroy()hard teardown (no end sent)client_disconnect

Backgrounding → fresh guest session

When the app is backgrounded the OS may drop the socket. On the mobile SDK, returning to the foreground always starts a fresh guest session — there is no resume and no recap, even if you reconnect within 60 seconds with the same conversation_id. The server binds a mobile session to the merchant, not the individual shopper, so per-subject continuity is not currently scheduled. The SDK still sends your current app context on the new session (see Context enrichment); only the prior conversation is not carried over.

The 60s Redis reattach window (reattach_session → resume, DB recap after 60s) is a web-widget behavior — it does not apply to the mobile SDK.

This is not an end. You never send end on backgrounding.

foreground ──tap launcher──▶ session live (WS open)

├─ minimize() ──────────────────────────────▶ session live (UI collapsed, WS open, NO end)

├─ app backgrounded ──▶ WS drops ──▶ return (any time) ─▶ fresh guest session (no resume, no recap)

└─ endSession() ─────────────────────────────▶ {type:"end"} ─▶ real end + teardown

Reconnect policy

The transport reconnects on transient drops with exponential backoff + jitter, capped at ≤3 attempts, then settles into a graceful state without crashing. A heartbeat ping runs about every 25s; if no pong is seen in ~30s the SDK closes and reconnects.

Fatal close codes are NOT retried — they mean the session cannot continue:

CodeMeaning
4400malformed request
4401invalid/expired token (or key blocked by the billing kill-switch)
4403app_id not allowed
4503voice provider down
1008session limit
1011server crash

All other drops are reconnected (within the ≤3 cap). See the wire protocol §8.

Observing state

const session = await client.startVoiceSession({ lang: 'uk' });
session.on('status', (s) => console.log(s.status)); // idle | connecting | … | ended
session.on('ended', (e) => console.log('ended:', e.reason));
session.on('error', (e) => console.warn(e.code, e.message));

session.minimize(); // UI-only — session stays alive
session.restore();
await session.endSession(); // the X — real end
session.destroy(); // hard teardown on unmount (no end sent)

Parity note. The web widget does not yet wire an explicit "X to end" button (the backend frame exists but is unwired on web today). The mobile SDKs expose it as endSession().