Embed widget

Drop a chat bubble into your site with one <script> tag. The widget runs in an iframe served from https://api.juglans.ai/embed/iframe, mints its own per-visitor session against the project's embed config, and streams chat through /api/embed/chat — your jg_p_* project key never touches the browser.

30-second start

  1. Open the project's API tab in the dashboard, then switch to the Embed sub-tab (/app/project/api?tab=embed).
  2. Pick a default agent → Enable embed.
  3. Copy the auto-bubble snippet and paste into your site's <head>:
<script
  src="https://api.juglans.ai/embed.js"
  data-embed-id="emb_a1b2c3d4e5f6a7">
</script>

A floating bubble appears bottom-right of every page that includes the snippet. Click it to open the chat. Visitors get an anonymous session keyed by their browser's localStorage — same visitor = same conversation across page loads.

Three authentication modes

Mode Config Best for
anonymous (default) Marketing sites, public docs, support widgets where you don't know who the visitor is
identity_required embed config + HMAC secret SaaS apps where your backend knows the user — pass their identity through so conversations are bound to your user id
either as above Both — public visitors get anonymous, signed-in users get identified

Change the mode in API → Embed → Authentication mode (/app/project/api?tab=embed) in the dashboard.

Anonymous mode

The default. No backend work on your side — just the <script> tag above. The widget generates anon_<random> on first visit, stores it in localStorage, and posts that as the X-USER-ID-equivalent. Conversations are partitioned per browser.

Anonymous visitors are still rows in project_external_users. From the dashboard you can see them as anon_* end-users and delete them (GDPR / abuse), though deletion does not block re-creation — see External users.

Identity verification (HMAC)

