On this page
The widget has no event emitter and no callbacks (there is no onOpen, onQuote or similar). The integration surface is: start it (init), control it (WidgetHandle), or skip the built-in interface and build your own on the headless client (createClient), where you see every call and result yourself.
init(options)#
function init(options: InitOptions): WidgetHandleMounts the widget (a launcher button and the chat panel in a closed Shadow DOM) and returns a handle. Calling it again returns the existing instance (there is one widget per page). It throws Error('Recent Visits: invalid key') if the key is malformed, and apiBase must use https (http is allowed only on localhost) or similar for a bad apiBase. Call it in the browser only; the server is contacted for the look at load and for a session when the visitor opens the chat.
InitOptions is every option of the configuration reference plus key, apiBase, storage, persist and getChallengeToken.
import { init } from '@recent-visits/widget'
const widget = init({ key: 'rvk_AAAAAAAAAAAAAAAAAAAAAAAA', launcher: { label: 'Ask us' } })WidgetHandle#
| Method | Signature | Behaviour |
|---|---|---|
open | () => void | Opens the chat (connecting first if needed). |
close | () => void | Closes it and returns focus to the launcher. |
update | (options: AppearanceOptions) => void | Changes appearance or behaviour while running, with no reload. Groups merge one level deep, so update({ theme: { radius: 8 } }) keeps your other theme values. Values are validated like the initial ones. |
mute | () => void | Never show invitations again (until unmute). |
unmute | () => void | Allow invitations again. |
destroy | () => void | Removes the widget, stops all timers, forgets the session token in this tab (it does not call the server) and clears the saved chat. After this init creates a new widget. |
import { init } from '@recent-visits/widget'
const widget = init({ key: 'rvk_AAAAAAAAAAAAAAAAAAAAAAAA' })
widget.update({ appearance: 'dark', theme: { radius: 8 } })
document.querySelector('#chat-button')?.addEventListener('click', () => widget.open())
// later, for example when your single-page app unmounts:
widget.destroy()From a script tag#
The auto-start tag does not hand you the handle, but because init returns the existing instance you can fetch it:
<script
src="https://recent-visit.com/widget/1/widget.js"
data-key="rvk_AAAAAAAAAAAAAAAAAAAAAAAA"
async
></script>
<script>
window.addEventListener('load', function () {
var widget = window.RecentVisits.init({ key: 'rvk_AAAAAAAAAAAAAAAAAAAAAAAA' })
widget.open()
})
</script>createClient(options) (headless)#
function createClient(options: ClientOptions): ClientClientOptions: key (required), apiBase, storage ('memory' by default for the client; 'session' keeps the session token in sessionStorage), timeoutMs (default 30,000; chat streams get 120,000), fetch (inject your own) and getChallengeToken. It never uses cookies (credentials: 'omit') or localStorage. The client creates and renews its session by itself.
| Method | Returns | Description |
|---|---|---|
session() | Promise<SessionInfo> | Creates (or returns the current) session: agency name, features, greeting, branding, environment, takeover availability. |
config() | Promise<PublicConfig> | The public look and features for your key, without a session. |
tours(params?) | Promise<Tour[]> | Your published tours. params: limit, destination, minDays, maxDays, difficulty (easy, moderate, challenging, strenuous). |
chat(input, onEvent?) | Promise<ChatResult> | Sends a message and streams the reply (see below). |
handoff(conversationId) | Promise<HandoffResult> | Asks for a person. |
messages(conversationId, after?, version?) | Promise<MessagesResult> | Polls for agency messages (cheap when nothing changed). |
quote(body) | Promise<void> | Sends a quote request (see QuoteInput). |
end() | Promise<void> | Ends the session on the server and forgets the token. |
dispose() | void | Forgets the token locally and stops renewal (no network). |
Chat events#
chat takes { message, conversationId?, visitorName?, tourSlug? } and calls onEvent with a ChatEvent:
| Event | Fields |
|---|---|
start | conversationId, replyId |
text | delta: the next piece of text |
finish | metadata: tours, toursApproximate, notice ('contact_redacted'), saved |
error | message |
It resolves to { status: 'streamed', conversationId }, { status: 'closed' } (the agency closed the conversation) or { status: 'queued', conversationId } (a person has taken over; the message was queued for them). Pass the returned conversationId on the next call to continue the conversation.
import { createClient, WidgetApiError } from '@recent-visits/widget'
const client = createClient({ key: 'rvk_AAAAAAAAAAAAAAAAAAAAAAAA', storage: 'session' })
export async function ask(message: string, conversationId?: string) {
let reply = ''
const result = await client.chat({ message, conversationId }, (event) => {
if (event.type === 'text') reply += event.delta
if (event.type === 'error') console.error(event.message)
})
return { reply, conversationId: result.status === 'closed' ? undefined : result.conversationId }
}
export async function loadTours() {
try {
return await client.tours({ limit: 6, difficulty: 'easy' })
} catch (error) {
if (error instanceof WidgetApiError && error.code === 'rate_limited') return []
throw error
}
}Quote requests#
QuoteInput requires name, email, message and consent: true; optional tourSlug (your tour), conversationId, phone, country, travelDate, durationDays, travelers, budget, currency. A website field is a bot trap: leave it empty. Only send consent: true after the visitor has agreed to share their details with the agency. See Examples.
Errors: WidgetApiError#
Every failed call throws WidgetApiError with code, status (HTTP status, 0 for network) and sometimes retryAfter (seconds). It never contains the session token.
code | Meaning |
|---|---|
invalid_request | The input was malformed (400). |
unauthorized | The session is missing or no longer valid (the client starts a new session once, then fails). |
session_expired | The session timed out; the client renews automatically. |
forbidden | The key or domain is not accepted, the feature is off, or the integration is inactive (403). The reason is deliberately not detailed. |
challenge_required | Recent Visits asks for a human check before starting a session (403). The SDK runs it and retries once by itself; you only see this code if you replaced getChallengeToken with a function that returned an unusable token twice. |
challenge_failed | The human check could not be completed (blocked, timed out or cancelled), so no session was started. A later try can succeed. |
not_found | The item does not exist for your agency. |
payload_too_large | The request was too large. |
rate_limited | Too many requests (429). retryAfter is the real number of seconds until that limit resets, not the length of its window. |
unavailable | The service or the agency is temporarily unavailable. |
internal | An unexpected server error. |
network | The request never reached the server (offline, blocked, timed out). |
Helpers and constants#
resolveConfig(input)returns{ config, warnings }: the validated, complete configuration and the list of console warnings for an options object. Useful for checking your options without starting the widget.linkify(text)andsafeHref(url): the helpers the widget uses to make links from text (onlyhttpsand Recent Visits paths become links).DEFAULT_API_BASE:https://recent-visit.com.- Exported types include
InitOptions,WidgetHandle,WidgetOptions,AppearanceOptions,ThemeInput,NotificationsInput,Client,ClientOptions,ErrorCode,SessionInfo,PublicConfig,Tour,TourQuery,ChatInput,ChatEvent,ChatResult,HandoffResult,MessagesResult,AgencyMessageandQuoteInput.
Data shapes#
| Shape | Fields |
|---|---|
| Session info | token, expiresAt, renewAfter, agency (name, slug), environment (production or test), features (chat, tours, quote), greeting, branding, takeover ({ available } or null). |
| Public config | agency, environment, features, branding, takeover: the same without a token. |
| Tour | slug, title, summary, durationDays, priceFrom, currency, difficulty, destinations, imageUrl, imageAlt, url; in chat also optional reasons, pinned, approximate. |
| Handoff result | { available: true, status: 'waiting' | 'live' } or { available: false, reason: 'closed' | 'disabled' | 'outside_hours' }. |
| Messages result | mode (ai, human, closed), handoff (none, waiting, live, expired), agencyAvailable, optional version, messages (each id, role agent or system, content, createdAt, sender). |
A session lasts 30 minutes of inactivity and at most 12 hours in total; the client renews it before it expires. tours limits: limit 1 to 24 (default 12), minDays and maxDays 1 to 60.
Server validation of a quote request#
The server refuses a quote request that fails these rules with invalid_request: name 2 to 120 characters, a valid email, message 10 to 3,000 characters, phone up to 40, country up to 80, travelDate as YYYY-MM-DD, durationDays 1 to 120, travelers 1 to 50, currency three letters (default USD), consent exactly true, and no unknown fields. A successful request returns HTTP 201. The same email asking about the same tour again within a day is refused with rate_limited.
HTTP endpoints#
The widget and client talk to https://recent-visit.com/api/widget/v1 (a different apiBase for staging). These endpoints exist for the SDK; use the SDK rather than calling them yourself.
| Endpoint | Purpose |
|---|---|
POST /config | The public look and features for a key (body { publicKey }). |
POST /session, DELETE /session, POST /session/refresh | Start, end and renew a session (DELETE ends it at once). |
GET /tours | Your published tours. |
POST /chat | Send a message; streams the reply. |
POST /chat/handoff, GET /chat/messages | Ask for a person; poll for agency messages. |
POST /quote | Send a quote request. |
Calls other than config and starting a session use Authorization: Bearer <session token>. There is no cookie authentication. Within version 1 the service only adds optional fields and new error codes; see Versioning.
Last checked against the product:
Related articles
- SDK configuration reference · Every option the Recent Visits widget accepts: type, default, limits and an example. Invalid values are replaced by safe defaults.
- SDK examples · Copy-and-paste integration examples for the Recent Visits widget: minimal, agency configuration, themed, Next.js, React with the package, and a headless quote request.
- SDK versioning · The widget's current version, how compatibility works within API version 1, how to upgrade, and what does not exist yet (changelog, published package).
Still stuck? Contact us and tell us what you tried.