Skip to content

⚠️ Recent Visits is in beta. All listings and data shown are for demonstration purposes only and do not represent real or verified information. Tour listings are based on content from partner agencies’ websites and have not yet been confirmed by the agencies.

Recent Visits

SDK API reference

The public surface of the Recent Visits widget package: init, the widget handle, the headless client, errors, types and the HTTP endpoints they use.

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)#

ts
function init(options: InitOptions): WidgetHandle

Mounts 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.

ts
import { init } from '@recent-visits/widget'

const widget = init({ key: 'rvk_AAAAAAAAAAAAAAAAAAAAAAAA', launcher: { label: 'Ask us' } })

WidgetHandle#

MethodSignatureBehaviour
open() => voidOpens the chat (connecting first if needed).
close() => voidCloses it and returns focus to the launcher.
update(options: AppearanceOptions) => voidChanges 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() => voidNever show invitations again (until unmute).
unmute() => voidAllow invitations again.
destroy() => voidRemoves 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.
ts
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:

html
<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)#

ts
function createClient(options: ClientOptions): Client

ClientOptions: 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.

MethodReturnsDescription
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()voidForgets the token locally and stops renewal (no network).

Chat events#

chat takes { message, conversationId?, visitorName?, tourSlug? } and calls onEvent with a ChatEvent:

EventFields
startconversationId, replyId
textdelta: the next piece of text
finishmetadata: tours, toursApproximate, notice ('contact_redacted'), saved
errormessage

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.

ts
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.

codeMeaning
invalid_requestThe input was malformed (400).
unauthorizedThe session is missing or no longer valid (the client starts a new session once, then fails).
session_expiredThe session timed out; the client renews automatically.
forbiddenThe key or domain is not accepted, the feature is off, or the integration is inactive (403). The reason is deliberately not detailed.
challenge_requiredRecent 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_failedThe human check could not be completed (blocked, timed out or cancelled), so no session was started. A later try can succeed.
not_foundThe item does not exist for your agency.
payload_too_largeThe request was too large.
rate_limitedToo many requests (429). retryAfter is the real number of seconds until that limit resets, not the length of its window.
unavailableThe service or the agency is temporarily unavailable.
internalAn unexpected server error.
networkThe 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) and safeHref(url): the helpers the widget uses to make links from text (only https and 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, AgencyMessage and QuoteInput.

Data shapes#

ShapeFields
Session infotoken, expiresAt, renewAfter, agency (name, slug), environment (production or test), features (chat, tours, quote), greeting, branding, takeover ({ available } or null).
Public configagency, environment, features, branding, takeover: the same without a token.
Tourslug, 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 resultmode (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.

EndpointPurpose
POST /configThe public look and features for a key (body { publicKey }).
POST /session, DELETE /session, POST /session/refreshStart, end and renew a session (DELETE ends it at once).
GET /toursYour published tours.
POST /chatSend a message; streams the reply.
POST /chat/handoff, GET /chat/messagesAsk for a person; poll for agency messages.
POST /quoteSend 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:

  • 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.