Widget Public API and Events
When Public API Is Needed
Use Website Widget for ordinary connection and channel settings. Public API is needed after installation when the site must open chat itself, change context, select a dialog, send a prepared question, or react to widget events.
The Public API does not require a separate cabinet switch. Its methods appear on window.SenlerWidget after the channel code loads. The linked setup guide shows cabinet actions with screenshots; this page covers only actions performed by site code.
Initialization
Create a channel with the screenshot guide, then copy the loader script and channel_id from Embed Code. Call SenlerWidget.init(config) once per page:
<script src="URL_FROM_GENERATED_CODE" crossorigin="anonymous"></script>
<script>
SenlerWidget.init({
channel_id: "xxx",
});
</script>
Replace both placeholders with cabinet values; do not reuse a loader URL from another project's or environment's example. See Initialization Parameters for init, placement modes, visitor data, and theme. Initialization creates one instance. Calling init again destroys the current instance first, so after startup use SenlerWidget methods without recreating the iframe or losing dialog state.
Methods
| Method | Returns | Result |
|---|---|---|
SenlerWidget.open(config?) | undefined | Applies runtime configuration when supplied and shows the widget. |
SenlerWidget.close() | undefined | Hides the widget while preserving the iframe and dialog state. |
SenlerWidget.toggle() | undefined | Toggles visibility. |
SenlerWidget.isOpen() | boolean | Reports whether the widget is visible. |
SenlerWidget.selectDialog(dialogId) | undefined | Selects an existing dialog without sending a message or automatically showing a hidden widget. |
SenlerWidget.setPageContext(items) | undefined | Replaces persistent page context. |
SenlerWidget.updateRuntime(config) | undefined | Changes runtime settings without recreating the widget or saving channel settings. |
SenlerWidget.createInlineTextEdit(config) | controller | Creates a controller for inline edits. |
SenlerWidget.destroy() | undefined | Removes the iframe, button, and handlers. A site-provided embedded container remains in the page. |
Use close() rather than destroy() for normal hiding. After close(), call open(); after destroy(), call SenlerWidget.init(...) again.
Before the instance is ready, isOpen() returns false. In button_only mode, open and close methods display nothing; see settings.
After the loader is available, SenlerWidget.runtimeProtocolVersion contains the current protocol number (number). It is a diagnostic compatibility value; do not tie site business logic to a particular number.
Runtime parameters
Runtime parameters are temporary state for the current instance. They are not saved to channel settings and reset after destroy() or another init. open(config?) and updateRuntime(config) accept the same configuration:
| Parameter | Type | What it changes |
|---|---|---|
lang | "ru" | "en" | "auto" | Interface language. |
display_mode | "popup" | "embedded" | Placement mode. The embedded container must already be provided during init. |
theme_mode | "light" | "dark" | "auto" | Current instance theme. |
border_radius | number | Current-instance rounding from 0 to 50. |
shell | object | collapse_button and mobile_edge_swipe. |
customActions | object | Complete set of available site actions and their browser-side handlers. |
customActionsLanguage | "ru" | "en" | Action-description language. |
autoExecuteCustomActionNames | string[] | Action names allowed to execute automatically in the current scenario. |
dialogId | string | Existing dialog to select. |
startNewDialog | boolean | Creates a new empty dialog when true. |
focusInput | boolean | Focuses the message input when true. |
pageContextItems | array | Replaces persistent page context. setPageContext(items) is clearer for navigation. |
contextItems | array | Supplies one-time context for the next message. |
message | object | Prepares or automatically sends a message. |
When dialogId is supplied, the widget selects that dialog; the ID must be a non-empty string of no more than 200 characters. startNewDialog: true creates an empty dialog, while message.startNewDialog: true sends without the current dialog_id. If dialogId and either form of startNewDialog: true are supplied together, the new dialog wins, so do not combine those intentions in one call. Do not treat autoExecuteCustomActionNames as persistent permission: supply it again in each scenario that needs automatic execution. Matching buttons are hidden; the widget automatically executes only the first matching action in a response.
autoExecuteCustomActionNames accepts no more than 20 unique names. Each name follows the same rules as a custom action name: 1–120 characters, starting with a Latin letter, followed by Latin letters, digits, _, ., :, or -. A duplicate name makes the configuration invalid. List only declared actions: a name without a corresponding customActions entry cannot invoke a site handler.
message accepts { text, requestId?, startNewDialog?, autoSend? }. Text must be a non-empty string up to 10,000 characters. requestId must be a non-empty string up to 200 characters. autoSend: true sends after the widget becomes ready; false or omission only places text in the input. Omit requestId for that draft-only case: result events are intended for automatic sending, and the loader reports a failure if the visitor does not start the request within 10 seconds.
An unknown runtime key, invalid type, or out-of-range value causes a synchronous call error. In production code, pass only fields from this table and catch errors around dynamically assembled configuration.
In an SPA, do not call destroy() and init() again on every transition. Update the page through setPageContext(items) and use open({ contextItems, message }) for a targeted scenario.
Collapsing an embedded widget
Popup mode already has a close button in its header. It hides the window, and the floating button reopens the same instance.
In embedded mode, enable shell.collapse_button: true when the user needs a collapse button in the header. The button only tells the site that the user wants to collapse the widget; the site calls SenlerWidget.close() or closes its outer panel.
SenlerWidget.init({
channel_id: "xxx",
display_mode: "embedded",
container: "#senler-widget",
shell: {
collapse_button: true,
},
onCollapse(detail) {
console.log("The user requested collapse", detail);
SenlerWidget.close();
},
});
Instead of onCollapse, subscribe once to the event:
window.addEventListener("senler-widget:collapse-request", (event) => {
if (event.detail.display_mode === "embedded") {
SenlerWidget.close();
}
});
detail contains channel_id and display_mode. If both the callback and event listener are configured, both handlers run. Normally choose one to avoid collapsing twice.
CLOSE_WIDGET and COLLAPSE_WIDGET are internal iframe protocol messages. The site must not send them through postMessage.
Mobile gesture
shell.mobile_edge_swipe: true enables a left-edge swipe inside the widget. The loader does not close the interface; it dispatches senler-widget:mobile-edge-swipe.
window.addEventListener("senler-widget:mobile-edge-swipe", (event) => {
if (event.detail.side === "left") {
closeMobilePanel();
}
});
detail contains channel_id, display_mode, and side: "left". This is a navigation signal for the site, not a replacement for the collapse button.
Runtime message result
When message.requestId is provided, the loader dispatches senler-widget:runtime-message-result. It links the site's request to message delivery and response completion without polling history.
const requestId = crypto.randomUUID();
window.addEventListener("senler-widget:runtime-message-result", (event) => {
if (event.detail.request_id !== requestId) return;
if (event.detail.status === "message_sent") {
console.log("Dialog", event.detail.dialog_id);
}
if (event.detail.status === "answered") {
console.log("The answer is ready");
}
if (["message_send_failed", "message_answer_failed", "preview_failed"].includes(
event.detail.status,
)) {
console.error(event.detail.error_message);
}
});
SenlerWidget.open({
message: {
text: "Make this text clearer.",
requestId,
startNewDialog: true,
autoSend: true,
},
});
Possible statuses:
| Status | Meaning |
|---|---|
accepted | The automatically sent message configuration was accepted by the widget, but sending has not started yet. |
sending | Sending started. |
message_sent | The message was sent and dialog_id is available. |
answered | The answer completed. |
message_send_failed | The message could not be sent. |
message_answer_failed | Answer generation failed. |
preview_failed | A preview could not be built for the associated edit flow. |
detail always contains request_id and status; dialog_id and error_message are included where applicable. The loader monitors two separate stages: the iframe has 10 seconds to become ready and accept the request; after delivery, the widget has another 10 seconds to report that sending started. A timeout at either stage produces message_send_failed. Open the dialog with SenlerWidget.open({ dialogId }) or SenlerWidget.selectDialog(dialogId).
For text editing, use the inline controller: it tracks requestId and returns a ready preview.
Credit top-up request
When Offer additional credits is enabled in channel settings, the Add credits button sends an internal message from the iframe to the loader. The loader validates the origin, iframe source, and channel, then dispatches the safe senler-widget:credit-purchase-requested event on window:
const expectedChannelId = "xxx";
window.addEventListener("senler-widget:credit-purchase-requested", (event) => {
if (event.detail.channel_id !== expectedChannelId) return;
openCreditPayment({
channelId: event.detail.channel_id,
leadId: event.detail.lead_id,
});
});
event.detail contains the validated channel_id and lead_id. The event only reports the user's intent: it neither processes payment nor changes the credit balance. The website must still verify that channel_id belongs to its integration and then open its own payment form.
Grant credits after payment
After payment is confirmed, the website backend must grant the purchased credits to the lead with a separate server-to-server request:
POST /api/projects/{projectId}/leads/{leadId}/credits
Authorization: Bearer senler_sk_...
Content-Type: application/json
{
"credits": 50000,
"type": "purchase",
"reason": "Payment for order shop-order-123",
"idempotency_key": "widget-credit-purchase:shop-order-123"
}
Put your integration's projectId in the request path and take leadId from the event. Before opening payment, verify that event.detail.channel_id matches this integration's channel. The project API key must belong to the same project and have the can_manage_leads permission. Keep the key on the backend only: never put it in loader configuration, page JavaScript, or browser network requests.
The credits field accepts an integer number of minimal credit units: one credit displayed to the user equals 10,000 units, so 50,000 grants 5 credits. The price and order contents remain website data and are not sent in this request.
Always reuse one stable idempotency_key for the same paid line item. After a network error, the request can be safely retried with the same body and key; using a new key for the same item grants the credits again. The grant changes only the selected lead's extra balance and does not top up the project's credit balance.
After the grant, the widget receives an update through its existing realtime connection, removes the block, and refreshes the balance. No additional browser request or polling is required.
Callbacks and Events
SenlerWidget.init accepts:
contextProvider()— synchronously returns persistent page context; Promises are not supported;onCollapse(detail)— reports a click onshell.collapse_button.
customActions buttons use each action's handler, not a shared callback.
window event | When it is dispatched |
|---|---|
senler-widget:collapse-request | The user clicked the collapse button. |
senler-widget:mobile-edge-swipe | The user completed an enabled left-edge swipe. |
senler-widget:credit-purchase-requested | The user requested additional credits; detail contains channel_id and lead_id. |
senler-widget:runtime-message-result | A runtime message was accepted, completed, or failed. |
senler-widget:stage | A diagnostic loading, connection, sending, history, or file stage changed. Use it for observability, not business logic. |
senler-widget:stage always includes area, phase, and timestamp in detail; it may also include attempt, duration_ms, and a final result: "success" | "error" | "timeout".
area | Possible phase values |
|---|---|
loader | instance-created, iframe-created, iframe-loading, iframe-loaded, app-mounted, ready-timeout, error |
bootstrap | loading, success, error, timeout, interactive |
dialogs | idle, loading, refetching, success, empty, error |
history | idle, loading, refetching, loading-more, success, empty, error |
realtime | disabled, token-loading, connecting, connected, reconnecting, disconnected, error |
message | optimistic, sending, queued, sent, failed, waiting, typing, streaming, done, error |
upload | requesting-url, uploading, confirming, ready, error |
This is a diagnostic stream, not the final state machine of a business process. The stage set may expand and individual phases may repeat or be skipped, so business logic must not depend on their exact sequence.
Actions in an embedded application
Regular pages only need element markup. For a separate application with its own interaction protocol, pass pageElementActions in init. This is a local website adapter, not an MCP tool; it cannot be changed through updateRuntime.
execute(payload)receivesevent_id,attempt_id,action,targetortarget_chain, andvaluefor filling or selection. A target containscontext_id, an optionalrole, and anentity_type/entity_idpair for a specific entity.- Return
nullif the target does not belong to the application: the loader will handle it. For your own target, return a result with the sameevent_id,attempt_id, andaction, anexecuted_attimestamp, and a status ofsuccess,not_found,blocked, orfailed. Includeerror_codeand a clearerror_messagefor a rejection. - A Promise result is supported. The adapter must finish within 2.5 seconds. After an exception, invalid result, or timeout, the loader reports an error and does not retry through another executor. Never discard target constraints: return
blockedif the application cannot select a specific entity. clear(scope)clears adapter highlights:toolrefers to agent guidance,selectedto the user-selected element, andallto both. The loader clears highlights on a new action, guide dismissal, and instance destruction.
SenlerWidget.init({
channel_id: "YOUR_CHANNEL_ID",
pageElementActions: {
async execute(payload) {
const target = payload.target ?? payload.target_chain.at(-1);
if (!target?.context_id.startsWith("my-app.")) return null;
return appBridge.execute(payload);
},
clear(scope) {
appBridge.clearHighlights(scope);
},
},
});
The application developer implements appBridge; it is not a Senler method. Its execute must preserve the complete target and return the result format described above. Do not add a second PAGE_ELEMENT_ACTION listener alongside it: the loader already accepts the command and sends its result.
When forwarding an action to child iframes, the loader waits for one frame's response before contacting the next. Only not_found allows the search to continue; success, rejection, or failure ends the attempt. The child-frame search has a total timeout of 2.5 seconds. If the current frame has not replied by then, the result is failed with child_frame_action_timeout, without repeating the action in another frame. A timeout does not prove that the action did not happen: check the application state before retrying.
Related pages
- Initialization Parameters — placement mode, user data, theme, and features.
- Page Context — persistent and one-time message data.
- Custom Actions — buttons handled by the site.
- Inline Edits — AI text editing with a preview.