Skip to content

Embedding Widgets

oHallo provides embeddable widgets that let your website visitors search your knowledge base, submit inquiries, book meetings, or chat with your assistant without leaving your site. Widgets are lightweight JavaScript components that load asynchronously and work on any website.

A search interface that lets visitors find answers from your oHallo knowledge base. The visitor types a question, and the widget returns matching KB entries with relevant excerpts. This can deflect support requests by giving visitors instant answers.

A structured form that lets visitors submit an inquiry. The submission creates a new conversation in oHallo, routed to the appropriate workspace and channel. You can configure which fields to show (name, email, subject, message, category).

A multi-step form that lets visitors book a meeting with your team. The widget walks the visitor through four steps: filling in contact details, picking a date, selecting an available time slot, and confirming the booking. oHallo creates a tentative calendar event, sends the visitor a confirmation email, and once they confirm, adds a Google Meet link to the event.

A live chat panel for your website. A visitor asks a question and the assistant answers in the panel, and passes the conversation to your team when the visitor needs it. The chat widget and the chat channel are one record: creating the widget provisions the channel behind it. Its presentation is set in the dashboard, as a floating launcher in the corner of the page, a full-height sidebar, or an inline section of the page.

  1. Open the oHallo dashboard.
  2. Go to Settings then Widgets.
  3. Click + New Widget.
  4. Choose the widget type: Knowledge Base, Contact Form, or Booking.
  5. Configure the widget:
    • Name: An internal label (e.g. “Support KB Widget”).
    • Workspace: The workspace whose KB entries should be searchable, or where form submissions should be routed.
    • Appearance: Primary colour, position on the page (bottom-right or bottom-left), and whether the widget starts open or collapsed.
    • Calendar connection (Booking only): Which connected calendar to use for availability and events.
    • Slot duration (Booking only): Meeting length (15, 30, 45, or 60 minutes).
  6. Click Create. oHallo generates a public key and embed code.

Chat widgets live on the Live chat tab of the same page, where New chat widget creates one. Live chat and the chat widget covers the rest of the setup, including how the assistant hands a conversation to your team.

After creating a widget, you will see an embed code snippet on the widget’s settings page. For a Knowledge Base widget it looks like this:

<div id="ohallo-kb"></div>
<script
src="https://widget.ohallo.eu/kb.js"
data-key="your-public-key"
data-target="ohallo-kb"
async
></script>

Copy this snippet. You will add it to your website in the next step.

Place the container where the widget should appear, and paste the script tag just before the closing </body> tag:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
</head>
<body>
<header>
<h1>Welcome to Example Company</h1>
</header>
<main>
<p>Your website content here.</p>
<div id="ohallo-kb"></div>
</main>
<!-- oHallo Widget -->
<script
src="https://widget.ohallo.eu/kb.js"
data-key="your-public-key"
data-target="ohallo-kb"
async
></script>
</body>
</html>

The widget loads asynchronously and does not block your page rendering. It renders inside the element whose id matches data-target.

For React, Vue, Next.js, or other single-page applications, add the script tag to your root HTML file (e.g. index.html or _document.tsx).

The chat widget follows navigation on its own. On a site that changes pages without a full page load (a single-page application, Astro view transitions, Turbo, htmx and similar), the chat stays on the page through every navigation, open or minimised as the visitor left it, with an unsent message still in the box. With the inline presentation it moves into the data-target element on each page that has one.

To let your framework carry the floating or sidebar chat itself, add an element marked data-ohallo-chat-slot to your layout: the chat mounts inside it. On Turbo, give that element an id and data-turbo-permanent, and Turbo moves it, with the chat in it, from page to page:

<div id="chat-slot" data-ohallo-chat-slot data-turbo-permanent></div>

The knowledge base, contact form and booking widgets render into their target element once, when their script runs. On a single-page application, place that element in a layout that stays on the page across routes, as in the root layout below.

React / Next.js. Add to pages/_document.tsx or app/layout.tsx:

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<div id="ohallo-kb" />
<script
src="https://widget.ohallo.eu/kb.js"
data-key="your-public-key"
data-target="ohallo-kb"
async
/>
</body>
</html>
)
}

Static site generators (Astro, Hugo, Jekyll). Add the script to your base layout template.

The booking widget uses its own script, booking.js, and renders inline within a target element. Add a container element and the script tag:

<div id="ohallo-booking"></div>
<script
src="https://widget.ohallo.eu/booking.js"
data-key="your-public-key"
data-target="ohallo-booking"
async
></script>

The data-key attribute is the widget’s public key (visible on the widget settings page). The data-target attribute is the ID of the DOM element where the widget mounts.

React / Next.js:

export default function BookingPage() {
return (
<div>
<h1>Book a meeting</h1>
<div id="ohallo-booking" />
<script
src="https://widget.ohallo.eu/booking.js"
data-key="your-public-key"
data-target="ohallo-booking"
async
/>
</div>
)
}

Astro:

<div id="ohallo-booking"></div>
<script
src="https://widget.ohallo.eu/booking.js"
data-key="your-public-key"
data-target="ohallo-booking"
async
></script>

