דלגו לתוכן

צ'אט באתר ללא ממשק (למפתחים)

ניסיוני — ללא תמיכה. התכונה עשויה להשתנות או להיעלם בלי הודעה מוקדמת, וצוות התמיכה שלנו לא יכול לעזור בבנייה עליה. כל המידע נמצא בעמוד הזה. הוא כתוב למתכנתים ולסוכני קוד, והפרוטוקול עצמו מתועד באנגלית.

מצב ללא ממשק (headless) מאפשר לאתר שלכם לצייר את הצ’אט בעצמו: בועות משלכם, חיווי הקלדה משלכם ותיבת הודעה משלכם. השיחה עצמה עדיין עוברת דרך הצ’אט האמיתי של omotto באתר, עם אותו בוט, אותה זהות מבקר ואותן מגבלות.

הדף שלכם טוען מסגרת (iframe) נסתרת של הצ’אט במצב headless. המסגרת לא מציירת כלום, ומדברת עם הדף שלכם בהודעות postMessage שמפורטות למטה.

שימו לב מה אתם מאשרים. אתר שרק מציג את הצ’אט הרגיל לא יכול לקרוא את השיחה. אתר שמשתמש במצב ללא ממשק יכול: הוא מקבל כל הודעה ויכול לשלוח בשם המבקר. לכן זה מתג נפרד, כבוי כברירת מחדל.

ב-omotto: הגדרות ← שירותי צ’אט ← אתר ← מצב ללא ממשק (למפתחים).

  • הצ’אט באתר צריך להיות מופעל ועובד.
  • רק הדומיינים המורשים של הצ’אט יכולים להשתמש במצב הזה, ורק רשומות מדויקות ב-HTTPS כמו https://www.example.com. רשומה עם תבנית כמו https://*.example.com לעולם לא נותנת גישה, ורשומת http:// לדומיין חיצוני מתעלמים ממנה.
  • לפיתוח מקומי בלבד, http://localhost (למשל http://localhost:4321) מתקבל בדומיינים המורשים.
<iframe src="https://app.omotto.ai/p/<your-org>/chat-widget?omw_mode=headless&locale=he"
title="" aria-hidden="true" tabindex="-1"
style="position:absolute;width:0;height:0;border:0;visibility:hidden"></iframe>
  • <your-org> הוא ה-slug של הארגון שלכם, הערך של data-org בקוד ההטמעה.
  • https://app.omotto.ai הוא המקור (origin) של הצ’אט בקוד ההטמעה שלכם (בדרך כלל https://app.omotto.ai).
  • locale ‏(he או en) הוא בקשה. אם שפת הצ’אט קבועה בהגדרות, היא קובעת. ב-ready.locale תראו מה התקבל.
  • שלחו hello מיד כשהמסגרת נטענת. כך המסגרת יודעת מה המקור (origin) שלכם גם כשהדף לא שולח referrer.
  • טעינת המסגרת לא יוצרת שיחה ולא הודעה. ה-send הראשון יוצר אותן.
  • כברירת מחדל הצ’אט במצב הזה לא מציג ולא שומר את הודעת הפתיחה, כך שהבוט לא חושב שאמר משהו שהדף שלכם לא הציג. אפשר להצטרף: הוסיפו &greeting=1 לכתובת המסגרת רק אם הדף שלכם יציג את הודעת הפתיחה. אז ready.greeting מכיל את הטקסט שלה, והיא נשמרת כהודעה הראשונה של הבוט, כך שהבוט יודע שאמר אותה. בלי greeting=1 אין שדה greeting ב-ready ושום דבר לא נשמר.

הפרוטוקול, מבנה ההודעות ודוגמת הקוד זהים לגרסה האנגלית של העמוד ומובאים כאן כפי שהם.

Every message, both directions, is a plain object:

{ v: 1, type: string, ...fields }
  • The frame ignores messages whose v is not 1, whose origin is not one of your exact Allowed domains, or that do not come from its parent window.
  • The frame posts only to that exact origin, never "*".
  • The host must check event.origin === <app origin> and event.source === iframe.contentWindow, and ignore unknown types and unknown fields. New fields and new event types are additive within v: 1; a breaking change becomes v: 2.
