This is the full developer documentation for Unless Docs # Conversations > Access bulk conversation content and metadata organized by account, website, session, and conversation. The conversations data API gives you direct access to the bulk content and metadata of your user's conversations. You can use this data in your own BI system for analyis. ## Access The conversation data is stored in a S3 bucket. You can get direct access to the bucket using a access key and secret key. Contact your Unless account manager if you'd like to get access. ## Storage Each conversation will be stored at a key with the following schema "_/account-id/conversations/website-id/session-id/conversation-id.json". Therefore, if you would like to bulk download all of the conversations associated with a given website, you'd download all of the session directories from "account-id/conversations/website-id". Each conversation object is regularly updated as users interact with the AI system. ## Structure Each file contains the following fields: | Field | Type | Notes | | :------------------ | :----------------------- | :----------------------------------------------------------------------------------------------------------------- | | chatHistory | ChatHistory | The ChatHistory is a serializable array of Log objects (see below) | | conversationName | string \| null | A string representing a human-readable name for the conversation | | createdAt | number (optional) | A timestamp representing when the conversation was created (May be absent on old data) | | history | Log[] | Array of log objects (see ChatHistory) | | metadata | Record\ | Free-form session metadata | | personalizationId | string | Tracking ID from the component | | sessionId | string | A unique identifier corresponding to a group of conversations had by one user without closing their browser window | | startingMessageSent | boolean (optional) | When true, the starting message has already been sent to the user in this conversation | | websiteId | string (optional) | The unique identifier of the workspace that the conversation is associated with | ### Log Objects | Field | Type | Notes | | :------------- | :------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | | type | string | A string describing the type of log, e.g. 'message', 'error', 'input' | | sender | 'bot' \| 'user' \| 'support' | The entity which sent the corresponding log item | | timestamp | number | The time at which the log item was created | | uuid | string | A unique identifier for the log item | | hidden | boolean | When true, this log item won't be rendered for the user in the chat window | | historical? | boolean \| null | When true, this log was from a previous conversation but added to create the illusion of continuity. | | local? | boolean \| null | Frontend-only, not sent to backend | | segmentId? | string \| null | This is the unique identifier corresponding to the topic that the user is in at the time the log item is created. | | messageFormat? | 'md' \| 'html' \| string \| null | This determines how the log item's content should be rendered | | parentId? | string \| null | Groups logs into Q&A pairs | | isLiveChat? | boolean \| null | When true, this log item was created while the user was in live chat mode (i.e. speaking to a customer support agent rather than chatbot) | # Events > Access raw analytical event data from S3 as hourly JSONL.gz files. The events data API gives you direct access to the raw analytical event data. You can use this data in your own BI system to analyze. ## Access The event data is stored in a S3 bucket. You can get direct access to the bucket using a access key and secret key. Contact your Unless account manager if you'd like to get access. ## Storage The logs will be stored in S3 with a prefix like "_/account-id/use-case-id/datetime.jsonl.gz_". Every hour we will add a new file with all new events since the last run. You should pull in the data into your own data warehouse. The files are stored in the JSONL format suitable for streaming it into BI systems. It is stored with gzip compression to save both storage and data bandwidth. You probably want to decompress it before storing it in your own solution. ## Structure Each file contains the following fields: | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | accountId | string | Your account id | | domainName | string | Domain name where the event was captured | | userId | string | ID of the user, either generated by us, or set as the identifier by you | | pageVisitId | string | Unique generated id of the page visit | | sessionId | string | Unique generated id of the session | | eventType | string | See event types below | | timestamp | string | ISO 8601 timestamp | | metadata | json | Available metadata depending on the eventType. If we didn't manage to find the answer then this contains: "questionIsUnanswered: true" | | origin | string | Either 'component' or an API form | | componentId | string | ID of the component that emitted the event (only if origin is component) | | componentName | string | Name of the component as named in the Unless dashboard | | componentTags | string | Tags set on the component in the Unless dashboard | ### Event types | Event Type | Description | | ------------------------------------------ | --------------------------------------------------- | | display | When a component is displayed | | close | When a component is closed | | primary-cta-click | When a primary CTA is clicked in a component | | secondary-cta-click | When a secondary CTA is clicked in a component | | chat-conversation | When a conversation is started | | chat-conversation-without-starting-message | When a conversation is started with a prompt | | chat-starting-response | Starting message got displayed | | chat-response | AI response is generated | | chat-negative | Thumbs down is pressed | | chat-positive | Thumbs up is pressed | | chat-escalated | An action was invoked that was marked as escalating | | chat-search | A search is triggered on the search tab | ### Computed properties on insights page On our insights page we show a couple of data fields that are computed using the raw data within a specific timeframe. | Property | Calculation | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Responses | Sum of chat-response | | Sessions | Sum of chat-starting-response and chat-conversation-without-starting-message. _Every time a user interacts with the AI by seeing a starter message or an auto-prompt message, it's called a session._ | | Conversations | Sum of chat-conversations | | Searches | Sum of chat-search | | Questions | Sum of chat-responses | | Answered | Sum of (chat-responses - chat-responses where data->>questionIsUnanswered = true) | | Success ratio | Answered / Questions | | Escalations | Sum of chat-escalated | | Tickets deflected | Conversations - Escalations | # Account management for AI agents > How an AI agent can investigate and manage an Unless account through the REST API with an API key. A curated set of Unless REST API endpoints accepts API key authentication, which makes them usable by an AI agent acting on behalf of an account user - no interactive login required. This guide explains how to authenticate, which endpoints are available, and how to chain them to complete common account management tasks. ## Authentication Every request needs four headers: | Header | Value | | -------------- | ---------------------------------------------------------------------------------- | | `x-api-key` | An API key. Both the account-wide key and a personal key from the account settings work. | | `x-user-email` | The email address of the user the agent acts on behalf of. | | `x-account-id` | The account ID, found in the account settings. | | `x-website-id` | The ID of the workspace (website) to operate on. | All endpoints are served from `https://api.unless.com` and prefixed with `/api/v1`. Requests and responses are JSON. Example: ```bash curl https://api.unless.com/api/v1/tasks?websiteId= \ -H "x-api-key: " \ -H "x-user-email: " \ -H "x-account-id: " \ -H "x-website-id: " ``` Notes: - Send either an API key or a JWT, never both. Requests carrying both an `Authorization` header and an `x-api-key` header are rejected. - The permissions of the resolved user apply. A key only reaches the websites its user has access to. ## Endpoint catalog Full request and response details for each endpoint are in the REST API reference. This catalog tells an agent which endpoint fits which job. ### Investigate an account | Endpoint | Use it to | | ----------------------------------- | ---------------------------------------------------------------------- | | `GET /api/v1/tasks` | List the account's work items: knowledge suggestions, detected gaps, flagged conversations, sales signals. | | `GET /api/v1/ai/configuration` | Inspect how the AI is configured: main language, product name, custom rules. | | `GET /api/v1/ai/training` | See which knowledge sources the AI is trained on and how ingestions went. | | `GET /api/v1/audiences` | List the visitor segments defined for the account. | | `GET /api/v1/personalizations` | List the personalizations (experiences) running on a website. | | `GET /api/v1/help-center/publish-status` | Check whether the help center is published, publishing or failed. | | `GET /api/v1/ai/quality/control` | List the control questions the AI is scored against. | | `GET /api/v1/ai/quality/control/report` | List quality control reports, or fetch one report's per-question results with `?reportId=`. | ### Act on an account | Endpoint | Use it to | | ----------------------------------- | ---------------------------------------------------------------------- | | `PUT /api/v1/tasks` | Change a task's state (`open`, `done`, `deleted`), assign it, or edit it. | | `POST /api/v1/tasks` | Create a new task for the account team. | | `POST /api/v1/ai/training/data` | Ingest a website (or another source) so the AI learns its content. | | `POST /api/v1/help-center/publish` | Publish the help center so content changes go live. | | `POST /api/v1/audiences` | Create a new audience, or update an existing one. | | `POST /api/v1/ai/quality/control` | Add or update a control question and its expected answer. | | `POST /api/v1/ai/quality/control/report` | Start a quality control run that scores the AI against every control question. Returns the `reportId` to poll. | ### Ask the AI | Endpoint | Use it to | | ----------------------------------- | ---------------------------------------------------------------------- | | `POST /api/v1/ai/query` | Ask the account's trained AI a question and get an answer in realtime. | ## Chaining endpoints Most real tasks combine a read to establish context with one or more writes, followed by a read to verify the result. ### Work through open tasks 1. `GET /api/v1/tasks?websiteId=...&state=open` to list what needs attention. 2. Investigate a task using the read endpoints, for example `GET /api/v1/ai/training` for a knowledge gap task. 3. `PUT /api/v1/tasks` with `{ "taskId": ..., "websiteId": ..., "state": "done" }` once handled. ### Ingest a website 1. `GET /api/v1/ai/training` to check whether the site is already a source. 2. `POST /api/v1/ai/training/data` with `{ "trainingData": { "accountId": ..., "websiteId": ..., "type": "url", "method": "all", "url": "https://www.example.com", "isPublic": true } }`. 3. Poll `GET /api/v1/ai/training` and watch `indexedUrlsCount` and `lastScanTimestamp` to follow progress. ### Publish the help center 1. `GET /api/v1/help-center/publish-status` to confirm no publish is running (`state.runningJobId` is `null`). 2. `POST /api/v1/help-center/publish` for a full publish, or with a `partial` scope for specific FAQs. 3. Poll `GET /api/v1/help-center/publish-status` until the job's status is terminal. ### Create an audience 1. `GET /api/v1/audiences` to inspect existing audiences and their `rule` format. 2. `POST /api/v1/audiences` with a `name`, a `rule` (JSON string) and a `state`. 3. `GET /api/v1/audiences?ruleId=...` to verify the result. ### Score the AI with a quality control report Generating a report costs one AI call per control question, so it is only available on the Enterprise, Flex, Fixed and Plus plans. On any other plan `POST /api/v1/ai/quality/control/report` answers `403`. 1. `GET /api/v1/ai/quality/control` to see the control questions. A report scores every one of them, so add what is missing with `POST /api/v1/ai/quality/control` first. 2. `POST /api/v1/ai/quality/control/report` to start the run. It answers `400` when there are no control questions yet, and otherwise returns the `reportId`. 3. Poll `GET /api/v1/ai/quality/control/report`. The new report appears in the list once its first question has been scored, with `processed` counting up to `totalQuestionsInReport`. 4. `GET /api/v1/ai/quality/control/report?reportId=...` for the per-question results once `processed` equals `totalQuestionsInReport`. # JavaScript API > Developer guide for enriching visitor data and controlling the Unless AI component programmatically. Using Unless' javascript API your developers can send additional data about visitors and audiences into Unless. This reference documentation for the javascript API makes it possible for our customers to send additional data into Unless. This data allows for more granular audience targeting, enriched visitor data, and more. :::note Please bear in mind that this API documentation is targeted at your developers. ::: # Open the AI component > Open the Unless AI component programmatically using Txt.openChat(). ```javascript Txt.openChat({ conversationName: "AI assistant", prompt: "Who is the CTO of Unless?" }) ``` | Argument | Type | Description | | ---------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | conversationName | string (optional) | A conversation name of this particular chat session. You can use this to separate different conversations. The conversation name will show up in the top left corner of the popover. | | prompt | string (optional) | Generates a hidden prompt that triggers the AI to answer directly to the user. | | promptLabel | string (optional) | If set, this shows as the label in the AI chat instead of the full prompt. | | segments | \{ name: string, id: string, description: string } [] (optional) | Note; a segment is called a **Topic** in the dashboard. If you supply multiple topics here, the user will be forced to choose one of the topics before continuing. | | mode | string (optional) | One of the following: 'sidebar' or 'conversation-center' or 'popover'. Defines if the chat will open in a popover, or in an overlay conversation center. Note: to start a chat in popover mode, a target must be defined. | | target | DOM Element (optional) | A target element on the page that you can attach the AI popover to, usually this is the button the user clicked on. | ## Current AI component state ```javascript Txt.isChatOpen() ``` ## Close the AI component ```javascript Txt.closeChat() ``` When the Ai components gets closed using the close button we throw the following event. ```javascript document.dispatchEvent(new CustomEvent('unless.chat-closed')) // you can listen for this event using document.addEventListener('unless.chat-closed', () => console.log('closed')) ``` # Livechat handover > Hand off Unless AI conversations to a human livechat agent, with a Zendesk integration example. Hand over conversations from the Unless AI assistant to your existing livechat system. When a user requests human support, the AI component triggers a custom JavaScript command that opens your livechat, passes the conversation transcript, and closes the AI chat. ## How it works 1. The AI assistant detects the user wants to speak with a human (based on the Guidance rules you configure). 2. A custom JavaScript command runs in the browser. 3. That script opens your livechat widget, sends the transcript, and closes the Unless AI component. ## Setup ### 1. Create a livechat skill In the Unless dashboard, create a new **AI Skill** for the livechat handover. Select **Custom JavaScript** as the command type. ### 2. Configure guidance Use the **Guidance** tab on the skill to define when the livechat option should be presented to the user — for example, when they explicitly ask for a human agent or when the AI cannot resolve their question. ### 3. Write the handover script Your custom JavaScript needs to do three things: 1. **Open** the livechat widget. 2. **Send the transcript** to the livechat so the human agent has full context. Use `Txt.getChatTranscript()` to retrieve it. 3. **Close** the Unless AI component with `Txt.closeChat()`. ### 4. Hide the livechat floating button Most livechat tools show a floating launcher button by default. Since Unless already provides the entry point, you should hide it. Depending on your provider you can: * Disable the launcher in the livechat tool's own settings. * Use the provider's JavaScript API to hide it on load. * Add custom CSS to hide it, for example: ```css /* Example: hide a livechat launcher by class name */ .livechat-launcher { display: none !important; } ``` ## Example: Zendesk Below is a complete handover script for **Zendesk Messaging**. Adapt it to match your Zendesk configuration. ```javascript // Start a new Zendesk conversation with the AI transcript zE("messenger:ui", "newConversation", { displayName: "Support Chat", metadata: { source: "unless" }, message: { content: { type: "text", text: Txt.getChatTranscript() } } }); // Tag the conversation so agents know it came from the AI assistant window.zE?.("messenger:set", "conversationTags", ["unless"]); // Show and open the Zendesk messenger window.zE?.("messenger", "show"); window.zE?.("messenger", "open"); // Close the Unless AI component Txt.closeChat(); ``` ### Hiding the Zendesk launcher To prevent the default Zendesk button from appearing alongside Unless, add this snippet when you load the Zendesk widget: ```javascript zE('messenger', 'hide'); ``` ## API reference | Method | Description | | ------------------------- | --------------------------------------------------------------- | | `Txt.getChatTranscript()` | Returns the full conversation transcript as a formatted string. | | `Txt.closeChat()` | Closes the Unless AI chat component. | ## Tips * **Test the handover flow end to end.** Verify that the transcript arrives in the livechat and that the agent can read it. * **Set conversation tags or metadata** so your support team can identify handovers from the AI assistant and route them appropriately. * **Keep the livechat script tag on the page** even though the launcher is hidden — the widget still needs to be loaded for the JavaScript API to work. # Open the AI component > Use Txt.openChat() to programmatically launch the Unless AI chat interface. ```javascript Txt.openChat({ conversationName: "AI assistant", prompt: "Who is the CTO of Unless?", }) ``` ## Parameters | Parameter | Type | Description | | ------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `conversationName` | string (optional) | A conversation name of this particular chat session. You can use this to separate different conversations. | | `prompt` | string (optional) | Generates a hidden prompt that triggers the AI to answer directly to the user. | | `promptLabel` | string (optional) | If set, this shows as the label in the AI chat instead of the full prompt. | | `segments` | array (optional) | Note; a segment is called a **Topic** in the dashboard. If you supply multiple topics here, the user will be forced to choose one of the topics before continuing. | | `mode` | string (optional) | One of the following: `'sidebar'` or `'conversation-center'` or `'popover'`. Defines if the chat will open in a popover, or in an overlay conversation center. | | `target` | DOM Element (optional) | A target element on the page that you can attach the AI popover to, usually this is the button the user clicked on. | ## Check if the component is open ```javascript Txt.isChatOpen() ``` ## Close the component ```javascript Txt.closeChat() ``` ## Chat closed event When the user closes the chat via the close button: ```javascript document.addEventListener('unless.chat-closed', () => { console.log('chat was closed') }) ``` # Open the help center > Use Txt.openHelpCenter() to open the Unless AI component on the help center, a category, or an article. `Txt.openHelpCenter()` opens the AI component on its help center. Use it to wire your own buttons and links to the help center. You can call it on any page that loads the Unless script. If the AI component is not loaded yet, it is loaded and shown for you - you do not need to call `Txt.openChat()` first. Calling it while the help center is already open navigates to the requested view instead of closing the component. ## Open the help center index Opens the categories grid. ```javascript Txt.openHelpCenter() ``` ## Open a category Opens that category's article list. ```javascript Txt.openHelpCenter({ categorySlug: "billing" }) ``` ## Open an article Opens the article for a question. Pass `categorySlug` and `faqId` when you know them so the article opens without having to resolve them first. ```javascript Txt.openHelpCenter({ categorySlug: "billing", question: "How do I update my payment method?", faqId: "f1b2c3d4", }) ``` ## Parameters | Parameter | Type | Description | | -------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- | | `categorySlug` | string (optional) | The slug of the category to open. On its own, opens that category's article list. | | `question` | string (optional) | The question of the article to open. Opens the help center directly on that article. | | `faqId` | string (optional) | The id of the FAQ the question belongs to. Optional, and only useful together with `question`. | ## Return value The function returns a promise that resolves once the component is showing its help center, so you can chain on it. A `categorySlug` or a `question` is applied right after that, as the category and the article load - the promise does not wait for them. ```javascript Txt.openHelpCenter({ categorySlug: "billing" }) .then(() => console.log("the help center is showing")) .catch((error) => console.error(error)) ``` The promise rejects if the component never shows the help center, for example when the AI component fails to load or the help center is not enabled on your account. Always attach a `.catch()` so the rejection does not surface as an unhandled error. ## Display behaviour The component behaves exactly as it does when a visitor opens the help center tab themselves. From a popover or a sidebar it promotes into the conversation center, and an inline component stays inline. # Overriding AI component settings > Programmatically override AI component settings before opening the chat using Txt.componentSettingOverrides. You can programmatically override component settings before you start a component. You can do this if you created your own UI button before you use the openChat() function. If instead you use one of our starter button components, then you can use the expert setting in the Button section to add a "custom button click javascript". Here you can paste your settings overrides and this javascript will execute before opening the AI component. ```javascript Txt.componentSettingOverrides = [ {path: 'stepper.steps[0].contactPage.contactCard1Label.value', value: 'My custom label'} ] ``` The best way to get the path of a setting is to download the settings file of your AI component. You can do that by going to the AI configuration and open the component editor for your AI component. Then in the editor in the Other section click on the "download settings" button. This will give you a settings file in json format. The paths you can override correspondent with the format of this file. **Examples:** ```javascript Txt.componentSettingOverrides = [ // override the disclaimer message {path: 'stepper.steps[0].layout.disclaimerMessage.value', value: 'My disclaimer message.'}, // override the starting message {path: 'stepper.steps[0].layout.startingMessage.value', value: 'My custom starter message.'}, // override a starting AI action {path: 'stepper.steps[0].layout.aiActions.value', value: 'c8f9ef50-726b-46d1-b598-b26c99f6aac5'} ] ``` # Procedures > Configure automated procedures including follow-up questions, JavaScript execution, API calls, and livechat triggers. You can create procedures in our dashboard, these actions can do a range of tasks, including but not limited to: * showing a followup question * Execute custom javascript * Call an API endpoint * Start a live chat By default, these procedures show as buttons in our AI component for the end-user to use. There is also an option to immediately execute an action by skipping the human in the loop. In that case, the action won't be shown to the user first but will immediately execute. ### Procedure data Procedures can require variables to execute. The variable you set on a procedure will show in a form to the end-user. The user has to fill out the form with the required field before the action can be executed. These can be any questions like requesting a name, email, selecting a category, etc. Variables can also be dependent on each other. This means that the value of the first variable will determine if a second variable will be shown or skipped. This way you can create a tree-like structure in the form questions the user will have to fill in. Procedure data will be stored in the conversation and shown in the conversation details in the Unless dashboard. If a variable is already set, the AI won't ask for it again in the form. So if the email is already known, the form will not prompt the user to fill in their email. If you want the user to fill in the variable even if it is already known, then you can use the "Re-ask every time" switch on the variable details to turn that on. :::note Procedure data can also be set using the javascript API. They are automatically also available to the AI. Anything set via the API will not be re-asked unless specifically switched on. See: [set procedure data](/javascript-api/ai-component/set-procedure-data/). ::: Finally, you can use a variable in the procedure by using our placeholder syntax: `{{variable_name}}`. For example, if you have a custom-javascript action you can create something like this: ```javascript // if statement depending on the variable value if ('{{type_select}}' === 'opening_times') { // send a prompt to the AI. Txt.sendMessage('What are the opening times') } else { // just showing here you can insert the variable anywhere. window.location.href = 'https://google.com?q={{type_select}}' } ``` ### Guidance To show a procedure to the user, you need to set guidance conditions. You can do this in the guidance tab. Procedures can be shown when: * Agentic * When a specific document is used to answer a question * On negative feedback (thumbs down) * After an unanswered question * After every single response * After the user is detected inactive There is also a special condition called "Display only if rule matches". Here you can add your own custom javascript that will determine if the action will be shown to the end-user. # Set procedure data > Attach data to conversation using Txt.set('procedureData', ...) for dashboard visibility. Any procedure data you set will be visible in the Unless dashboard when reviewing a conversation. Procedures will also directly use procedure data. The data can already be set before opening the AI component. Note that any of these will be overridden by any **secure traits** you might have set when identifying the user. :::note Values may be a string, number or boolean. Any fields that have an error are dropped and reported with a warning in the browser console. ::: ```javascript Txt.setProcedureData({ userType: 'Admin', subscription: 'fixed' }) ``` # Listening for events > Listen for unless-event document events to forward analytics to your tracking service. Internally we use events to track whether a component has been seen or not, or in the case of a control group, if the component would have been seen if it wasn't for the control group. Based on these events we can make our insights pages. The events are also emitted from our script itself. This allows you to listen from them programatically and forward them to any analytics service you are using. The events are emitted on the document using the key `unless-event`. The event detail has the following fields: | Field name | Description | | :-------------- | :---------------------------------------------------------------------------------------- | | event_type | The type of the event, usually `experience_started`. | | experience_name | The name of the experience/component that is started. | | experience_id | The internal ID of the experience/component. | | experience_type | The experience type, this can be either `Component` or `On-site`. | | experience_test | The test type of the visitor. Either `Control`, `Participant` or the name of the variant. | The following example will simply print the detail of the event: ```javascript // make sure to add the event listener to the document before loading the Unless script to make sure you won't miss any events. document.addEventListener('unless-event', (e) => console.log(e.detail)) ``` # Audiences > Use audience rules to show components to specific visitors and restrict AI knowledge. Audiences in Unless are used in two ways: 1. to show certain components to certain audiences only 2. to restrict the AI from certain knowledge articles based on the audience # Checking audiences > Use Txt.inAudiences to check if the current visitor belongs to a given audience. If you need to check if a user is in a specific audience you can use the available variable `Txt.inAudiences`. This variable will contain an array of Audience objects that the user is currently a member of. ```javascript // example output of the audiences list [ { "duration": 2592000000, "ruleName": "Everyone", "readOnly": true, "joinDate": 1757407967108, "sessionId": "395df9eb-f3b1-7bd8-1499-41c663aca218", "ruleId": "0be4b18e-9079-4c22-85cc-d97c0f75a52b" }, { "duration": 2592000000, "ruleName": "Desktop", "readOnly": true, "joinDate": 1757407967108, "sessionId": "395df9eb-f3b1-7bd8-1499-41c663aca218", "ruleId": "5773a5eb-4971-4d1b-b328-6c2879a91b42" } ] ``` You could use this information in for example, the AI actions custom javascript to determine if you want to show the action to the end user based on an audience they are in. Here is an example how that would work as a "Display only if rule matches" condition on an action: ```javascript return Txt.inAudiences.some((a) => a.ruleId === 'c1f79991-5f8d-40df-8c5a-4a17ce59fcaa') //make sure to replace the audienceId with a valid audienceId from your own audiences in your account ``` # Updating a profile > Enrich visitor profiles with key-value data using updateProfile() to power audience targeting. Audiences are joined by matching user profile fields using the audience builder in the Unless dashboard. To be able to set up these audiences, you first need to send us some profile fields using the updateProfile function: ```javascript // update the profile with additional data points Txt.updateProfile([ {key: 'firstName', value: 'John', type: 'text'}, {key: 'role', value: 'CMO', type: 'text'} ]) ``` The update profile function enriches an existing visitor profile with additional data points. ```javascript Txt.updateProfile(data) ``` The data argument is required and should be an array of objects. The data object within the array should be structured as follows: | Key | Type | Description | | ----- | --------------------- | --------------------------------------------------------------------------------------------------- | | key | string **required** | The key to store as a trait. | | value | string **required** | The value to store. | | type | string **required** | The type of the data that you are sending, options are **text**, **number**, **bool**, **date**. | :::note The **Txt.setProfile(_data_)** function works exactly the same as **Txt.updateProfile(_data_)** except it will not immediately force an update. Instead, it will only happen locally and waits until the next call. This enables you to do multiple set profile calls in a function. This is better for resources and network traffic. It works well with the custom startup script feature in Unless. ::: # Content security policy (CSP) > Minimum CSP configuration required to allow Unless to run on your website. Content Security Policy (CSP) is a security feature that helps prevent a range of attacks such as cross-site scripting (XSS) and data injection by specifying which sources of content are considered safe. CSP allows you to control resources the user agent is allowed to load, reducing the risk of malicious content being executed on a user's browser. Implementing CSP is good practice because it strengthens the security of web applications by mitigating vulnerabilities introduced through third-party content or untrusted sources. If you are using a CSP then you will need to add the Unless domains to the CSP for it to work. This is the minimum setup you need: ```text script-src 'self' 'unsafe-inline' https://unless.com https://*.unless.com; style-src 'self' 'unsafe-inline' https://unless.com https://*.unless.com; connect-src 'self' https://unless.com https://*.unless.com wss://*.unless.com; font-src 'self' https://unless.com https://*.unless.com; frame-src 'self' https://unless.com https://*.unless.com; img-src 'self' data: https://unless.com https://*.unless.com; media-src 'self' https://unless.com https://*.unless.com; ``` Explanation: * **script-src** is needed to load our initial script _note_: 'unsafe-inline' is only needed if you don't use a nonce or hash method, see "Strict CSP" below. _note_: 'unsafe-eval' is optional and only necessary if you want to be able to run custom javascript on component-load, or on a button click. * **style-src** is required with 'unsafe-inline' for us to be able to load the styles for our components. * **connect-src** connects to our backend with XHR and to our websocket endpoint for the AI conversations. * **font-src** is needed to load fonts & icons in the components. * **frame-src** is required to load our editor from the dashboard. The editor loads your own website in an iframe so we can add extra functionality like point-and-click selector generation for inline components. * **img-src** is needed to load images used in components. * **media-src** for our text-to-speech functionality ### Strict CSP To improve security even further you can add a nonce to your CSP headers and the scripts on your page. This will work fine with our script if you add the nonce with 'strict-dynamic'. For more information see: [https://web.dev/articles/strict-csp](https://web.dev/articles/strict-csp) Example of a strict CSP using a nonce (the _$\{nonce}_ should be dynamically generated on the server): ```text script-src 'nonce-${nonce}' 'strict-dynamic' https://unless.com https://*.unless.com; object-src 'none'; base-uri 'none'; style-src 'self' 'unsafe-inline' https://unless.com https://*.unless.com; connect-src 'self' https://unless.com https://*.unless.com wss://*.unless.com; font-src 'self' https://unless.com https://*.unless.com; frame-src 'self' https://unless.com https://*.unless.com; img-src 'self' data: https://unless.com https://*.unless.com; ``` The same nonce should then also be added to all the scripts on your page including the Unless snippet. # Identifying users > Set a custom identifier or secure JWT token to identify visitors in Unless. Usually you don't want a visitor to see a certain experiences multiple times, for example a popup that the visitor closes should not be shown again later. To do this, we set a cookie to make sure that doesn't happen. However, if your product runs behind a login, then it's best practice to set a customer identifier before loading our snippet. This will make sure the visitor will always have the same id, across devices and even after clearing cookies. This guide explains how to identify users with the client-side API. It supports two modes: * Basic identification with unsecured profile data. * Secure identification with a backend-signed JWT for trusted traits. ## Set the identifier before the snippet loads (preferred) If possible, set a stable identifier before loading the Unless snippet. The custom identifier will be used as the identifier of the user. The JWT token is optional and is described below. ```html ``` ### Alternatively set the identifier after the snippet loads In cases you can't set the identifier or JWT before the script loads. For example this can happen in single page application (SPA) that load statically but use runtime requests to log the user in. In that case, you can use our `initialize` function on the Txt object instead. When using this approach make sure to first set `autoInitialize` to `false` in the `TxtOptions` object to prevent loading of the user before the initialize call happened (see example below). Signature: ```js Txt.initialize({ identifier: string, data?: [], //see signature: https://docs.unless.com/javascript-api/audiences/updating-a-profile/ jwt?: string, }) ``` Parameters: * `identifier` (string, required): Stable user ID. This becomes the visitor identifier. * `data` (object, optional): Unsecured traits stored in `profile`. * `jwt` (string, optional): Signed JWT containing secure traits. Use `Txt.initialize(...)` when identifying a user for the first time on page load, and `Txt.setJwt(...)` when updating or rotating the signed JWT later in the same session. This is important to prevent the token from expiring if you have a Single Page Application. ### Example identification When using the Txt object, you have to make sure the script is fully loaded before calling a function on it. To do this use the txt-loaded event. ```html ``` ## Secure identification (recommended) Secure identification protects any user information in our system using a signed JWT token. This is highly recommended to protect user information and conversational data. Some functionality in the Unless system require verified identification. For example, if you want to send data to 3rd party API's through agentic skills. In that case the data you are going to send needs to be validated and secure. To do this you need to create a JWT token on your backend to provide secure traits. If you create a skill that contains a variable with the same name as one of your secure traits, then we will automatically use the value of that secured trait for the variable. You can also put authentication tokens inside a secured traits so we can pass that on to your receiving API, make sure this token is called `authToken` and is **short lived** and not a permanent authentication token. Don't store any information in the JWT that should be a secret to your user (like secret API keys). By calling the property `authToken` we provide some extra checks on our backend to make it isn't send to the client and/or logged. ### What is a JSON web token (JWT)? A JSON Web Token (JWT) is an industry standard way to sign data. It typically consists of three parts, separated by dots. A typical JWT looks like this: header.payload.signature. * The header specifies the token type (JWT) and the signing algorithm (e.g., HS256). * The payload contains claims about the user or session (e.g., user_id, email). * Finally, the signature ensures that the token hasn't been tampered with, using a secret or private key. For more information see: [https://www.jwt.io/introduction#what-is-json-web-token](https://www.jwt.io/introduction#what-is-json-web-token). ### JWT requirements * **Algorithm:** HS256 * **Secret:** your account API key (from the dashboard) * **Required claim:** `sub` (must match `identifier`) * **Recommended claim:** `exp` (expiration timestamp, in seconds) * **Optional claims:** `traits`. **Note**: any **variables** you define in your AI skills are automatically filled using these traits and can be considered secure. Example payload (all of the traits are optional and customizable): ```json { "sub": "visitor_123", "traits": { "plan": "enterprise", "region": "emea", "email": "user@company.com", "authToken": "your-custom-token" }, "exp": 1769509999 } ``` :::danger Avoid setting long lifetimes for access tokens. Recommended is to use a maximum of 1 hour. ::: ### Refreshing the JWT token If you have a Single Page Application, you have to make sure the JWT token does not expire. If your application refreshes or rotates secure identification tokens during a session, you can update the JWT without reloading the Unless snippet by using the `Txt.setJwt()` function. ```js window.Txt.setJwt(newJwt) ``` This updates the JWT used for secure identification requests. If the AI component is already loaded, active chat connections will also refresh their authentication automatically. #### Arguments | Argument | Type | Description | | -------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `jwt` | `string` | A backend-signed JWT using `HS256`. The token should contain a `sub` claim that matches the current identifier. | #### Example ```js const refreshedJwt = await fetch('/api/unless/identify-jwt').then((res) => res.text()) window.Txt.setJwt(refreshedJwt) ``` ### Overview 1. Your backend creates an HS256 JWT using your account API key. This API key should stay on the backend and should never be accessible by your end-users in your application. 2. Your frontend calls `Txt.initialize({ identifier, data, jwt })`. 3. Unless stores unsecured `data` in `profile` and the secured JWT `traits` in `secureProfile`. 4. Make sure to refresh the JWT token before it expires.

