Sensever widget: installation and setup
A complete developer guide: installing the widget on a website, choosing World/RU, language and appearance, your own button, JavaScript events and Yandex Metrica goals. The widget runs inside an isolated window, while public events are available to the website page.
The same code is also a free analytics pixel: it records page views and active time of every visitor for the "Web analytics" section. What it collects, consent and funnels: Web analytics: website visitors, the Sensever pixel and data collection.
Quick start
Open "Projects", select a project and add a site under "Sites and installation".
Check the exact HTTPS address: https://example.com and https://www.example.com are different origins. Add the addresses you need to the site settings.
In the "Widget code" block, choose WORLD or RU and copy the code for exactly the selected site.
Paste one copy of the code before the closing </body> tag. In an SPA, install it once in the shared shell.
Open the site, choose "Website chat" and send a test message. A request should appear for the project team.
The public key w1.… in HTML isn't a secret. In the examples below, replace w1.YOUR_PUBLIC_KEY with your own key. Don't use another site's key, and never put s1.… server keys into browser code.
World: standard installation
<script>window.SenseverWidget=window.SenseverWidget||{q:[]};['open','close','toggle','identify','track','reset','consent'].forEach(function(m){SenseverWidget[m]=SenseverWidget[m]||function(){(SenseverWidget.q=SenseverWidget.q||[]).push([m,[].slice.call(arguments)])}});</script>
<script src="https://sensever.com/widget/loader.js?v=events-20260922"
data-site="w1.YOUR_PUBLIC_KEY"
data-lang="auto"
async></script>World loads the widget through .com. The ready-made code from the project settings is better than assembling it by hand: it already contains the selected site's key. The first line is a small stub: SenseverWidget exists from the start of the page, and calls made before the widget has loaded wait in a queue (see "JavaScript API"). Code copied before 15.10.2026 has no stub; copy the current code to get it and the new events.
RU: automatic choice of an available address
RU is a mode with a fallback connection. It doesn't detect the country or a VPN, and it doesn't force the browser to use .ru. The loader, embedded directly in the page, tries sensever.com and sensever.ru; the first script to load starts the single widget.
Choose RU above the code in the project settings and copy the whole <script>…</script> block; it carries the same stub as the World code. Don't replace it with a plain link to .ru: that would remove the fallback connection. For your own button, take the RU code from the expandable "Your own button instead of ours" block.
If the widget window hasn't confirmed it's ready within 12 seconds, the loader tries the second address. The successful address is remembered; on the next load it gets a 600 ms head start. A chat that's already running doesn't switch between addresses automatically.
Guest browser sessions on .com and .ru are separate: if the address has to change, automatic transfer of guest access isn't guaranteed. The server and project are shared. Don't count a route change as a new lead: use the events below for goals.
Interface language
The ready-made code uses auto: the language is chosen from the browser language. For Russian, set data-lang="ru"; for English, data-lang="en". If the parameter is missing or the language isn't supported, English is used. The HTML page's language doesn't override this parameter on its own.
In the RU code, find the line script.dataset.lang = 'auto'; and replace auto with the code you need, for example ru. RU refers to the connection mode, not to a Russian interface language.
Supported: ru, en, de, fr, es, it, tr, pt, pl, nl, uk, az, kk, uz, zh, ja, ko, hi, bn, id, vi, th. On a multilingual site, pass the locale chosen by the user when installing. Messages, the project name and custom texts set by the owner aren't translated automatically.
Appearance and installation parameters
Parameter | Value and purpose |
|---|---|
data-site | Required public site key w1.… |
data-lang | auto or a language code; without the parameter — en |
data-launcher | none hides the standard open button |
data-scheme | light or dark locks the theme; without a value — the system theme |
Color, button position, greeting, sizes and automatic opening are configured in the project. Changes to your site's CSS don't affect the widget's internal markup: the button is isolated in Shadow DOM, and the chat is in an iframe. Don't tie analytics to internal CSS selectors; use the public events.
Your own button instead of the standard one
In the project settings, expand "Your own button instead of ours" and copy the code with data-launcher="none". In the RU variant, this parameter is already passed to the standalone loader. Don't install the regular and the alternative code at the same time.
<button type="button" id="contact-support" disabled>Contact us</button>
<script>
const button = document.getElementById('contact-support');
button.addEventListener('click', () => window.SenseverWidget?.open());
window.addEventListener('sensever-widget', ({detail}) => {
if (detail.event === 'ready') button.disabled = false;
});
</script>
<!-- After the handler, paste the widget code for your own button. -->Register the ready handler before the widget code. A click on your button calls the API, so the source of the open event will be api. In the Metrica example below, api opens are counted together with manual ones; if your code opens the widget automatically, exclude api from that goal.
JavaScript API
Call | Result |
|---|---|
SenseverWidget.open() | Opens the window |
SenseverWidget.close() | Closes the window |
SenseverWidget.toggle() | Toggles between open and closed |
SenseverWidget.isOpen() | Returns true when the ready window is open |
SenseverWidget.identify(…), track(…), reset() | Who the visitor is and what they did, see the next section |
SenseverWidget.consent(true | false) | The visitor allowed or withdrew analytics, when the site requires consent |
SenseverWidget.activityId() | The visitor number a1.… for your server, or null (no consent yet, or before the widget has loaded) |
With the current installation code, open, close, toggle, identify, track, reset and consent can be called from the first line of the page: until the widget script has loaded, the calls wait in SenseverWidget.q and then run in order. isOpen() and activityId() return values, so call them after the ready event. With older code without the stub, the object appears only after the script loads; use ready or check window.SenseverWidget. Once the widget is loaded, you can call open() before the window is fully ready — it opens as soon as it's ready. Calling open() again on an already open window doesn't create a second open event.
Visitor and site events: identify and track
Your site can tell the widget who the visitor is and what they did. This fills auto-message texts ("Hello, {name}!"), drives the "Event on the site" trigger, shows the team a "From the site" block in "About the request", and puts the events into the visitor's history in "Web analytics".
Call | What it does |
|---|---|
SenseverWidget.identify({id, name, email, phone, ...your fields}) | Remembers the visitor: the id on your site (up to 200 characters), name, email, phone and up to 20 fields of your own. Each call replaces the previous data |
SenseverWidget.track(name, props) | An event: name matches |
SenseverWidget.reset() | Forgets the visitor's identify data on this device (call it when the user signs out of your site) |
SenseverWidget.activityId() | The visitor number a1.… to pass to your server for server events, or null |
Field and property names match ^[A-Za-z0-9_.-]{1,64}$; values are a string up to 500 characters, a number or true/false. Nested objects and arrays are dropped. Your own identify fields can also be passed as fields: {…}. Phones are stored as digits, emails in lowercase.
<script>
SenseverWidget.identify({id: 'u-42', name: 'Ivan Petrov', email: 'ivan@example.com', fields: {plan: 'pro'}});
SenseverWidget.track('cart_abandoned', {total: 129, items: 3});
</script>With the current installation code these lines work anywhere on the page, before or after the widget code. If your site still has your own stub with a q queue from an earlier version of this guide, it keeps working; you can remove it once the installation code is updated.
This is browser data, not a sign-in. Any visitor can call identify with someone else's email. The team sees such fields marked "Sent by the visitor's browser and not verified", and they never open a conversation or history. To make the data confirmed, sign it on your server: user_hash = HMAC-SHA256 of the id (or the email when there is no id) with the key = SHA-256 of the site's server key s1.… in hex. The server key is issued in "Events and API" of the site's widget (it is the same key as in "Comments on your site"); a new key makes old signatures invalid.
$userHash = hash_hmac('sha256', $user->id, hash('sha256', getenv('SENSEVER_SERVER_KEY')));Pass it to identify: SenseverWidget.identify({id: 'u-42', user_hash: '…'}). The server key never reaches the browser — only the signature does.
Where the data goes. identify is personal data, so it is kept only by the chat: the widget keeps it in your site's browser storage until reset() and the chat saves it for the visitor once they have a widget conversation (the last call replaces the previous data). It never goes into analytics. track() events go into the visitor's history in "Web analytics" (only the name and properties), on every page except those where a "Don't record analytics" rule applies. A signed identify also links the visitor's devices into one person in "Web analytics".
Triggers and visitor data in the project
In the site's "Widget" section, the "Triggers" block shows what your site has told the widget: every event sent with track() — how many times in 30 days and when last — and the visitor data you can use (language, name, email, phone, id and your own identify fields). Events appear there on their own after the first send; you can also add one in advance with a description, and each has a ready line of code to copy.
In an auto-message, the "Event on the site" trigger takes its event from this list. In "Who", every widget trigger can have visitor data conditions: language equals ru, email is known, plan is not free, orders greater than 2. A message is shown only to visitors who match all of them.
To reach a person outside the site — after a payment in your shop or from your CRM — use a Sensever bot: Sensever bots.
Events from your system: API and webhook
Your server, CRM or shop can tell Sensever about a person's event: an order paid, handed to delivery, a form submitted. Auto-messages with the "Event from your system" trigger answer it in the widget the next time the person opens the site, or in their request. Since 14.10.2026, the same event also appears in the person's history in "Web analytics". Addresses, keys, a curl example and the log of the last 50 events are in the "Events and API" block of the site's widget.
POST https://chat.sensever.com/api/sites/events
Authorization: Bearer s1.YOUR_SERVER_KEY
Content-Type: application/json
Idempotency-Key: order-1001-paid
{"user": {"id": "u-42", "email": "ivan@example.com", "activity_id": "a1.Xy3...22 characters"},
"event": "order.paid",
"props": {"order_id": "1001", "total": 129},
"occurred_at": "2026-10-14T12:00:00Z"}user: at least one of id (up to 200 characters), email, phone, activity_id (the page's SenseverWidget.activityId()). event:
^[a-z0-9_.-]{1,64}$. props: up to 20 simple values. occurred_at: optional.Idempotency-Key (or an event_id field), up to 200 characters: a repeat doesn't create a second event and answers 200 with the same id. A new event answers 202 with id, status (matched — the person was found, pending — not yet, the message waits 7 days; no_messages — no matching auto-messages), deliveries and activity (sent — added to the person's history in "Web analytics", skipped — nobody to attach it to).
Errors: an invalid body 422 with the field (shown in the log as rejected), a wrong key 401, a request from a browser (with an Origin header) 403, more than 600 events a minute per site 429.
The person is found by a signed identify on your site with the same id, email or phone, or by a WhatsApp contact of the project with that phone. An unsigned identify never matches: otherwise anyone could claim someone else's email and receive their messages.
Incoming webhook. For Tilda, shops and CRMs that can only post a form or JSON: "Events and API" → "Issue a webhook address". The address contains a secret and is shown once; a new address switches the old one off. The body is flat: email (Email), phone (Phone, tel), id (user_id, customer_id) for the person; event in the body or as ?event=… in the address; up to 20 other simple fields become the event's properties.
POST https://chat.sensever.com/api/hooks/site-events/{site}/{secret}?event=form.sentHow server events appear in "Web analytics", and which properties are removed before they get there: Web analytics.
Built-in triggers and messenger delivery
Three more triggers in "When to send": "Visitor came back" (a returning visitor after N days; a bubble or the open window), "Request closed" (right after closing — for example, to ask for a rating) and "Customer hasn't replied" (the customer has been silent for N hours). Messages of the request triggers go into the request itself: the widget chat or the messenger it came from; a closed request stays closed until the person replies.
WhatsApp only lets a business write first within 24 hours of the person's last message. Outside the window the message goes as the approved channel template chosen in the auto-message, with its variables filled from placeholders; without a template the delivery is skipped and the reason is shown in the list.
Site rules: messages, chat display and analytics
In the project, the "Rules" tile lists the rules of the chosen site. Each rule has a trigger, conditions and one action: "Send a message" (an auto-message), "Show or hide the chat" or "Site analytics". The filters above the list are All, Messages, Chat display and Analytics.
Show or hide the chat (trigger "Page opened"): "Hide chat" removes the button, invitations and bubbles on matching pages, and SenseverWidget.open() does nothing there; close() still works. Page history, identify and track keep working. If you use your own button, hide it yourself on those pages.
Site analytics (trigger "Page opened"): "Don't record analytics" sends nothing from matching pages to Google Tag Manager or gtag, and the pages are not saved in the visitor's history in "Web analytics". The sensever-widget event on window still arrives, with analytics: false. "Record analytics" can't turn on what is off in the widget settings or with data-analytics="off".
Conditions: pages (all, only, or all except; the address contains, is, or starts with; a value starting with "/" is a path, otherwise the whole address; the part after "?" is not checked), first or returning visit, team online or not, device (phone: a site window 600 px wide or less) and visitor data (widget language and identify fields).
Order: without rules the chat is shown and analytics is recorded everywhere. Among matching chat or analytics rules the last one in the list wins, so a general rule goes above its exceptions; among messages the first matching one is shown. A switched-off rule doesn't apply. Rules are checked on every page load, on every navigation inside a single-page site, after identify or reset, and when the team status changes.
Visitors of your site in Sensever (the pixel)
On every page where it is installed, the widget code records page views and active time of all visitors, not only of those who write to the chat. The project team sees them in "Web analytics" in the left panel: people, visits, pages, sources and funnels, and a request shows the visitor's visits in "About the request". It is free on every plan.
No cookies. By default the visitor number a1.… is kept in your site's localStorage.
With "Collect visitor statistics only after the visitor's consent" on in "Widget settings", visitors are counted without cookies until SenseverWidget.consent(true) or a Google Consent Mode analytics_storage: granted signal.
Not recorded: pages with a "Don't record analytics" rule, and any page while "Show widget" is off. data-analytics="off" only stops events going to your Google Tag Manager or GA4; it doesn't stop the pixel.
The full description — what is collected and what is not, consent, track() from Google Tag Manager, server events, funnels and retention: Web analytics: website visitors, the Sensever pixel and data collection.
Public widget events
The available events are ready (ready), open (opened), close (closed), channel_select (channel chosen) and first_message_sent (first confirmed message). Listen for a single sensever-widget event on window. The subscription works the same with World, RU and your own button. It doesn't need access to the iframe and doesn't depend on which domain the loader chose.
window.addEventListener('sensever-widget', ({detail}) => {
console.log('Sensever:', detail.event, detail);
});detail.event | When it fires | Additional fields |
|---|---|---|
ready | The widget interface is ready | — |
open | A closed window opened | source: manual, api, auto or restore |
close | An open window closed | source: manual or api |
channel_select | A contact method was chosen | channel: web, sensever, telegram, max, whatsapp or instagram |
first_message_sent | The server confirmed the visitor's first message in the request | A stable event_id |
Event format
Common fields: version — the contract version (1); event — the name; event_id — the event ID; site_key — the public key of the installed widget; timestamp — the event time in ISO 8601 format. Events don't contain message text, attachments, names, contacts, tokens or internal conversation IDs.
Manual opening, auto-open and restore
manual means a click on the widget's standard button or the close button inside the window. api means your code called open(), close() or toggle(). auto is opening on a timer set by the project owner. restore is restoring an open window when navigating to another page in the same tab.
For a "Visitor opened the widget" goal, you usually count manual and api calls from your own button. It's better to exclude auto and restore, otherwise ordinary navigation around the site will look like new interested opens.
Channel choice and the confirmed first message
channel_select reports that a tile was chosen, including with the keyboard. web is "Website chat", sensever is signing in through Sensever, telegram/max/whatsapp/instagram is going to an external messenger. Which tiles the widget shows and in what order is set in Projects → the "Channels" tile: "Chat on the website" and "Sensever" are rows there next to the messengers, each with "Show in widget", and all rows share one drag order. Choosing Sensever doesn't confirm a successful sign-in; choosing an external messenger doesn't confirm a message was sent there.
first_message_sent applies only to sending inside the web widget. It fires after a successful server response, including a message with an attachment. Clicking "Send", starting a file upload and a send error don't count as reaching the goal. This is the visitor's first message in the entire history of the request, not the first per visit or page; team replies don't count as visitor messages.
A retried request after a lost response gets the same event_id. The widget suppresses a repeated event in the current instance and, when the site's browser storage is available, after a reload. Deleting the first message doesn't make the next one the first. For your own analytics system, also use event_id as the deduplication key. Cleared storage, another device or an analytics blocker make it impossible to guarantee the goal is delivered exactly once; the event isn't a payment or a trusted server webhook.
Google Tag Manager and GA4
You don't need code for Google: turn on "Send widget events to the site's analytics" in "Widget settings", and when the page has Google Tag Manager or the Google tag (gtag.js), the widget passes these events there, as sensever_widget_open, sensever_widget_channel_select and sensever_widget_message_sent. A ready-made container to import, the dataLayer format and how to turn it off: Sensever widget in Google Tag Manager and GA4.
Yandex Metrica: creating goals
Install the Metrica counter on your site. In the "Goals" section, add goals of the "Target event" type (in the old interface, "JavaScript event") with an exact-match condition on the identifier.
Goal identifier | Purpose |
|---|---|
sv_widget_open | Manual opening or opening with your own button |
sv_widget_web | Website chat chosen |
sv_widget_sensever | Sensever chosen |
sv_widget_telegram | Telegram chosen |
sv_widget_max | MAX chosen |
sv_widget_whatsapp | WhatsApp chosen |
sv_widget_instagram | Instagram chosen |
sv_widget_first_message | First confirmed message |
The identifiers in Metrica settings and in JavaScript must match, including case. Replace the counter number below with your own. The Metrica code must create window.ym before the handler; the standard counter code can queue calls until the library loads.
Yandex Metrica: a ready-made handler
Place the handler after the standard Metrica code but before the widget code. In an SPA, add it once in the shared shell, not on every navigation.
<script>
(() => {
if (window.__senseverMetrikaBound) return;
window.__senseverMetrikaBound = true;
const counterId = 12345678; // Your counter number
const seen = new Set();
const channels = {
web: 'sv_widget_web',
sensever: 'sv_widget_sensever',
telegram: 'sv_widget_telegram',
max: 'sv_widget_max',
whatsapp: 'sv_widget_whatsapp',
instagram: 'sv_widget_instagram'
};
window.addEventListener('sensever-widget', ({detail: e}) => {
if (!e || e.version !== 1 || typeof window.ym !== 'function') return;
let goal;
if (e.event === 'open' && ['manual', 'api'].includes(e.source)) {
goal = 'sv_widget_open';
} else if (e.event === 'channel_select') {
goal = channels[e.channel];
} else if (e.event === 'first_message_sent') {
goal = 'sv_widget_first_message';
}
if (!goal || seen.has(e.event_id)) return;
seen.add(e.event_id);
window.ym(counterId, 'reachGoal', goal, {
widget_event_id: e.event_id,
widget_channel: e.channel || '',
widget_source: e.source || ''
});
});
})();
</script>The widget doesn't load Metrica itself and doesn't send it any data. The decision about which goals to trigger and when to allow analytics stays with the site. If the analytics library is blocked, the chat keeps working.
Official documentation: reachGoal, creating a target event, checking goals.
Testing the integration
First register the console.log handler from the events section, then load the widget.
Click the standard button: you should see ready and open with source=manual. Auto-open should have source=auto.
Click each connected tile and check channel_select with the right channel.
Start a new test request and send a message. After confirmation, exactly one first_message_sent should appear. A second message shouldn't repeat the goal.
Check the reachGoal calls with your counter number and goal identifiers. Test requests will be visible to the team as regular requests.
For the RU code, block .com and .ru one at a time in the browser tools: events should come through the remaining route.
An old guest conversation isn't suitable for testing the first message: it already had a first message. Test on a separate test site/project or in a new guest browser profile. Metrica reports may not update right away.
Load errors and diagnostics
The RU loader fires a separate sensever-widget-error event. detail.reason=unavailable means no script loaded; frame-unavailable means the windows at both addresses didn't confirm they were ready. These are startup errors, not errors of an individual message. The regular World code has no such fallback mechanism.
window.addEventListener('sensever-widget-error', ({detail}) => {
console.warn('Sensever widget:', detail.reason);
});If the widget doesn't appear, check the key, the allowed HTTPS origin, whether the widget is turned on, network errors and CSP. If a messenger tile is missing, check the connection in the project: Telegram/MAX need a public bot username, WhatsApp needs a verified number and a webhook, Instagram needs a saved webhook; the channel must have the "Show in widget" checkbox selected. The order of messenger tiles in the widget matches the order of channels in the "Messaging channels" block and is changed by dragging. If the event fires but the goal doesn't, check the counter number, the exact goal name and that Metrica loaded.
CSP, mirrors and safe embedding
Allow the script address in script-src and the window address in frame-src. RU needs both addresses: https://sensever.ru and https://sensever.ru. If you use chat.sensever.com, allow exactly that address. The RU inline loader and your analytics handlers must be allowed by your CSP nonce or hash; the nonce must also be passed to the script elements the loader creates, or you can use a suitable strict-dynamic policy. Don't weaken the whole site's protection for the sake of installation.
The public events API doesn't require permission to read messages. With your own postMessage handlers, check origin and source; for goals, use the ready-made sensever-widget event — the loader already checks the sender window and passes through only the allowed fields. Any event in the browser can be faked by the page's code, so don't grant money, access or privileges based on it.
Guest access, history and team work
Messages are stored on the server; ending a guest session doesn't delete the history for the team. Guest access is granted for 45 days and, with activity, is extended to 45 days when fewer than 30 days remain. It's tied to browser storage; clearing data, private mode or browser restrictions can prevent a guest from restoring access. Signing in through Sensever lets the person continue the conversation with their account on another device.
How long visitor data is kept
Conversations (requests, messages, files) are not affected by these terms and are not deleted automatically.
Visitor behaviour (pages, browser, language, source, UTM, track events) is kept for 13 months; the full IP address for 30 days, then only its network part; visits counted without cookies store no IP address at all.
identify data (name, email, phone, id on your site, your own fields) is kept only in the chat, with the visitor of the conversation, and is removed together with the visitor or the site.
A guest who never wrote a message is removed together with their sessions a day after guest access expires (45 days). A guest who wrote something is not removed.
The back arrow returns to the channel choice, keeping the current conversation and draft. Going to a messenger doesn't move the guest conversation automatically. Agents reply in "Requests". Quiet mode and hiding from the main list are an agent's personal settings; they don't turn off receiving messages or events on the customer's site. Read status for requests is shared by the team, while in groups it's personal.
Comments and a private CRM
The support chat and comments are different widgets. The events in this guide apply to the support chat and don't add analytics to comments/loader.js. Below are the instructions for private comments and server-side access grants.
Installing private comments in a CRM or on a private site
This is a separate embedded block for discussing an article, product or task. It stores messages in a regular Chat context thread. Private means access only for admitted members, not mandatory registration in Sensever. Your platform checks access to the page and the object; Chat checks the permission it issued and membership in the thread. An outsider can't read a discussion by learning its ID.
Project settings
In "Projects → Sites and installation → Comments on your site", turn on comments and choose "Private" for new discussions. Issue a server key and save it in your server's environment. The s1.… key is shown once; it must not be placed in HTML, client-side JavaScript or a URL. The public w1.… key can go in the widget code. The allowed HTTPS origin must exactly match the page address.
The mode affects only new discussions. An existing public thread doesn't become private automatically; an attempt to connect to it privately returns 409.
How the CRM grants access
Create a handler on your site, for example POST /api/sensever-comments/session. It:
Checks your CRM user's session and CSRF.
Uses
context_typeandcontext_external_idto check the right to read the specific object.Determines the user's persistent ID, name and write permission on its own. It doesn't trust these values from the browser.
Sends a server request to Chat:
POST https://chat.sensever.com/api/comments/private/session
Authorization: Bearer s1.YOUR_SERVER_KEY
Content-Type: application/json{
"origin": "https://crm.example",
"context_type": "task",
"context_external_id": "123",
"user_id": "crm-user-42",
"name": "John",
"can_write": true,
"expires_in": 300
}Chat returns token, expires_in, thread_id, visibility and can_write. Pass only token and expires_in to the browser, with the Cache-Control: no-store header. The token applies to one site, origin, user and discussion. The TTL is 60–900 seconds. Each refresh request requires checking the session and rights in the CRM again. Local IDs of different sites are isolated; an external server can't point to an arbitrary Sensever account.
Code on the page
<script src="https://chat.sensever.com/comments/loader.js?v=2"
data-site="w1.YOUR_PUBLIC_KEY"
data-entity-type="task"
data-entity-id="123"
data-session-url="/api/sensever-comments/session"
data-lang="ru" async></script>data-session-url is the address of your handler on the same origin. The widget calls it automatically and refreshes the token. The user doesn't sign in a second time or configure rights in Sensever. For CSP, allow https://chat.sensever.com in script-src and frame-src. The token is passed to the iframe via postMessage with window and origin checks, not in the URL.
Membership, revocation and a Sensever account
It's the widget session that expires, not the membership. Closing the page doesn't remove the member or the history. To remove a person, the CRM server sends the same request with "revoke": true: this revokes their tokens and membership in that discussion. When someone is removed from the CRM or their access changes, the platform must call revocation explicitly. A new permitted request can add the member again.
To continue the conversation inside the Sensever app, an external member needs a verified link to an account. This endpoint doesn't create such a link and doesn't require an account to work on the site. Flow uses its own trusted Accounts mapping. Project owners and administrators can moderate their project's discussions.
Comment editor
The comments widget uses the shared Chat editor: a paperclip, file picker and preview, a growing input field and a send button that appears when there's text or an attachment. Enter sends, Shift+Enter adds a line break. Attachments go through the standard UploadService/MessageService and are displayed by the same component as in chat. In private discussions, uploading is available only to members with the right to send.
Discussions and the source page
In chats, the "Discussions" tab after "Requests" contains the member's context threads, with their own icon and unread counter. In a discussion's header and in "Info", "Open page" leads to the widget's page; "Info" shows its title and URL. The site continues to check access to the page itself.
Persistent key: the project site (site_id) + context_type + context_external_id. The URL and title are NOT part of the key. Re-registering/initializing with the same key updates the metadata of the same thread, keeping messages and members. Metadata omitted from a private session isn't cleared.
Protection against copying and false renaming: the public w1 isn't a secret. allowed_origins restricts embedding by the browser (origin check + CSP frame-ancestors). Only the server secret s1 allows changing the canonical URL and title. The key is stored on the server, not in HTML, a URL or the browser. The public frame/guest API ignores client page_url/page_title for changing the thread. HTTP Origin can technically be forged, so it doesn't replace the secret key.
For public mode, the site server calls this before rendering the page (or when creating/changing it):
POST https://chat.sensever.com/api/comments/context
Authorization: Bearer s1.YOUR_SERVER_KEY
Content-Type: application/json
{"origin":"https://site.example","context_type":"article","context_external_id":"123","page_url":"https://site.example/articles/123","page_title":"Article title"}The response is {thread_id}. The endpoint creates a public thread if there isn't one or updates the existing one. The secret is bound to a specific site: you can't pass another site's site_id. page_url must be HTTPS, with the same host and port as the allowed origin; page_title is up to 255 characters, without control characters. Requests with a browser Origin, w1 instead of s1, or a foreign origin or URL are rejected. The server takes the URL/title from its own data rather than blindly forwarding them from the browser. HTML is escaped. Public reading stays public; this protection doesn't make a public discussion private.
For private mode, the server passes page_url and page_title along with the access grant in POST /api/comments/private/session. Creating a private discussion requires this grant; /context doesn't create one by itself. Flow sends the task name and builds its URL automatically through a trusted S2S call. Public data-page-* attributes can be used by your handler as a hint, but they don't change the shared metadata by themselves. Comments without server-side metadata registration work with a neutral title and no backlink.
Bình luận