type fields meaning
hello — Tells the frame your origin. Send it on iframe load and re-send it every 250 ms until ready or unavailable arrives (a frame that is still starting may miss the first one). If the frame is already ready, it answers every hello with ready again; duplicates are harmless.
send ref: string (≤ 64 chars, yours), text: string (≤ 2000 chars) — or file_ids: string[] with an optional text (a caption) Send plain text as the visitor, or the attached files named by file_ids (each must be ready, see attach). Answered by exactly one send_ack or send_failed carrying the same ref. A send of several files is one command and one ack: send_ack.id is the id of the first file’s bubble, and only that bubble carries ref. A file_id that is unknown, already sent or detached, or not yet ready answers send_failed reason: "invalid", as do a repeated file_id and more ids than ready.uploads.max_files. After a send_failed for a send with files, the files stay attached with any reason except error (attachment events resume): send them again or detach them. This includes a retry that fails for any reason except error. With error they stay held, because the send may have landed: use retry, and a late send_ack may still arrive.
press ref: string, message_id: string, option_id: string Tap a quick reply. Sent as the visitor’s message (marked as a button press). Answered like send.
attach ref: string, files: File[] (1–20 File objects, passed by structured clone) Stage files for a later send. Answered by one attachment event per file, all carrying this ref. Check ready.capabilities for "attachments" first; without it every file answers attachment state: "failed" reason: "unavailable".
detach file_id: string Drop a staged file. Never answered; an unknown id is ignored.
react ref: string, message_id: string, emoji: string (≤ 16 characters) Set the visitor’s reaction on an assistant or agent bubble. emoji must be one of ready.allowed_reactions; an empty string "" removes the visitor’s own reaction. Answered by react_ack or react_failed with the same ref. While the chat is not ready (paused included) it answers react_failed reason: "not_ready"; the status event is the signal for paused, and paused as a react_failed reason appears only if the chat pauses while the write is in flight. Setting what is already set, or removing nothing, is acknowledged at once without a change. A bubble you do not have, a visitor bubble, an emoji that is not allowed, or a bubble the server no longer has answers react_failed reason: "invalid".
retry ref: string, message_id: string Resend a visitor message whose send failed (message_id is the bubble’s id). Answered like send, under the new ref: the bubble’s later events carry the new ref, and a late send_ack for the old attempt is reported under it too. A bubble that is not a failed visitor send answers send_failed reason: "invalid"; one the chat cannot take up yet answers reason: "cooling_down".
history — Ask for the transcript again. Answered by history; ignored (no reply) before ready. Returns what the frame holds and does not refresh links.

Send only after ready. Before it, send/press/retry answer send_failed reason: "not_ready", react answers react_failed reason: "not_ready", and attach answers one attachment state: "failed" reason: "not_ready" per file. Commands sent after unavailable get no reply at all. ref must be unique per command. A command with a valid ref but a bad body (empty text, missing ids, no files) answers invalid in its own vocabulary. A command without a valid ref (1–64 characters) cannot be answered and is ignored; so is a detach without a file_id.