HS256 JWT

Txt.initialize

store profile + secureProfile

Your backend

Your frontend

Unless API

Visitor profile

### Backend example (Node.js) ```js import jwt from "jsonwebtoken" const apiKey = process.env.UNLESS_API_KEY const token = jwt.sign( { sub: "visitor_123", traits: { plan: "enterprise", region: "emea", email: "user@company.com", }, exp: Math.floor(Date.now() / 1000) + 300, }, apiKey, { algorithm: "HS256" }, ) ``` ## Troubleshooting * **Invalid token:** Check the signing secret and algorithm (must be HS256). * **Missing `sub`:** Tokens must include a `sub` claim that matches `identifier`. * **Expired token:** Ensure `exp` is in the future and in seconds (not milliseconds).
# Initialization > How to wait for the Txt object to be available before calling its methods. In case you need to call a function on the global Txt object, you have to make sure the object is loaded properly. You can do this by using our event `txt-loaded`. ```javascript document.addEventListener('txt-loaded', (e) => { console.log('Txt object is now available.'); // do something like Txt.updateProfile(...); }); ``` # Launching a web component > Programmatically trigger an Unless component by experience ID using Txt.launchComponent(). ```json { "text": "Sample code", "sidebar": true } ``` ```javascript // you can get the experienceId from the dashboard Txt.launchComponent('abcd1234', {force: false}) //by default force is set to true ``` Use the launch component function to programmatically launch a component regardless of audience by providing the experienceId. If you set force to false, then the component will only be shown if: * The target page settings match the current URL of the visitor * The state of the experience is 'live' * The visitor is not in the control group * The components 'Keep hidden after closing for' setting matches If you want to make sure the component always shows, simply target all pages, set the value of 'Keep hidden after closing for' to 'never' and set the testing mode to 'no control group'. If you want to trigger the component only by API and you never want to trigger it automatically by audiences, then simply set the target audience of the component to 'nobody'. ```javascript Txt.launchComponent(experienceId, options) ``` The launch component function takes the following arguments: | Argument | Type | Description | | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | | experienceId | string **required** | The experience ID of the component to launch. | | options | object (optional) | The options contain the following settings: **force: boolean** (default true, forces to show the component regardless of settings) | # Tracking consent > Control visitor tracking consent with the Unless opt-in and opt-out API. The consent API controls whether a visitor is shown Unless components and whether that visitor is tracked. It lets you connect your own cookie banner or consent-management platform to Unless, so tracking only turns on when you decide it should. By default, Unless does **not** use cookies or track end users — components and the AI assistant work fine without it. Once a visitor opts in (or is identified as a logged-in user), features like A/B testing, Audiences, User Profiles, and Chat History become active, and the `unless_id`/`unless_sid` cookies are set. For the full breakdown of what these cookies do, which features need consent, and when you're legally required to ask for it, see [End user tracking](https://unless.com/en/legal/resources/cookies-and-tracking-for-end-users/). # Opting in > Enable tracking consent for a visitor using Txt.optInTracking(). This function opts you in to tracking. ```javascript Txt.optInTracking() ``` # Opting out > Withdraw tracking consent using Txt.optOutTracking() with optional mode parameter. This function opts you out of tracking. ```javascript Txt.optOutTracking() ``` ```javascript Txt.optOutTracking('essential') ``` ## Parameters | Argument | Type | Description | | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | string (optional) | Should be either `'essential'` or `'none'`. Choosing `'essential'` will still show experiences wherever possible without placing cookies. Choosing `'none'` will opt out of all experiences. | # Utility functions > Helper methods available on the Txt global object. The following utility functions are available on the `Txt` global object: - [Get Value](./get) — retrieve a visitor data value by key - [Set Value](./set) — store a custom key/value pair on the visitor - [Logout](./logout) — clear all Unless storage and cookies - [Send a Message](./send-a-message) — programmatically send a prompt to the AI - [Get Conversation Transcript](./get-transcript) — retrieve the current conversation as a string - [Get Last QA Pair](./get-last-qa-pair) — retrieve the most recent question and answer # Get last QA pair > Retrieve the most recent question and answer pair with metadata using Txt.getLastQaPair(). To get the last asked question, and the answer you can use the following utility function: ```javascript Txt.getLastQaPair() ``` This will return a question and answer object containing all the associated metadata for the QA pair. # Get conversation transcript > Retrieve the current conversation as a formatted string using Txt.getChatTranscript(). You can get the transcript of the current conversation by calling ```javascript Txt.getChatTranscript() ``` This will return a formatted string with the conversation. # Logout > Fully log out a visitor and clear all Unless storage using Txt.logout(). You can call this function if you want to completely log out the visitor and remove any Unless related local/session storage & cookies. ```javascript Txt.logout() ``` # Send a message > Programmatically trigger the AI to answer a prompt using Txt.sendMessage(). A simple helper function that you can invoke if you want the AI to answer a prompt. Make sure the AI component is already opened before calling this. If not, use the `Txt.openChat()` function instead. This helper function is useful if you want to trigger a follow up prompt from an action. ```javascript Txt.sendMessage(prompt: string, hidden = false) ``` # REST API Reference > Complete reference for the Unless REST API — AI query, summarization, similarity, feedback, and personalizations endpoints. All endpoints require an `x-api-key` request header. Default base URL: `https://api.unless.com`. ## POST /api/v1/ai/query — Sending a question (realtime) The query endpoint always gives you a chat-data object that has the original question and the response it generated. Normally, you would use this object to show the response in your view. It's important to use the `chatDataJson` property when you ask any more questions to make sure the conversation history is kept correctly. **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `query`, `visitorId`, `sessionId`, `chatDataJson`. **Optional body fields:** `conversationName`, `segmentId`, `audienceId`, `metadata`, `messageFormat`, `skipLanguageDetection`. **Response:** Successful response containing updated chat data.. ## POST /api/v1/ai/async/query — Sending a question (async) *Base URL: `https://api.unless.com`* Submit AI queries for asynchronous processing using the Unless API. This returns a questionId to retrieve the results later. The async AI query endpoint allows you to submit questions for background processing, ideal for long-running queries or batch processing scenarios where immediate response is not required. **How it works** 1. **Submit query**: Send your question to `/ai/async/query` 2. **Get question ID**: Receive a `questionId` immediately 3. **Retrieve results**: Use the `questionId` with `/ai/async/answer` to get results when ready **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `query`, `sessionId`, `visitorId`. **Optional body fields:** `segmentId`, `metadata`, `conversationName`, `audienceId`. **Response:** Question accepted. Use `questionId` to poll for the answer.. ## GET /api/v1/ai/async/answer — Retrieving an answer (async) *Base URL: `https://api.unless.com`* Retrieve the result of an asynchronous AI query using the `questionId` returned from `/v1/ai/async/query`. This endpoint returns the current processing status and the full ChatData response when complete. **Status values** - **PENDING**: Query is queued for processing - **PROCESSING**: Query is currently being processed - **DONE**: Query completed successfully, `chatData` contains the result - **ERROR**: Query failed, `error` contains the error message **Usage flow** 1. First, submit a query using `/v1/ai/async/query` to get a `questionId` 2. Poll this endpoint using the `questionId` to check the status 3. Continue polling until status changes to `DONE` or `ERROR` 4. When `DONE`, the full AI response is available in the `chatData` object **Required parameters:** `questionId` (query). **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** Current status and, when complete, the chat data.. ## POST /api/v1/ai/summarization — Summarization **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `prompt`. **Optional body fields:** `filterPII`, `instructionsTemplate`, `responseLanguage`. **Response:** Generated summary.. ## POST /api/v1/ai/similarity — Similarity **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `query`. **Optional body fields:** `segmentId`, `maxResults`, `personalizationId`. **Response:** Array of similar content pages, ranked by relevance.. ## POST /api/v1/ai/feedback — Sending feedback Initially, when an answer is rated, you should set the value of the feedback property within the chata-data history object. This value can be set to either negative or positive. Following this, proceed to call this endpoint to store the feedback. The endpoint will respond with a generated reply in the appropriate language of the conversation. As a subsequent step, you can present this message to the user and request their email address. If you intend to forward this conversation to the configured support email address, you can simply make another call to this endpoint, setting the sendEmail property to true and including the user's email address within the email property. Once more, this endpoint will provide a generated response that you can display to the user. **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `chatDataJson`, `visitorId`, `feedback`. ## GET /api/v1/personalizations — Get personalizations *Base URL: `https://api.unless.com`* Retrieve all personalizations (experiences) for a specific website, including their configuration, targeting rules, variants, and status information. **Use Cases** - **Dashboard Overview**: Retrieve all personalizations for management interface - **Performance Monitoring**: Get current state and configuration of all experiences - **Bulk Operations**: Fetch personalizations for batch updates or analysis - **Integration**: Sync personalization data with external systems - **Reporting**: Generate reports on personalization coverage and status **Notes** - All timestamp fields are in Unix timestamp format (milliseconds) - Rules are stored as JSON strings and need to be parsed - Priority (`prio`) determines execution order when multiple personalizations could apply - Component-based personalizations have additional fields for component configuration - The `variants` array contains the different variations being tested **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** Successful response with array of personalizations.. ## GET /api/v1/audiences — Get audiences *Base URL: `https://api.unless.com`* Retrieve audiences for an account, optionally filtered by website or state. When `ruleId` is provided, returns a single audience by ID. **Filtering** - Filter by `websiteId` to scope results to a specific website - Filter by `state` to retrieve live or stopped audiences (default: `live`) - Use `ruleId` to fetch a single specific audience by its ID - returns `{ audience }` instead of `{ audiences }` **Notes** - All timestamp fields are in Unix timestamp format (milliseconds) - Rules are stored as JSON strings and need to be parsed - Priority (`prio`) determines execution order when multiple audiences could apply - The `variants` array contains the different variations being tested **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `ruleId` (query), `state` (query). **Response:** Successful response with list of audiences.. ## POST /api/v1/audiences — Create or update an audience *Base URL: `https://api.unless.com`* Create a new audience, or update an existing one by passing its `ruleId`. An audience is a reusable visitor segment defined by a targeting rule, used to scope personalizations and AI behaviour. **Use cases** - **Segmentation**: Create audiences for visitor groups you want to target, e.g. returning visitors or visitors from a specific campaign - **Account setup**: Provision standard audiences when configuring a new workspace - **Maintenance**: Update the rule or name of an existing audience by passing its `ruleId` **Notes** - Omit `ruleId` to create a new audience, pass it to update that audience - The `rule` field is a JSON string containing the targeting rule expression - Use the get audiences endpoint to inspect existing audiences and their rule format first **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `name`, `rule`, `state`. **Optional body fields:** `ruleId`, `duration`, `durationType`, `audienceTags`. **Response:** The created or updated audience.. ## GET /api/v1/tasks — Get tasks *Base URL: `https://api.unless.com`* Retrieve the tasks for a website. Tasks are work items the Unless platform creates for the account team, such as knowledge suggestions from the AI, detected knowledge gaps, flagged conversations to review, and sales or retention signals. **Use cases** - **Account investigation**: List open tasks to understand what needs attention on an account - **Task triage**: Fetch tasks before updating their state or assignee with the update endpoint - **Reporting**: Count tasks per state to summarise the account's backlog **Notes** - `websiteId` is required as a query parameter and should match the `x-website-id` header - Filter on `state` (`open`, `done` or `deleted`) to reduce the result set, tasks of every state are returned otherwise - Tasks are sorted by `updatedAt`, most recent first **Required parameters:** `websiteId` (query). **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `state` (query). **Response:** The matching tasks with a total count.. ## POST /api/v1/tasks — Create a task *Base URL: `https://api.unless.com`* Create a new task for the account team. The task appears in the dashboard inbox in the `open` state. **Use cases** - **Knowledge suggestions**: Record a suggestion to add or improve knowledge content - **Follow-ups**: Log something a human needs to review or act on after an automated investigation **Notes** - `name`, `description`, `type` and `source` are required - Use the `data` field for structured context the assignee needs **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `name`, `description`, `type`, `source`. **Optional body fields:** `data`. **Response:** The created task.. ## PUT /api/v1/tasks — Update a task *Base URL: `https://api.unless.com`* Update an existing task. Only the fields present in the body are changed, everything else is left as is. **Use cases** - **Task management**: Mark a task as `done` after completing it, or reopen it - **Assignment**: Assign a task to a team member by email, or unassign it with an empty `assignee` - **Editing**: Adjust the name, description or data of a task **Notes** - `taskId` and `websiteId` are required in the body - Change the task state by setting `state` to `open`, `done` or `deleted` - Use the get tasks endpoint first to find the `taskId` **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `taskId`, `websiteId`. **Optional body fields:** `state`, `name`, `description`, `assignee`, `data`. **Response:** The updated task.. ## GET /api/v1/ai/configuration — Get AI configuration *Base URL: `https://api.unless.com`* Retrieve the AI configuration for a website: the main language, response length, product name, custom rules and other settings that control how the AI answers questions. **Use cases** - **Account investigation**: Inspect how the AI is configured before diagnosing answer quality or behaviour - **Verification**: Confirm settings such as the main language or custom rules after making changes in the dashboard **Notes** - The configuration is returned as a flat object - When a website has no configuration yet, a default configuration is created and returned **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** The AI configuration for the website.. ## GET /api/v1/ai/training — Get training data *Base URL: `https://api.unless.com`* Retrieve the training data sources of a website: the websites, files, FAQs and integrations the AI has been trained on. **Use cases** - **Account investigation**: See which knowledge sources the AI can answer from - **Ingestion follow-up**: Check the status of a source after ingesting a website, including how many URLs were indexed - **Lookup**: Fetch a single source by `trainingDataId` before updating it **Notes** - Without parameters, all training data sources for the website are returned - Pass `trainingDataId` to fetch one specific source - `indexedUrlsCount`, `totalUrlsCount` and `lastScanTimestamp` tell you how a website ingestion went **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `trainingDataId` (query). **Response:** The training data sources.. ## POST /api/v1/ai/training/data — Ingest a training data source *Base URL: `https://api.unless.com`* Create a new training data source, or update an existing one by passing its `trainingDataId`. Creating a source of type `url` starts a website ingestion: Unless crawls the site and trains the AI on its content. **Use cases** - **Ingest a website**: Create a `url` source pointing at the website you want the AI to learn from - **Rescan**: Update an existing source to change its URL filters or schedule and trigger a retrain **Notes** - Omit `trainingData.trainingDataId` to create a new source, pass it to update that source - `trainingData.accountId` and `trainingData.websiteId` must match the `x-account-id` and `x-website-id` headers - For website ingestion use `type: url` with `method` set to `all` (crawl the whole site), `single` (one page) or `sitemap` - Ingestion runs asynchronously, poll the get training data endpoint to follow progress **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `trainingData`. **Optional body fields:** `skipRetrain`. **Response:** The source was saved and ingestion was started.. ## POST /api/v1/help-center/publish — Publish the help center *Base URL: `https://api.unless.com`* Start a help center publish. This builds the help center from the current FAQs, categories and settings and deploys it to the live help center site. Publishing runs asynchronously and only one publish can run per website at a time. **Use cases** - **Content release**: Publish after adding or changing FAQs so the changes go live - **Partial updates**: Publish only specific FAQs with a `partial` scope, or only structure and settings with a `structural` scope **Notes** - Returns `202` with the created job, poll the publish status endpoint to follow progress - Returns `409` when a publish is already running for the website - Omit the body for a full publish **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Optional body fields:** `scope`. ## GET /api/v1/help-center/publish-status — Get help center publish status *Base URL: `https://api.unless.com`* Retrieve the publish state of the help center for a website: whether a publish is currently running, plus the most recent publish jobs, newest first. **Use cases** - **Progress polling**: After starting a publish, poll this endpoint until the job status is terminal - **Account investigation**: Check when the help center was last published and whether recent publishes succeeded **Notes** - `state.runningJobId` is set while a publish is in flight, `null` when the website is idle - Up to 25 recent jobs are returned **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** The publish state and recent jobs.. ## GET /api/v1/ai/quality/control — Get quality control questions *Base URL: `https://api.unless.com`* List the control questions the AI is scored against for a website, oldest first. A quality control report grades the AI on every one of these. **Use cases** - **Account investigation**: See what the account measures answer quality against, and whether anything is being measured at all - **Before a report run**: A report needs at least one control question, so read these before starting one **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** The control questions for the website.. ## POST /api/v1/ai/quality/control — Create or update a quality control question *Base URL: `https://api.unless.com`* Add a control question and the answer the AI is expected to give, or update an existing one by sending its `id`. **Notes** - Omit `id` to create. The server generates the `id` and sets `createdAt` - Send `id` to update. Every other field is overwritten with what you send, so send the whole question - `accountId` and `websiteId` are taken from the request headers and ignored in the body **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Required body fields:** `question`, `answer`. **Optional body fields:** `id`, `segmentId`, `audienceId`. **Response:** The question was created or updated.. ## GET /api/v1/ai/quality/control/report — Get quality control reports *Base URL: `https://api.unless.com`* Without `reportId`, list every quality control report for the website, newest first. With `reportId`, return that report's per-question results. **Use cases** - **Progress polling**: After starting a report, poll the list until `processed` equals `totalQuestionsInReport` - **Account investigation**: Read `grade` over time to see whether answer quality is improving **Notes** - A report only appears in the list once its first question has been scored, so a run you just started is briefly absent - Questions that fail to score are dropped rather than retried, so `processed` can stop short of `totalQuestionsInReport` permanently. Treat a report whose `timestamp` has not moved for a few minutes as finished - `grade` is out of 10, and a question counts towards `passed` at 6 or above **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `reportId` (query). **Response:** The report list, or the per-question results when `reportId` is given.. ## POST /api/v1/ai/quality/control/report — Generate a quality control report *Base URL: `https://api.unless.com`* Start a run that asks the account's AI every control question and grades each answer against the expected one. Returns immediately with the `reportId`; the grading happens asynchronously. **Notes** - Every control question costs one AI call, so this is only available on the Enterprise, Flex, Fixed and Plus plans. Other plans get `403` - Answers `400` when the website has no control questions yet - Poll `GET /api/v1/ai/quality/control/report` for progress. The report is absent from the list until its first question has been scored **Optional parameters:** `undefined` (undefined), `undefined` (undefined), `undefined` (undefined), `undefined` (undefined). **Response:** Report generation was started..