When you want the chat bound to your user id (so the agent knows who's asking, and conversations follow the user across devices), mint an HMAC secret and have your backend sign a short-lived token.

One-time: generate the HMAC secret

  1. In API → Embed, set Authentication mode to identity_required or either.
  2. Click Generate secret under Identity Verification (HMAC).
  3. The plaintext is shown once — copy it now (wse_…). The server only stores its encrypted form; the prefix wse_xxxxxxxx is what you'll see afterwards.

Each request: your backend signs a token

Sign hmac_sha256(secret, "{embed_id}.{user_id}.{ts}") and ship {ts}.{base64url(sig)} to the browser. The browser passes it as data-user-token on the script tag (or as the user-token attribute on <juglans-chat>).

Server-side validation requires |now - ts| ≤ 300s, so use a freshly minted token per page load.

Node.js

import crypto from 'node:crypto';

function juglansToken(embedId, userId, secret) {
  const ts = Math.floor(Date.now() / 1000);
  const sig = crypto
    .createHmac('sha256', secret)
    .update(`${embedId}.${userId}.${ts}`)
    .digest('base64url');
  return `${ts}.${sig}`;
}

// In your page template:
res.render('page', {
  embedId: 'emb_a1b2c3d4e5f6a7',
  userId: req.user.id,
  userToken: juglansToken(
    'emb_a1b2c3d4e5f6a7',
    req.user.id,
    process.env.JUGLANS_EMBED_SECRET,
  ),
});

Python

import base64, hashlib, hmac, time

def juglans_token(embed_id: str, user_id: str, secret: str) -> str:
    ts = int(time.time())
    msg = f"{embed_id}.{user_id}.{ts}".encode()
    sig = hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    return f"{ts}.{base64.urlsafe_b64encode(sig).decode().rstrip('=')}"

# Pass into your template:
token = juglans_token("emb_a1b2c3d4e5f6a7", current_user.id, EMBED_SECRET)

PHP

function juglans_token(string $embed_id, string $user_id, string $secret): string {
    $ts = time();
    $msg = "$embed_id.$user_id.$ts";
    $sig = hash_hmac('sha256', $msg, $secret, true);
    $b64 = rtrim(strtr(base64_encode($sig), '+/', '-_'), '=');
    return "$ts.$b64";
}

Ruby

require 'openssl'
require 'base64'

def juglans_token(embed_id, user_id, secret)
  ts = Time.now.to_i
  msg = "#{embed_id}.#{user_id}.#{ts}"
  sig = OpenSSL::HMAC.digest('sha256', secret, msg)
  b64 = Base64.urlsafe_encode64(sig, padding: false)
  "#{ts}.#{b64}"
end

Pass the token to the widget

<script
  src="https://api.juglans.ai/embed.js"
  data-embed-id="emb_a1b2c3d4e5f6a7"
  data-user-id="customer_47291"
  data-user-token="<%= userToken %>">
</script>

The browser POSTs {embed_id, user_id, token} to /api/embed/session; the server decrypts the stored HMAC secret, re-computes the signature with the same payload, and constant-time compares. A match returns an embed JWT bound to that user.

Allowed origins

The widget refuses to mint a session when the host page's Origin isn't in the embed config's allowlist. Set this from Allowed origins in the API → Embed panel — one per line, exact match:

https://app.acme.example
https://staging.acme.example

Special cases:

Value Effect
(empty) Enforcement disabled — dev only. The server logs a warning per request.
* (sole entry) Any origin. Marketing-site escape hatch.

Subdomain wildcards (*.acme.example) are not supported in v1 — list each one.

Prompt placement (hero search-bar style)

For landing pages and hero sections, where you want a permanent "ask me anything" input that lives in the page flow and pops the chat into a floating modal when the visitor submits:

<script type="module" src="https://api.juglans.ai/embed.js"></script>

<juglans-chat
  embed-id="emb_a1b2c3d4e5f6a7"
  position="prompt"
  placeholder="Ask anything about pricing, integrations, or roadmap…">
</juglans-chat>

Behavior:

  • The input renders directly in the host page flow (block element, max-width 720px, centered) — style it with your own CSS by wrapping it in a container.
  • On submit, a bubble-style modal pops up at the bottom-right of the viewport. The first message is streamed there as the assistant reply.
  • The input stays put. Submitting again continues the same conversation (or starts a new one if the user closes & re-opens the modal via JuglansChat.close() + JuglansChat.startNew()).
  • Mobile-friendly: modal fills the viewport on screens < 480px.

Same identity / theme / HMAC flows as the other modes. The only extra attribute is placeholder, which sets the input's empty-state hint text.

Inline placement (Web Component)

For chats sitting in a page (sidebar widget, dedicated chat page, internal tooling), drop the auto-bubble and use the custom element:

<script type="module" src="https://api.juglans.ai/embed.js"></script>

<div style="height: 600px">
  <juglans-chat
    embed-id="emb_a1b2c3d4e5f6a7"
    position="inline">
  </juglans-chat>
</div>

The chat fills its parent container. Sizing is up to your CSS — the chat reports back-edge content height only when explicitly opted in (future autoresize attribute).

Attributes

Attribute Default Effect
embed-id (required) The emb_* identifier.
position bubble bubble = floating popup, inline = fill container.
user-id Customer-side user id (pair with user-token).
user-token HMAC token (above).
show-header true Set to "false" to hide the title bar entirely.
compact false Tightens paddings, hides the composer avatar. Use for narrow sidebars (≤ 380px).
max-width 720 Center-column cap in pixels. Useful when the iframe is wider than the chat should be.
title (from server) Override the agent name in the header.
greeting (from server) Override the empty-state greeting.
suggestions (from server) Suggestion chips above the composer. JSON array, or pipe / newline-separated.
primary-color (from server) CSS color for the accent.
agent-bubble-color (from server) CSS color for the assistant bubble background.
bg-color (from server) CSS color for the chat surface.

Theming

theme is a free-form JSONB field on the embed config. The widget reads the keys below; everything else is ignored verbatim (room to grow).

Key Type Effect
theme.title string Overrides the agent name in the header.
theme.greeting string Replaces "How can I help?" in the empty state.
theme.suggestions string[] Chip buttons shown above the composer when the log is empty. First 6 used.
theme.colors.primary CSS color Bubble + send button accent.
theme.colors.primary_on CSS color Text/icon color on the primary background.
theme.colors.agent_bubble CSS color Assistant message bubble background.
theme.colors.bg_surface CSS color Main chat background.
theme.colors.bg_elevated CSS color User bubble / composer pill background.
theme.colors.text_primary CSS color Main text color.
theme.colors.text_secondary CSS color Labels, placeholders.
theme.colors.border CSS color Composer + dividers.

Set these from the Theme section inside API → Embed (color pickers + plain text inputs for greeting / suggestions).

Override from the page

Server-side theme is the default; you can override per-page from your own JS or from extra attributes on the script / element. Two shapes, same precedence (page wins):

Static attributes — handy when the values are known at render time and you don't need runtime mutation. Works on both the auto bubble (<script data-…>) and the explicit element (<juglans-chat …>):

<juglans-chat
  embed-id="emb_a1b2c3d4e5f6a7"
  position="inline"
  title="Acme Helpdesk"
  greeting="Welcome back — what's broken today?"
  suggestions='["Reset my password","Update billing","Talk to a human"]'
  primary-color="#FF5722"
  agent-bubble-color="rgba(255, 87, 34, 0.08)">
</juglans-chat>

suggestions accepts either a JSON array, pipe-separated (a | b | c), or one-per-line. The first six entries are used.

Runtime API — call any time after embed.js has parsed; the chat re-paints immediately. Useful when the values depend on a state your SPA only has after hydration (i18n strings, the user's plan, dark/light mode, etc.):

JuglansChat.configure({
  title: 'Acme Helpdesk',
  greeting: i18n.t('chat.greeting'),
  suggestions: i18n.tArray('chat.suggestions'),
  colors: {
    primary: '#FF5722',
    primary_on: '#FFFFFF',
    agent_bubble: 'rgba(255, 87, 34, 0.08)',
  },
});

// Clear an override and fall back to the server value:
JuglansChat.configure({ greeting: null });

// Target a specific element (when you have several on the page):
JuglansChat.configure({ title: 'Sales' }, '#chat-sales');

The loader sanitizes the patch before crossing the iframe origin boundary — only the whitelisted keys flow through:

Top-level Type
title string
greeting string
suggestions string[] (first 6 used)
colors object — keys below

colors: primary, primary_on, agent_bubble, bg_surface, bg_elevated, text_primary, text_secondary, border. Each is any valid CSS color string.

Security-sensitive settings (mode, allowed_origins, the HMAC secret, the branding_powered_by toggle) stay server-only and cannot be patched from JS by design.

Endpoint reference

The widget calls these — listed here so you can debug from the browser network panel.

Method Path Auth Purpose
GET /embed.js None The loader script.
GET /embed/iframe?id=…&mode=… None The chat iframe HTML.
GET /api/embed/config?id=… None Public subset of the embed config (theme, mode, origins, agent_name).
POST /api/embed/session Origin-checked Mint a short-lived (15 min) embed JWT from {embed_id, anon_id} or {embed_id, user_id, token}.
POST /api/embed/chat Embed JWT Stream a chat reply as SSE. Same `{type: "meta"
GET /api/embed/conversations Embed JWT List the current visitor's conversations.
GET /api/embed/conversations/{cid} Embed JWT Fetch one conversation with full message log — used by the iframe to hydrate history on reload.

Troubleshooting

Symptom Likely cause
Embed not found: "emb_…" in the bubble Wrong data-embed-id, embed disabled in the dashboard, or the <juglans-chat> element was rendered before the framework wired the embed-id attribute (Solid/Vue can do this — make sure you spell the attribute with a hyphen, not camelCase).
Origin not allowed (403 on session) Add the customer's exact origin to Allowed origins, or use * for any.
Identity verification required (403) Mode is identity_required and the script tag is missing data-user-token, or your token's ts is more than 5 minutes old.
(no reply) in the chat bubble Server received the message but juglans library produced no events within 8s. Check agent_data/{id}/chat.jg exists and [ai.providers.juglans] in its juglans.toml is correct.
Markdown not rendering Hard refresh — the loader script is cached for 5 minutes and the iframe HTML for 5 minutes. After a deploy, the very first iframe load may serve the cached old version.