type fields meaning
ready protocol: 1, locale: "he"|"en", dir: "rtl"|"ltr", org: { name }, history: Bubble[], has_visitor_turns: boolean, history_complete: boolean, capabilities: string[], allowed_reactions: string[], uploads: { available, max_files, max_file_bytes, content_types } | null, greeting?: string Sent within about 3 s of the frame accepting the identity: when the first transcript load settles, or when 3 s pass without it. history is the visitor’s earlier conversation (most recent 50 bubbles, oldest first) — not pushed as message events. Show it or not; that is your choice. history_complete: false means the transcript load failed or was still running, so history may be short; a history event may follow once the transcript loads, and its rows are never re-sent as message. ready can arrive again later (the visitor’s identity was reset); treat it as a fresh start and replace your transcript with its history. Feature-detect through capabilities, never through the version: it lists "attachments" (uploads are available; uploads gives max_files, max_file_bytes and the accepted content_types), "reactions" (allowed_reactions is the set of emoji you may send) "retry" and "streaming" (the frame can emit draft events, see Live replies; drafts arrive only when the business has live replies on, so never assume they will). Unknown entries are for later; ignore them. greeting is present only when you loaded the frame with greeting=1 and the chat has one.
unavailable reason: "paused"|"unavailable"|"error" Sent instead of ready when the boot fails, or later if access is withdrawn (headless switched off, chat paused or removed). Fall back; the frame sends nothing further, and the results of commands still in flight (a react, an upload) are dropped rather than sent after it, and every command sent after it gets no reply. Before the first ready, exactly one of ready/unavailable is sent.
typing active: boolean The assistant started / stopped preparing a reply. Every active: true is followed by an active: false — at the latest just before the reply’s message, on a send failure, or when the server’s typing window expires.
draft turn: string, text: string The reply being written. text is the whole text so far, never a delta, so a dropped event is harmless: replace your partial bubble with it. It uses the same light markup as message text parts. Sent roughly every 80 ms per turn, and the latest text is always flushed right before draft_end. Never empty: when the partial has no text (for example only a card so far), you get draft_end instead. Only after ready. Check ready.capabilities for "streaming". See Live replies.
draft_end turn: string The partial reply for that turn is gone: the assistant paused (for example to look something up), the reply was cancelled or failed, or the final reply arrived. Remove your partial bubble. When the final reply arrives, draft_end normally comes right before its message (after any typing active: false). Rarely the message arrives first: if an assistant message arrives while a partial is open, treat it as the reply and drop the partial. A paused turn that resumes sends new draft events with the same turn.
message bubble: Bubble A new bubble after ready, as rows reach the frame (place bubbles by created_at if order matters). Includes the visitor’s own sends (with ref when it came from this host’s send).
message_updated bubble: Bubble A bubble you already have changed: a reaction was added or removed, staff edited the text, or media became available. Replace by bubble.id.
message_removed id: string Staff deleted a bubble. Remove it.
send_ack ref, id The server accepted your send/press/retry. The visitor bubble you got with this ref (whose id was a temporary wm_… id) is now permanently id: re-key it. Later updates use id.
send_failed ref, reason: "not_ready"|"cooling_down"|"rate_limited"|"paused"|"unavailable"|"invalid"|"error" The send did not happen. Nothing was posted. If you already got a message for this ref, mark or remove that bubble. Show a polite fallback (e.g. a contact link). With reason error the server may still have received it: a later send_ack for the same ref means it was sent after all.
connection state: "online"|"offline" Both live transports are down (offline) or one recovered (online). Informational.
attachment ref, file_id: string, name: string, state: "preparing"|"uploading"|"checking"|"ready"|"failed", progress?: number (0–1, only while uploading), reason?: string (only when failed) Progress of one staged file, answering attach (ref is the attach ref). file_id is what you pass to send and detach; it is stable. A new event is sent on each state change, and progress at most every 250 ms per file. ready means the file can be sent. A file that is sent or detached leaves silently. reason is a short token: a refusal (not_ready, unavailable, paused, error, invalid), too_many_files, or an upload failure such as too_large, invalid_file, infected, scan_delayed or network_error. The list is open: show a generic message for one you do not know. A file the chat refused before staging it (and every file of an attach that was refused) gets a one-off file_id starting with refused_ and state: "failed"; it cannot be sent or detached.
react_ack ref The react was applied (or was already in place).
react_failed ref, reason: "not_ready"|"cooling_down"|"rate_limited"|"paused"|"unavailable"|"invalid"|"error" The react did not happen. The bubble’s reactions are unchanged.
status state: "ok"|"paused"|"cooling_down"|"offline", retry_after_ms?: number (only with cooling_down) Whether the chat can take a send right now. Sent on every change, not on a countdown; nothing is sent while it stays ok. After every ready, the frame re-sends the current status if it is not ok, so a repeated ready never leaves you assuming ok. If more than one applies, paused wins over offline, and offline over cooling_down. cooling_down is the wait after a rate-limit or a temporary refusal, and retry_after_ms says how long. Informational: a send during cooling_down still answers send_failed reason: "cooling_down".
history history: Bubble[], has_visitor_turns: boolean Answer to a history request, or sent unsolicited (once, when the transcript loads after a ready with history_complete: false). Hosts may receive it at any time and should treat it as “replace your transcript”.

Live replies. draft and draft_end are optional and additive: they flow only when the business has live replies switched on, so a host that ignores them still works, because every reply still arrives as a message. Show draft.text in a temporary partial bubble and replace it on every draft; remove it on draft_end, and the final message follows. Drafts are text only (cards appear in the final message), never appear in history, and are not sent after unavailable.