Like the KB and Contact Form widgets, the booking widget’s appearance (variant, colours, theme) is configured entirely in the dashboard. The embed code only needs the public key and target element.

The contact form renders inline in a target element, the same way the booking widget does:

<div id="ohallo-form"></div>
<script
src="https://widget.ohallo.eu/form.js"
data-key="your-public-key"
data-target="ohallo-form"
async
></script>

data-key is the widget’s public key and data-target is the id of the element the form mounts into. Both are required: with either missing, the widget renders nothing rather than guessing.

The form’s fields, styling and success behaviour all come from the widget’s own configuration in the dashboard, so one key always produces the same form on every page that embeds it.

Some values belong to the page rather than the visitor: which job posting an application answers, which product page a lead came from, which campaign brought them.

Add a field of type Hidden to the form in the dashboard, give it a label such as Role, and the form will never show it. Then have the embedding page supply its value with a data-field-<name> attribute:

<div id="ohallo-form"></div>
<script
src="https://widget.ohallo.eu/form.js"
data-key="your-public-key"
data-target="ohallo-form"
data-field-role="Senior Marketing Specialist"
async
></script>

<name> matches the field’s id, or its label reduced to lowercase with non-alphanumeric runs replaced by hyphens. A field labelled Role is addressed as data-field-role, one labelled Where did you hear about us as data-field-where-did-you-hear-about-us. Matching by label is usually what you want, because field ids are generated when the field is created.

The value is submitted like any other answer, so it is stored with the submission and appears in the conversation the form creates.

Whether a field is shown is decided by its type in the dashboard, never by the embed. A data-field-<name> aimed at a visible field sets that field’s initial value, and the visitor can still change it, exactly like browser autofill. Notes:

  • HTML lowercases attribute names, so data-field-jobRole reaches the widget as jobrole. Use lowercase and hyphens.
  • An attribute naming a field the form does not have is ignored, and the widget logs one console error listing the fields it did find. It never invents a field and never blocks the form from rendering.
  • File, checkbox and captcha fields cannot be filled this way.
  • A hidden field marked required must be supplied by every page that embeds the form, otherwise submission fails validation.

A value supplied this way is not verified. It reaches the server with exactly the trust of something the visitor typed, so treat it as a label for your team to read, not as proof of anything.

The chat widget loads from chat.js and takes its presentation from the dashboard. The script tag is the same for the floating, sidebar and inline presentations; inline adds a container of your own, below.

<script
src="https://widget.ohallo.eu/chat.js"
data-key="your-public-key"
data-api-base="https://api.ohallo.eu"
async
></script>

The inline presentation renders the conversation inside an element of your own. Add the container and point data-target at its id:

<div id="ohallo-chat"></div>
<script
src="https://widget.ohallo.eu/chat.js"
data-key="your-public-key"
data-api-base="https://api.ohallo.eu"
data-target="ohallo-chat"
async
></script>

Whether the panel is open is remembered for the browser tab, so a visitor who opens the chat and then moves to another page of your site arrives with the conversation still open.

If your site sends a Content Security Policy, add the chat widget’s script host and its connection to the oHallo API to the directives your policy already has:

script-src https://widget.ohallo.eu
connect-src https://api.ohallo.eu wss://api.ohallo.eu

The chat widget publishes window.__ohalloChat, so a link or button anywhere on the page can open the chat: a “Chat with us” line in your page copy, a button at the end of a help article, an item in your site navigation.

CallWhat it does
window.__ohalloChat.open()Opens the chat panel. On a panel that was closed, the cursor lands in the message box
window.__ohalloChat.close()Collapses the panel back to the launcher
window.__ohalloChat.isOpen()Returns true while the panel is open

The widget script publishes the object when it loads, so write the calls with optional chaining (window.__ohalloChat?.open()). On a page whose own code can run before that script, the object is not there yet and the call does nothing. Once the object exists, a call made while the widget is still starting up is held and applied as soon as it is ready, so a control the visitor clicks is reliable: by the time anyone can click it, the script has loaded. When the chat does not start on the page, for example because the channel is switched off, the object is removed again and the call does nothing.

In the inline presentation the conversation is a section of the page and is always open, so open() and close() leave it as it is, and isOpen() reports true there once the widget has mounted.

Three details worth knowing when you wire your own control. close() returns focus to the launcher, the same as the visitor minimising the panel. Calling open() on a panel that is already open leaves it alone rather than moving the cursor back to the message box. And isOpen() reports false until the widget has mounted.

A page button that opens the chat:

<p>
Questions about your order?
<button type="button" id="chat-with-us">Chat with us</button>
</p>
<script
src="https://widget.ohallo.eu/chat.js"
data-key="your-public-key"
data-api-base="https://api.ohallo.eu"
async
></script>
<script>
document.getElementById('chat-with-us').addEventListener('click', () => {
window.__ohalloChat?.open()
})
</script>

React / Next.js. Declare the global once, then call it from any component:

'use client'
declare global {
interface Window {
__ohalloChat?: { open(): void; close(): void; isOpen(): boolean }
}
}
export function ChatWithUsButton() {
return (
<button type="button" onClick={() => window.__ohalloChat?.open()}>
Chat with us
</button>
)
}

Every widget’s appearance (colours, fonts, theme, fields) is configured in the dashboard, on the widget’s settings page. The script tag carries no styling, so a change in the dashboard reaches every page that embeds the widget.

Each widget loads from its own script: kb.js for the Knowledge Base widget, form.js for the Contact Form widget, booking.js for the Booking widget and chat.js for the chat widget. The script tag reads these attributes:

AttributeRequiredDescription
data-keyYesThe widget’s public key
data-targetKB, Contact Form and Booking: yes. Chat: for the inline presentationThe id of the element the widget mounts into
data-api-baseNoThe oHallo API base URL, as an absolute https:// URL. A script loaded from widget.ohallo.eu derives it from its own address. A script served from any other host needs it, and renders nothing without it
data-field-<name>NoContact Form only. Supplies a field value from the page. See Hidden fields filled by the page

The snippet the dashboard gives you sets data-api-base explicitly:

<div id="ohallo-kb"></div>
<script
src="https://widget.ohallo.eu/kb.js"
data-key="your-public-key"
data-api-base="https://api.ohallo.eu"
data-target="ohallo-kb"
async
></script>

The Knowledge Base and Contact Form widgets show their own text (buttons, the search box, validation and submit messages, the article count) in the operating language of the widget’s workspace. The language comes from the workspace, not from the visitor’s browser or the embed code, so the script tag takes no language attribute. Each widget sets lang on its root element to the language it renders, and the text you write in the dashboard (field labels, field placeholders, dropdown options, the success message, knowledge base articles and topics) appears exactly as written. A change to the workspace’s operating language reaches embedded widgets within a few minutes.

Widgets use public key authentication. The public key in data-key identifies the widget and its associated workspace. No API key is needed in the embed code, and the public key is safe to include in client-side code.

The widget endpoint validates the public key server-side and only returns KB entries, accepts form submissions, or shows booking availability for the configured workspace. There is no way for a visitor to access data from other workspaces or tenants through the widget.

Widget requests are rate-limited per IP address to prevent abuse. The default limits are generous enough for normal usage. If you expect very high traffic, contact support to adjust the limits for your widget.

When a visitor submits the Contact Form widget, oHallo creates a new conversation:

  • Channel: The submission arrives as a widget channel conversation.
  • Contact: oHallo creates or matches a contact based on the email address provided.
  • Workspace: The conversation is routed to the workspace configured for the widget.
  • Processing: The conversation goes through the same agent pipeline as any other inbound message: the orchestrator plans and dispatches, specialist agents gather data, and the message agent composes and validates the reply.

A submission carries at most 200 fields and a field at most 200 values. A value is at most 20,000 characters, a submission’s text at most 64 KiB, and a file at most 10 MiB. A larger submission is refused (413 for its size, 422 otherwise) and creates nothing. A form takes at most 200 fields, and a field at most 200 options.

You can view widget submissions alongside your email and chat conversations in the oHallo dashboard.

When a visitor completes the booking widget, the following sequence occurs:

  1. The widget fetches its configuration from GET /public/{publicKey}/config, which returns the form fields, appearance settings, and calendar metadata.
  2. The visitor fills in the contact fields and picks a date.
  3. The widget fetches available slots from GET /public/{publicKey}/availability?date=YYYY-MM-DD&timezone=Europe/Amsterdam. This returns time slots calculated from the calendar’s working hours, minus existing events, minus buffer time.
  4. The visitor selects a time slot and submits.
  5. POST /public/{publicKey}/submit with { fields, selectedSlot, sourceUrl } creates the booking.
  6. oHallo creates a new conversation and a tentative event on your connected calendar.
  7. The visitor receives a confirmation email with a link. The tentative slot is held for the configured expiry period (default: 24 hours).
  8. When the visitor clicks the confirmation link, the calendar event is updated to confirmed and a Google Meet link is generated.
  9. Both the visitor and your team member receive confirmation emails with the meeting details and conferencing link.

If the visitor does not confirm within the expiry period, the tentative event is removed from the calendar and the slot becomes available again.

Here is a full HTML page with a Knowledge Base widget embedded:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example Company - Support</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
}
h1 { margin-bottom: 8px; }
p { line-height: 1.6; }
.help-note {
background: #f0f4ff;
border-left: 4px solid #0F62FE;
padding: 16px;
margin: 24px 0;
}
</style>
</head>
<body>
<h1>Example Company Support</h1>
<p>Find answers to common questions or reach out to our team.</p>
<div class="help-note">
<p>Looking for a quick answer? Search our knowledge base below.</p>
</div>
<div id="ohallo-kb"></div>
<h2>Contact us</h2>
<p>
If you cannot find what you are looking for, send us a message.
Our team typically responds within 4 business hours.
</p>
<!-- oHallo Knowledge Base Widget -->
<script
src="https://widget.ohallo.eu/kb.js"
data-key="your-public-key"
data-target="ohallo-kb"
async
></script>
</body>
</html>