interface Bubble {
id: string // stable public id ("msg_…"); a pending visitor
// send carries its client id until acked
role: 'visitor' | 'assistant' | 'agent' // agent = a human teammate
created_at: string // ISO 8601
ref?: string // your send/press ref, on visitor bubbles you sent
progress?: true // a short "let me check" note ahead of a slow reply
edited?: true
parts: Part[] // in display order; never empty
buttons?: LinkButton | QuickReplies
reactions?: { emoji: string; mine: boolean }[] // mine = the visitor's own
}
type Part =
| { type: 'text'; text: string }
| { type: 'card'; title?: string; note?: string; price?: string;
url?: string; image_url?: string } // a product card
| { type: 'media'; kind: 'image' | 'file' | 'audio'; state: 'pending' | 'ready' | 'unavailable';
url?: string; filename?: string; width?: number; height?: number }
interface LinkButton { kind: 'link'; label: string; url: string }
interface QuickReplies { kind: 'quick_replies'; options: { id: string; label: string }[] }
  • Every URL is absolute. Card and button URLs are http(s) only; anything else is dropped. Media and card image URLs are signed and valid for about 10 minutes, and v1 has no refresh command. If one expires, re-render without it or reload the frame.
  • A media part’s state is ready (it has a url), pending (still being scanned; a later message_updated adds the url), or unavailable (gone for good, never arrives: show a placeholder, do not wait).
  • A product-card message and a composed response (text interleaved with cards) both arrive as one bubble whose parts are already flattened in order.
  • Quick replies are only ever on the newest bubble; when a newer one arrives the older bubble is re-sent via message_updated without them. A press on any other bubble answers send_failed reason: "invalid".
  • Reactions are what the visitor sees in the ordinary chat: grouped by emoji, with no actor beyond mine.
  • Only what the ordinary chat shows crosses: no internal notes, team threads, other conversations, or other visitors.
host ⇢ iframe loads /p/your-org/chat-widget?omw_mode=headless&locale=en
frame → {v:1,type:"ready",protocol:1,locale:"en",dir:"ltr",org:{name:"omotto"},
history:[],has_visitor_turns:false,history_complete:true}
host → {v:1,type:"send",ref:"h1",text:"Let's book a demo meeting"}
frame → {v:1,type:"message",bubble:{id:"wm_01J…",role:"visitor",ref:"h1",parts:[{type:"text",text:"Let's book a demo meeting"}],…}}
frame → {v:1,type:"send_ack",ref:"h1",id:"msg_01J…"}
frame → {v:1,type:"typing",active:true}
frame → {v:1,type:"draft",turn:"t_01K…",text:"Happily! Which"} (only with live replies on)
frame → {v:1,type:"draft",turn:"t_01K…",text:"Happily! Which works better for you?"}
frame → {v:1,type:"typing",active:false}
frame → {v:1,type:"draft_end",turn:"t_01K…"}
frame → {v:1,type:"message",bubble:{id:"msg_01K…",role:"assistant",
parts:[{type:"text",text:"Happily! Which works better for you?"}],
buttons:{kind:"quick_replies",options:[{id:"msg_01K…:0",label:"Tomorrow morning"},
{id:"msg_01K…:1",label:"Thursday afternoon"}]}}}
host → {v:1,type:"press",ref:"h2",message_id:"msg_01K…",option_id:"msg_01K…:1"}
frame → … send_ack h2, typing true/false, message …
frame → {v:1,type:"message_updated",bubble:{id:"msg_01J…",…,reactions:[{emoji:"👍",mine:false}]}}

A file with a caption (ready listed "attachments"; file is a File from an <input type="file">):

host → {v:1,type:"attach",ref:"a1",files:[file]}
frame → {v:1,type:"attachment",ref:"a1",file_id:"u_7",name:"menu.jpg",state:"preparing"}
frame → {v:1,type:"attachment",ref:"a1",file_id:"u_7",name:"menu.jpg",state:"uploading",progress:0.4}
frame → {v:1,type:"attachment",ref:"a1",file_id:"u_7",name:"menu.jpg",state:"checking"}
frame → {v:1,type:"attachment",ref:"a1",file_id:"u_7",name:"menu.jpg",state:"ready"}
host → {v:1,type:"send",ref:"h3",file_ids:["u_7"],text:"Is this the current menu?"}
frame → {v:1,type:"message",bubble:{id:"wm_01L…",role:"visitor",ref:"h3",parts:[{type:"media",kind:"image",state:"pending"},…],…}}
frame → {v:1,type:"send_ack",ref:"h3",id:"msg_01L…"}
host → {v:1,type:"react",ref:"r1",message_id:"msg_01K…",emoji:"👍"}
frame → {v:1,type:"react_ack",ref:"r1"}

Failure and timeouts (host side)

הסעיף ״Failure and timeouts (host side)״
  • Headless mode switched off, a wrong org slug, and too many loads from one network all return the same empty, script-less document. A page that is not in Allowed domains is refused by the browser, which does not render the frame at all. Either way you get load (or not even that) and then nothing. Wait about 6 s for ready or unavailable (the frame itself answers within about 3 s of its identity call), then fall back. A ready that arrives after you have fallen back may be ignored, or adopted if your UI can switch back.
  • After ready, a send with no send_ack/send_failed within ~20 s should be treated as failed on your side; the frame retries nothing on its own.
  • The visitor’s identity is kept in the frame’s own browser storage, exactly as for the ordinary chat. A browser that blocks frame storage, or a private window, starts a new visitor on each load.
  • Rate limits and abuse protection are the same as for the ordinary chat.

A complete minimal host script. Add your own rendering where the comments say so.

const APP = 'https://app.omotto.ai' // the origin of the chat in your install snippet
const ORG = 'your-org' // your org slug, the data-org value in your install snippet
const frame = document.createElement('iframe')
frame.src = `${APP}/p/${ORG}/chat-widget?omw_mode=headless&locale=en`
frame.setAttribute('aria-hidden', 'true')
frame.tabIndex = -1
frame.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden'
let ready = false
let partial = null // your one live partial bubble element, if any
let seq = 0
let helloTimer
const post = (msg) => frame.contentWindow.postMessage({ v: 1, ...msg }, APP)
const fallback = () => { ready = false; /* show your own contact link */ }
const settled = () => { clearInterval(helloTimer); clearTimeout(giveUp) }
const giveUp = setTimeout(() => { if (!ready) { clearInterval(helloTimer); fallback() } }, 6000)
window.addEventListener('message', (event) => {
if (event.origin !== APP || event.source !== frame.contentWindow) return
const msg = event.data
if (!msg || msg.v !== 1) return
switch (msg.type) {
case 'ready': // can repeat (repeated hello, reset visitor): replace, never append
ready = true; settled(); /* replace your whole transcript with msg.history */ break
case 'history': /* replace your whole transcript with msg.history */ break
case 'unavailable': settled(); fallback(); break // also after ready
case 'typing': /* show or hide your indicator: msg.active */ break
case 'draft': // only with live replies on, and only if ready listed 'streaming'
// msg.text is the WHOLE reply so far, not a delta: replace one partial bubble's text with it
if (!partial) partial = createBubble('assistant') // your own bubble element
partial.textContent = msg.text
break
case 'draft_end': // paused, cancelled, failed, or the final reply is next
partial?.remove(); partial = null // if a reply follows, it arrives as 'message'
break
case 'message': // append msg.bubble (place by created_at if order matters)
if (msg.bubble.role !== 'visitor') { partial?.remove(); partial = null } // a reply replaces any partial
break
case 'message_updated': /* replace the bubble with id msg.bubble.id */ break
case 'message_removed': /* remove the bubble with id msg.id */ break
case 'send_ack': /* the bubble you drew with ref msg.ref now has id msg.id: re-key it */ break
case 'send_failed': /* the bubble with ref msg.ref was not sent; tell the visitor (msg.reason) */ break
case 'attachment': /* update the chip for msg.file_id: msg.state, msg.progress, msg.reason */ break
case 'react_ack': case 'react_failed': /* msg.ref is your react; failed: undo your optimistic reaction */ break
case 'status': /* msg.state: disable your send box unless 'ok' (msg.retry_after_ms) */ break
default: break // ignore anything you do not know
}
})
// A frame that is still starting may miss the first hello: repeat it until it answers.
frame.addEventListener('load', () => {
post({ type: 'hello' })
helloTimer = setInterval(() => post({ type: 'hello' }), 250)
})
document.body.appendChild(frame)
// `ref` must be unique per send; the frame answers each one with send_ack or send_failed.
export function send(text) {
if (!ready) return false
post({ type: 'send', ref: `s${++seq}`, text })
return true
}
export function press(bubble, option) {
post({ type: 'press', ref: `p${++seq}`, message_id: bubble.id, option_id: option.id })
}
// Attach, wait for `attachment` state 'ready' for each file_id, then send them.
// Only if the ready event listed 'attachments' in its capabilities.
export function attach(files) { post({ type: 'attach', ref: `a${++seq}`, files }) }
export function sendFiles(fileIds, caption) {
post({ type: 'send', ref: `f${++seq}`, file_ids: fileIds, text: caption })
}
// emoji must be in ready.allowed_reactions; '' removes the visitor's own reaction.
export function react(bubble, emoji) {
post({ type: 'react', ref: `r${++seq}`, message_id: bubble.id, emoji })
}