# Account management for AI agents

> How an AI agent can investigate and manage an Unless account through the REST API with an API key.

This file is the complete, self-contained version of the guide for automated consumption. It carries the full
specification of every endpoint - parameters, request bodies, responses and examples - so no further page needs
to be fetched. The human-readable version lives at https://docs.unless.com/guides/account-management-for-ai-agents/.

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.

## Instructions for the agent

If you are an AI agent working on someone's Unless account, follow these rules.

**Ask the user for the credentials.** Before the first call, ask for all four values in one message:

| Ask for             | Where the user finds it                                                                               |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| Personal API key    | Dashboard, Account > Settings, the "Personal API key" row. Keys are issued with a `personal_` prefix. |
| Their email address | The address they sign in with. The key is tied to that user, so the two have to match.                |
| Account ID          | Dashboard, Account > Settings, the "Account ID" row at the top of the page.                           |
| Workspace ID        | Dashboard, Workspaces, the actions menu on the workspace row, "Copy workspace ID".                    |

Ask for the personal key, never the account-wide one. Both authenticate, but the personal key carries only that one user's permissions and can be replaced without affecting anyone else. A key without the `personal_` prefix is probably the account-wide key, so ask again.

The account and workspace pages need the administrator role, so a user without it has to get these values from an administrator.

**Do not go looking for the credentials yourself.** Do not search files, `.env` files, environment variables, git history, shell history or a password manager, and do not reuse an account or workspace ID you happened to see somewhere. Acting on the wrong account is far worse than asking. No endpoint lists the accounts or workspaces a key can reach, so the IDs have to come from the user.

**Make the calls yourself.** Issue the HTTP requests with whatever tool you have. Do not hand the user a `curl` command and ask them to paste the output back. If you have no way to reach `https://api.unless.com`, say so plainly instead of delegating the work.

**Keep the key in the request headers.** Do not repeat it back to the user, write it to a file, commit it, or include it in a summary.

**Stay in the workspace the user named.** Every call takes `x-website-id`, and most reads also want the same value as a `websiteId` query parameter. If the account has several workspaces and the user did not say which one, ask.

**Confirm before anything a visitor will see.** Publishing the help center, setting a personalization or an audience live, and accepting content library changes all reach real visitors. Describe what you are about to do and wait for a yes. Reads need no confirmation.

**Report what the API actually returned.** On failure, give the status code and the response body rather than guessing at the cause, and do not report a change as done until a response confirmed it.

## Authentication

Every request needs four headers:

| Header         | Value                                                                                                                      |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `x-api-key`    | The user's personal API key from Account > Settings. The account-wide key also authenticates, but prefer the personal one. |
| `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=<website-id> \
  -H "x-api-key: <api-key>" \
  -H "x-user-email: <user-email>" \
  -H "x-account-id: <account-id>" \
  -H "x-website-id: <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

This catalog tells an agent which endpoint fits which job. Every endpoint links to its entry in the [REST API reference](#endpoint-reference), where the headers, parameters, request body and responses are documented in full.

### Investigate an account

| Endpoint                                                                                         | Use it to                                                                                                               |
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| [`GET /api/v1/tasks`](#get-tasks)                                           | List the account's work items: knowledge suggestions, detected gaps, flagged conversations, sales signals.              |
| [`GET /api/v1/ai/configuration`](#get-ai-configuration)                     | Inspect how the AI is configured: main language, product name, custom rules.                                            |
| [`GET /api/v1/ai/training`](#get-training-data)                             | See which knowledge sources the AI is trained on and how ingestions went.                                               |
| [`GET /api/v1/audiences`](#get-audiences)                                   | List the visitor segments defined for the account.                                                                      |
| [`GET /api/v1/personalizations`](#get-personalizations)                     | List the personalizations (experiences) running on a website.                                                           |
| [`GET /api/v1/help-center/publish-status`](#get-help-center-publish-status) | Check whether the help center is published, publishing or failed.                                                       |
| [`GET /api/v1/ai/quality/control`](#get-quality-control-questions)          | List the control questions the AI is scored against.                                                                    |
| [`GET /api/v1/ai/quality/control/report`](#get-quality-control-reports)     | List quality control reports, or fetch one report's per-question results with `?reportId=`.                             |
| [`GET /api/v1/ai/actions`](#get-procedures)                                 | List the procedures the AI can run: what each one does, whether it is enabled, and which training documents trigger it. |
| [`GET /api/v1/components/custom-components`](#get-custom-components)        | List the custom components available to the account.                                                                    |
| [`GET /api/v1/accounts/logs`](#get-account-log)                             | Read the audit trail of who changed what. Last 90 days, 100 entries.                                                    |

### Read the numbers

Insights are read-only over the API. Every endpoint takes `startTime` and `endTime` as inclusive `YYYY-MM-DD` days, except the ROI and maturity endpoints, which report over the account's whole history. Results come from a query cache, so check `status` before trusting the numbers, and reach for `forceRefresh=true` only when you know the cache is stale.

| Endpoint                                                                                                        | Use it to                                                                                     |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [`GET /api/v1/insights/pageviews`](#get-insights-pageviews)                                | Pageview totals, split by new versus returning and personalized versus not.                   |
| [`GET /api/v1/insights/pageviews/per-day`](#get-insights-pageviews-per-day)                | The same split, one entry per day.                                                            |
| [`GET /api/v1/insights/outcomes`](#get-insights-outcomes)                                  | How many outcomes (conversions) were recorded.                                                |
| [`GET /api/v1/insights/outcomes/per-day`](#get-insights-outcomes-per-day)                  | Outcomes per day, optionally for one personalization.                                         |
| [`GET /api/v1/insights/audiences/per-day`](#get-insights-audiences-per-day)                | How many sessions one audience matched per day. Needs `audienceId` and `domainName`.          |
| [`GET /api/v1/insights/components/events`](#get-insights-component-events)                 | Engagement per variation: views, CTA clicks, and the AI chat counters.                        |
| [`GET /api/v1/insights/components/events/per-day`](#get-insights-component-events-per-day) | The same counters, one entry per variation per day.                                           |
| [`GET /api/v1/insights/wiki/events`](#get-insights-wiki-events)                            | Content library updates the AI made, and the tokens it spent.                                 |
| [`GET /api/v1/insights/wiki/events/per-day`](#get-insights-wiki-events-per-day)            | The same, one entry per day.                                                                  |
| [`GET /api/v1/insights/roi`](#get-insights-roi)                                            | What the platform handled, per surface, against the account's business figures.               |
| [`GET /api/v1/insights/ai-maturity`](#get-insights-ai-maturity)                            | The maturity assessment: score, stage and level summaries, and the features behind each cell. |

### Read conversations

| Endpoint                                                                                 | Use it to                                                            |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| [`GET /api/v1/visitors/conversations`](#get-conversations)          | List conversations, or fetch one transcript with `?conversationId=`. |
| [`GET /api/v1/ai/conversations/details`](#get-conversation-details) | Read one conversation's analysis: sentiment, rating, outcome, tags.  |

### Work with the content library

The content library (wiki) is the AI's own editable knowledge base. Changes an agent makes are staged as a suggestion task and only reach the library once accepted.

| Endpoint                                                                                                | Use it to                                                                                          |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`GET /api/v1/wiki/tree`](#get-wiki-tree)                                          | Browse the entries under a path. Walk it repeatedly to see the whole tree.                         |
| [`GET /api/v1/wiki/pages/{path}`](#get-wiki-page)                                  | Read one page.                                                                                     |
| [`GET /api/v1/wiki/change-log`](#get-wiki-change-log)                              | See what changed, with the before and after content of each edit.                                  |
| [`POST /api/v1/wiki/chat`](#wiki-chat)                                             | Instruct the content library agent in natural language. Streams, and stages its changes as a task. |
| [`POST /api/v1/wiki/force-import`](#force-wiki-import)                             | Re-import a source URL, ignoring what the importer already knows.                                  |
| [`PATCH /api/v1/wiki/tasks/{taskId}/changes/{changeId}`](#update-wiki-task-change) | Accept, deny or retry a single staged change.                                                      |
| [`POST /api/v1/wiki/tasks/{taskId}/accept-all`](#accept-all-wiki-task-changes)     | Accept every staged change in a task.                                                              |
| [`POST /api/v1/wiki/tasks/{taskId}/deny-all`](#deny-all-wiki-task-changes)         | Discard every staged change in a task.                                                             |
| [`POST /api/v1/wiki/tasks/bulk-accept`](#bulk-accept-wiki-tasks)                   | Accept several tasks at once.                                                                      |
| [`POST /api/v1/wiki/tasks/bulk-deny`](#bulk-deny-wiki-tasks)                       | Discard several tasks at once.                                                                     |

### Act on an account

| Endpoint                                                                                              | Use it to                                                                                                      |
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| [`PUT /api/v1/tasks`](#update-task)                                              | Change a task's state (`open`, `done`, `deleted`), assign it, or edit it.                                      |
| [`POST /api/v1/tasks`](#create-task)                                             | Create a new task for the account team.                                                                        |
| [`POST /api/v1/ai/training/data`](#ingest-training-data)                         | Ingest a website (or another source) so the AI learns its content.                                             |
| [`POST /api/v1/help-center/publish`](#publish-help-center)                       | Publish the help center so content changes go live.                                                            |
| [`POST /api/v1/audiences`](#upsert-audience)                                     | Create a new audience, or update an existing one.                                                              |
| [`POST /api/v1/ai/quality/control`](#upsert-quality-control-question)            | Add or update a control question and its expected answer.                                                      |
| [`POST /api/v1/ai/quality/control/report`](#generate-quality-control-report)     | Start a quality control run that scores the AI against every control question. Returns the `reportId` to poll. |
| [`POST /api/v1/ai/actions`](#upsert-procedure)                                   | Create a new procedure, or update an existing one.                                                             |
| [`POST /api/v1/ai/configuration`](#update-ai-configuration)                      | Change how the AI behaves: language, product name, custom rules, PII filtering.                                |
| [`POST /api/v1/personalizations`](#upsert-personalization)                       | Create a personalization, or update an existing one.                                                           |
| [`POST /api/v1/help-center/categories`](#upsert-help-center-category)            | Create or update a help center category.                                                                       |
| [`POST /api/v1/help-center/faqs`](#upsert-help-center-faq)                       | Create or update a help center FAQ. You supply the question; the AI writes the answer.                         |
| [`POST /api/v1/help-center/faqs/retry`](#retry-help-center-faq)                  | Regenerate an FAQ's answer, for instance after the training data changed.                                      |
| [`POST /api/v1/help-center/categories/reorder`](#reorder-help-center-categories) | Set the sort position of several categories.                                                                   |
| [`POST /api/v1/help-center/faqs/reorder`](#reorder-help-center-faqs)             | Set the sort position of several FAQs.                                                                         |
| [`POST /api/v1/wiki/change-log/{id}/restore`](#restore-wiki-change)              | Roll a content library page back to one side of a logged change.                                               |

Deleting is deliberately not part of the API key surface. Removing a procedure, an audience, a control question, a training source, an FAQ, a category, a personalization, a custom component or a content library page is dashboard-only. So is anything that changes what the insights measure: the ROI business figures, granting a maturity certificate and skipping a maturity task.

### Ask the AI

| Endpoint                                                  | Use it to                                                              |
| --------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`POST /api/v1/ai/query`](#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.

### Create or update a procedure

A procedure is something the AI can do beyond answering: call an API, collect information, or escalate to a human. `POST /api/v1/ai/actions` takes the whole procedure rather than a patch, so an update starts by reading the current one.

1. `GET /api/v1/ai/actions` to list the procedures and copy the `actionId` of the one to change.
2. Decide on the `actionId`. To update, use the existing one. To create, generate a UUID yourself rather than letting the API generate one, because `arguments` has to carry the same value and you cannot know an API-generated ID until the write has already happened.
3. `POST /api/v1/ai/actions` with the full procedure, including `actionId` and `arguments`. `accountId` and `websiteId` come from the auth headers and overwrite whatever the body says.
4. `GET /api/v1/ai/actions` again to read back what was stored. The write answers `{ "actionId": ..., "message": "AI action upserted" }`.

The write endpoint stores whatever you send without validating it, so a procedure that is missing a field is created successfully and then fails the first time the AI tries to run it. Check the readback rather than trusting the `200`.

Things that are easy to get wrong:

- `arguments` is required and easy to miss. It holds the command's input, and for `command: "api"` it must be the procedure's own `actionId` - the AI dispatches the procedure as `/api <arguments>`, so an empty or wrong value means nothing gets called. For `mcp` it is the MCP tool ID, and for `direct-answer` the answer text.
- Variable `type` accepts only `string`, `multiline`, `email`, `regex` and `select`. `number`, `boolean`, `date` and `phone` look plausible but are not implemented, and a stored `number` makes `GET /api/v1/ai/actions` answer `502` for every procedure in the workspace until you correct it.
- Send every field you want to keep. Anything left out of the body is not preserved.
- Set `smartAction` explicitly: `true` lets the AI fill the variables from the conversation, `false` asks the visitor for each one.
- `body` is a string containing JSON, not a nested object.
- Credentials go in `headers` as a `{{placeholder}}` resolved from the visitor's secure profile. A value declared in `variables` ends up in the chat data, which is stored with the conversation and sent to the model.
- Deleting a procedure is not available with an API key, only from the dashboard.

A minimal `api` procedure, with the same UUID in both places:

```json
{
  "actionId": "3f6c1b52-9a4e-4c8d-9f1a-2b7d5e0c4a13",
  "arguments": "3f6c1b52-9a4e-4c8d-9f1a-2b7d5e0c4a13",
  "name": "Look up order status",
  "description": "Retrieves the status of an order from the shop backend.",
  "command": "api",
  "enabled": true,
  "smartAction": true,
  "method": "GET",
  "endpoint": "https://api.example.com/orders/{{orderId}}",
  "body": "{}",
  "bodyType": "json",
  "variables": [{ "name": "orderId", "description": "The order number the visitor is asking about", "type": "string" }]
}
```

### Add a help center FAQ

1. `POST /api/v1/help-center/categories` to create the category, or read an existing `categoryId` from the published help center.
2. `POST /api/v1/help-center/faqs` with `{ "faq": { "question": ..., "categoryId": ..., "websiteId": ... } }`. You supply the question only - the FAQ is stored with `answerStatus: "pending"` and the AI writes the answer from the training data.
3. Poll the FAQ until `answerStatus` leaves `pending`. Use `POST /api/v1/help-center/faqs/retry` if the answer needs rewriting after a training change.
4. `POST /api/v1/help-center/publish` and poll `GET /api/v1/help-center/publish-status`. Nothing reaches visitors until that publish completes.

Note that `faq.websiteId` and `category.websiteId` must be set in the body as well as in the `x-website-id` header, and that the URL slug is derived from the question or name and then kept stable across later edits.

### Edit the content library through the agent

1. `GET /api/v1/wiki/tree` and `GET /api/v1/wiki/pages/{path}` to see what is already there.
2. `POST /api/v1/wiki/chat` with `{ "instruction": ... }`. The response is a stream of plain text, not JSON: the agent's narration arrives incrementally, and the last line is the marker `__WIKI_CHAT_RESULT__` followed by a JSON object with `taskId`, `changesStaged` and `agentSummary`. Split on the marker.
3. Review the staged changes on the task, then `PATCH /api/v1/wiki/tasks/{taskId}/changes/{changeId}` with `{ "action": "accept" }` per change, or `POST /api/v1/wiki/tasks/{taskId}/accept-all` for the lot.
4. `GET /api/v1/wiki/change-log` to confirm what was written. If a change was wrong, `POST /api/v1/wiki/change-log/{id}/restore` with `{ "target": "before" }` undoes it.

The agent never writes directly. A `changesStaged` of zero means it decided nothing needed changing, not that it failed.

### 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`.

## Endpoint reference

Every endpoint below takes the four authentication headers described under Authentication. The catalog above
covers the account management subset; this reference documents the whole API, including the endpoints a website
component calls on a visitor's behalf.

### Accounts

#### get-account-log

`GET /api/v1/accounts/logs` - Get the account log

The audit trail of who changed what in the account: procedure edits, configuration changes, publishes and the rest.

**Notes**

- Covers the last 90 days and returns at most 100 entries, newest first. There is no paging
- Filter by `category` to narrow to one kind of change, and add `categoryId` to follow a single object's history
- `user` filters to one actor's email address

Query parameters:

- `category` (string, optional) - Only return entries in this category, e.g. `ai`.
- `categoryId` (string, optional) - Combined with `category`, follows one object's history.
- `user` (string, optional) - Only return entries by this user's email address.

Responses:

- `200` - Up to 100 log entries from the last 90 days.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `logs` (array of object, optional)

### AI

#### ai-query

`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.

Request body (required):

- `query` (string, required) - The question you want to send to the AI.
- `visitorId` (string, required) - An unique identifier for your visitor.
- `sessionId` (string, required) - An unique identifier for the current session.
- `conversationName` (string, optional) - In case you have multiple topics, you can give each a different name.
- `segmentId` (string, optional) - Optional segment to search in.
- `audienceId` (string, optional) - You can provide an audienceId to be used for content lookup. Use comma-separated id's to supply multiple.
- `metadata` (string, optional) - Send an additional metadata object that we'll show in the dashboard.
- `messageFormat` (string, optional, one of `md`, `html`, default `md`) - You can set this value to 'html' if you want the messages formatted as html instead of markdown.
- `skipLanguageDetection` (boolean, optional, default `false`) - The AI will validate if the question asked is in the same language as the current conversation language. If that doesn't match, the AI will send back a command to switch the conversation language. You can turn off this language check using this property, which means the AI will always respond in the conversation language.
- `chatDataJson` (string, required) - Our chat-data wrapper for managing history. See the response at the right for an example. To maintain a proper history, which is important for follow-up questions and logging, you should supply this with every query (except the first query).

Responses:

- `200` - Successful response containing updated chat data.

`200` response body:

- `history` (array of object, optional) - Array of conversation turns.
  - `type` (string, optional)
  - `timestamp` (integer, optional, default `0`)
  - `uuid` (string, optional)
  - `hidden` (boolean, optional, default `true`)
  - `sender` (string, optional)
  - `message` (string, optional)
  - `metadata` (array of object, optional)
  - `languageCode` (string, optional)
  - `showMetadata` (boolean, optional, default `true`)
  - `canGetFeedback` (boolean, optional, default `true`)
  - `feedback` (string, optional)
- `metadata` (object, optional) - Arbitrary metadata associated with the conversation.
- `toolsetName` (string, nullable, optional)
- `personalizationId` (string, optional)
- `sessionId` (string, optional)
- `conversationName` (string, nullable, optional)
- `conversationId` (string, optional)

#### ai-query-async

`POST /api/v1/ai/async/query` - Sending a question (async)

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

Request body (required):

- `query` (string, required) - The question you want to send to the AI.
- `sessionId` (string, required) - An unique identifier for the current session.
- `visitorId` (string, required) - An unique identifier for your visitor.
- `segmentId` (string, optional) - Optional segment ID to search within for relevant content.
- `metadata` (object, optional) - Optional metadata object displayed in the dashboard.
- `conversationName` (string, optional) - In case you have multiple topics, you can give each a different name.
- `audienceId` (string, optional) - An audienceId to use for content lookup.

Responses:

- `200` - Question accepted. Use `questionId` to poll for the answer.

`200` response body:

- `questionId` (string, uuid, required) - Unique identifier for retrieving the query results later.

#### ai-answer-async

`GET /api/v1/ai/async/answer` - Retrieving an answer (async)

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

Query parameters:

- `questionId` (string, uuid, required) - The questionId returned from `/v1/ai/async/query`.

Responses:

- `200` - Current status and, when complete, the chat data.

`200` response body:

- `questionId` (string, uuid, required)
- `status` (string, required, one of `PENDING`, `PROCESSING`, `DONE`, `ERROR`) - `PENDING` — queued, `PROCESSING` — actively running, `DONE` — completed (chatData present), `ERROR` — failed (error present).
- `chatData` (object, optional) - Conversation state object returned by query endpoints.
  - `history` (array of object, optional) - Array of conversation turns.
    - `type` (string, optional)
    - `timestamp` (integer, optional, default `0`)
    - `uuid` (string, optional)
    - `hidden` (boolean, optional, default `true`)
    - `sender` (string, optional)
    - `message` (string, optional)
    - `metadata` (array of object, optional)
    - `languageCode` (string, optional)
    - `showMetadata` (boolean, optional, default `true`)
    - `canGetFeedback` (boolean, optional, default `true`)
    - `feedback` (string, optional)
  - `metadata` (object, optional) - Arbitrary metadata associated with the conversation.
  - `toolsetName` (string, nullable, optional)
  - `personalizationId` (string, optional)
  - `sessionId` (string, optional)
  - `conversationName` (string, nullable, optional)
  - `conversationId` (string, optional)
- `error` (string, optional)

#### ai-summarization

`POST /api/v1/ai/summarization` - Summarization

Request body (required):

- `prompt` (string, required) - The input of the content you'd like summarized.
- `filterPII` (boolean, optional) - Optionally you can filter any detected personal identifiable information. This means the response of this call will also not contain any PII.
- `instructionsTemplate` (string, optional, one of `EXTRACT_MAIN_QUESTION`, `EXTRACT_MULTIPLE_QUESTIONS`) - Can be either "EXTRACT_MAIN_QUESTION" or "EXTRACT_MULTIPLE_QUESTIONS". If not set, we will generate a summary of the prompt.
- `responseLanguage` (string, optional) - Use this to force a certain response language. If not set, we will automatically detect the language based on the prompt.

Responses:

- `200` - Generated summary.
- `400` - Invalid request parameters.

`200` response body:

- `statusCode` (integer, optional)
- `body` (string, optional) - The generated summary text.

Example `200` response:

```json
{
  "statusCode": 200,
  "body": "Generated response."
}
```

#### ai-similarity

`POST /api/v1/ai/similarity` - Similarity

Request body (required):

- `query` (string, required) - The query you want to base the similarity search on.
- `segmentId` (string, optional) - The segment to search within.
- `maxResults` (integer, optional, default `5`)
- `personalizationId` (string, optional) - Optional personalization ID recorded against the search event.

Responses:

- `200` - Array of similar content pages, ranked by relevance.
- `400` - Invalid request parameters.

`200` response body:

An array of object.
- `source` (string, uri, optional) - URL of the matched source page.
- `title` (string, optional)
- `isPublic` (boolean, optional, default `true`)

Example `200` response:

```json
[
  {
    "source": "https://unless.zendesk.com/agent/tickets/2",
    "title": "Test ticket (#2)",
    "isPublic": false
  },
  {
    "source": "faq",
    "title": "What is the answer to everything and all?",
    "isPublic": false
  }
]
```

#### ai-feedback

`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.

Request body (required):

- `chatDataJson` (string, required) - Use the returned chat-data object from the query to render the answer. Then when an answer received negative feedback (for example using a thumbs-down button), you can set the `feedback` property to the value `negative` for that rated answer. You must always supply this chat-data object submitting feedback.
- `visitorId` (string, required) - An unique identifier for your visitor.
- `feedback` (string, required, one of `negative`, `positive`, `escalated`, `note`) - Can be 'negative', 'positive', 'escalated' or 'note'.

Responses:

- `200`
- `400` - Invalid request parameters.

`200` response body:

- `conversationId` (string, optional)
- `sender` (string, optional)
- `message` (string, optional)
- `type` (string, optional) - When `requireEmail`, the UI should prompt the visitor for their email to escalate the conversation.

Example `200` response:

```json
{
  "conversationId": "d35f69a04d7b252b25f0186b2524e517f96704cc78da7e38a29ab8b92be41617",
  "sender": "bot",
  "message": "I'm sorry to hear that. If you prefer to communicate with a person, kindly provide your email, and someone from support will get in touch with you.",
  "type": "requireEmail"
}
```

#### get-ai-configuration

`GET /api/v1/ai/configuration` - Get AI configuration

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

Responses:

- `200` - The AI configuration for the website.
- `400` - Bad request - missing required parameters.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `accountId` (string, optional)
- `websiteId` (string, optional)
- `mainLanguage` (string, optional) - Two-letter code of the main conversation language.
- `productName` (string, optional) - The product name the AI refers to.
- `customRules` (string, optional) - Free-form instructions that steer the AI's answers.
- `responseLength` (string, optional) - Preferred answer length.
- `suggestReplies` (boolean, optional) - Whether the AI suggests follow-up replies.
- `componentFilterPII` (boolean, optional) - Whether personally identifiable information is filtered.
- `negativeFeedbackReply` (string, optional) - Reply shown after negative feedback.
- `positiveFeedbackReply` (string, optional) - Reply shown after positive feedback.

Example `200` response:

```json
{
  "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "mainLanguage": "en",
  "productName": "Unless",
  "customRules": "Always answer in a friendly tone.",
  "responseLength": "long",
  "suggestReplies": true,
  "componentFilterPII": true,
  "negativeFeedbackReply": "I'm sorry to hear that.",
  "positiveFeedbackReply": "Thank you for your feedback."
}
```

#### update-ai-configuration

`POST /api/v1/ai/configuration` - Update the AI configuration

Update how the AI behaves for a workspace: its main language, product name, custom rules, response length, PII filtering and feedback replies.

**Notes**

- A partial configuration is accepted. Fields you leave out keep their stored value, and derived attributes are recomputed from the merged result
- `accountId` and `websiteId` come from the auth headers; anything the body says about them is ignored
- Toggling `optimizerEnabled` clears the training-data content hashes, which makes the next ingest re-process every source
- A `translationCorpus` whose rows have no recognised language column is rejected with `400`. That normally means the CSV headers are not language names

Request body (required):

- `mainLanguage` (string, optional) - Primary language the AI answers in, as an ISO code.
- `productName` (string, optional) - Name the AI uses for the product it supports.
- `customRules` (string, nullable, optional) - Extra instructions applied to every answer.
- `responseLength` (string, optional) - How long answers should be, e.g. `Max`.
- `suggestReplies` (boolean, optional)
- `componentFilterPII` (boolean, optional)
- `filterPIIByDefault` (boolean, optional)
- `allowedPIIWords` (array of string, optional) - Words never treated as PII, however the detector scores them.
- `allowAllLanguages` (boolean, optional)
- `restrictedAllowedLanguages` (array of string, optional) - The languages the AI may answer in when `allowAllLanguages` is false.
- `negativeFeedbackReply` (string, optional)
- `positiveFeedbackReply` (string, optional)
- `optimizerEnabled` (boolean, optional) - Changing this clears the training-data hashes and forces a full re-ingest.
- `topK` (number, optional) - How many documents are retrieved per question.
- `modelTemperatureForAnswers` (number, optional)
- `translationCorpus` (object, optional) - Glossary of terms that must translate a fixed way. Rows are keyed by language name.

Example request:

```json
{
  "productName": "Unless",
  "customRules": "Always answer in a friendly tone.",
  "responseLength": "Max",
  "suggestReplies": true
}
```

Responses:

- `200` - The stored configuration.
- `400` - Bad request - missing body or websiteId, or an invalid translation corpus.
- `401` - Unauthorized - invalid API key.

#### get-quality-control-questions

`GET /api/v1/ai/quality/control` - Get quality control questions

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

Responses:

- `200` - The control questions for the website.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `items` (array of object, optional)
  - `accountId` (string, optional)
  - `websiteId` (string, optional)
  - `id` (string, optional) - Unique identifier for this control question.
  - `question` (string, optional)
  - `answer` (string, optional) - The answer the AI is graded against.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the question was created.
  - `segmentId` (string, nullable, optional)
  - `audienceId` (string, nullable, optional)

Example `200` response:

```json
{
  "items": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "question": "How long does delivery take?",
      "answer": "Standard delivery takes three to five working days.",
      "createdAt": 1641081600000
    }
  ]
}
```

#### upsert-quality-control-question

`POST /api/v1/ai/quality/control` - Create or update a quality control question

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

Request body (required):

- `id` (string, optional) - Omit to create a new question, send it to update an existing one.
- `question` (string, required)
- `answer` (string, required) - The answer the AI is graded against.
- `segmentId` (string, optional)
- `audienceId` (string, optional)

Example request:

```json
{
  "question": "How long does delivery take?",
  "answer": "Standard delivery takes three to five working days."
}
```

Responses:

- `200` - The question was created or updated.
- `400` - Bad request - missing request body or websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `message` (string, optional)

Example `200` response:

```json
{
  "message": "Quality control question upserted"
}
```

#### get-quality-control-reports

`GET /api/v1/ai/quality/control/report` - Get quality control reports

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

Query parameters:

- `reportId` (string, optional) - Return the per-question results for this report instead of the report list.

Responses:

- `200` - The report list, or the per-question results when `reportId` is given.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `data` (array of one of several shapes, optional)
  - variant 1 (object)
    - `accountId` (string, optional)
    - `websiteId` (string, optional)
    - `reportId` (string, optional) - Unique identifier for this run, as returned when it was started.
    - `timestamp` (number, optional) - Unix timestamp (ms) of the most recently scored question in this run.
    - `grade` (number, optional) - Average grade out of 10 across the questions scored so far.
    - `totalQuestionsInReport` (number, optional) - How many control questions the run was started with.
    - `passed` (number, optional) - How many scored questions graded 6 or above.
    - `processed` (number, optional) - How many questions have been scored. Can stop below totalQuestionsInReport, since failed questions are dropped rather than retried.
  - variant 2 (object)
    - `accountId` (string, optional)
    - `websiteId` (string, optional)
    - `reportId` (string, optional)
    - `timestamp` (number, optional) - Unix timestamp (ms) when this question was scored.
    - `questionCreatedAt` (number, optional)
    - `grade` (number, optional) - Grade out of 10 for this answer.
    - `controlQuestion` (string, optional) - The question that was asked.
    - `controlAnswer` (string, optional) - The expected answer.
    - `generatedAnswer` (string, optional) - What the AI actually answered.
    - `explanation` (string, optional) - Why the grader awarded this grade.
    - `questionWasUnanswered` (boolean, optional) - True when the AI declined to answer at all.
    - `questionId` (string, optional)
    - `qualityControlQuestionId` (string, optional)
    - `metadata` (string, optional)
    - `totalQuestionsInReport` (number, optional)

Example `200` response:

```json
{
  "data": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "reportId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "timestamp": 1641081900000,
      "grade": 8,
      "totalQuestionsInReport": 12,
      "passed": 10,
      "processed": 12
    }
  ]
}
```

#### generate-quality-control-report

`POST /api/v1/ai/quality/control/report` - Generate a quality control report

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

Responses:

- `200` - Report generation was started.
- `400` - Bad request - missing websiteId, or the website has no control questions.
- `401` - Unauthorized - invalid API key.
- `403` - The account's plan does not include quality control reports.

`200` response body:

- `message` (string, optional)
- `reportId` (string, optional) - Poll the report list for this id.

Example `200` response:

```json
{
  "message": "Report generation initiated",
  "reportId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

#### get-procedures

`GET /api/v1/ai/actions` - Get procedures

List the procedures (AI actions) configured for the workspace. A procedure is something the AI can do beyond answering, for example call an API, invoke an MCP tool or hand the conversation to a human.

**Notes**

- Scoped to the workspace in `x-website-id`; there is no query parameter to widen or narrow it
- `documentsTriggeringAction` lists the training documents that reference each procedure, so you can see which knowledge triggers it
- Procedures saved before the `enabled` flag existed are returned as enabled and backfilled on read, so a procedure never comes back without one
- Every procedure in the workspace is validated as one batch, so a single stored procedure holding a value the schema rejects, such as a variable `type` of `number`, makes this endpoint answer `502` for the whole workspace rather than skipping that one procedure. The failure lasts as long as the procedure does; fix it with the create or update endpoint

Responses:

- `200` - Successful response with the workspace's procedures.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.
- `502` - A stored procedure in the workspace failed schema validation, for instance a variable with a `type` outside the documented enum. No procedures are returned until it is corrected.

`200` response body:

- `actions` (array of object, optional)
  - `accountId` (string, optional)
  - `websiteId` (string, optional)
  - `actionId` (string, optional) - Unique identifier for this procedure.
  - `name` (string, optional) - Display name, also what the AI refers to when it decides to run the procedure.
  - `description` (string, nullable, optional) - What the procedure does. The AI reads this when deciding whether it applies.
  - `command` (string, optional, one of `api`, `mcp`, `direct-answer`, `rephrase-and-answer`, `redirect-to-url`, `start-experience`, `start-live-chat`, `execute-custom-javascript`, `switch-segment`, `send-message-to-segment`, `send-message-to-segment-and-switch`, `resend-message-to-segment-and-switch`, `open-image`, `open-video`) - What the procedure executes. `api` calls an HTTP endpoint, `mcp` invokes an MCP tool, `direct-answer` supplies a fixed answer, and the rest drive the component. Each command reads its input from `arguments`.
  - `arguments` (string, nullable, optional) - The command's input. What it has to contain depends on `command`, and a procedure whose `arguments` is wrong is stored happily but fails when the AI tries to run it. For `api` and `rephrase-and-answer` it must be the procedure's own `actionId`. For `mcp` it is the MCP tool ID, for `direct-answer` the answer text, for `redirect-to-url` the URL, for `start-experience` the component ID, for `execute-custom-javascript` the code, for the segment commands the topic (and message), and for `open-image` and `open-video` the media URL. Only `start-live-chat` takes no input.
  - `enabled` (boolean, nullable, optional) - Whether the procedure is live. Procedures saved before this field existed are treated as enabled and backfilled on read.
  - `smartAction` (boolean, nullable, optional) - When true the AI fills the variables from the conversation. When false the visitor is asked for each one.
  - `escalate` (boolean, optional) - Whether running this procedure hands the conversation to a human.
  - `guidance` (string, nullable, optional) - Extra instructions for the AI on how to use this procedure.
  - `variables` (array of object, optional) - The values collected before the procedure runs, referenced as `{{name}}` in the request.
    - `name` (string, optional) - Referenced as `{{name}}` in the endpoint, headers or body.
    - `description` (string, optional) - What the value is. Shown to the visitor, and what the AI matches against when filling it itself.
    - `type` (string, optional, one of `string`, `multiline`, `email`, `regex`, `select`) - How the value is collected and validated. `string` is a single-line input, `multiline` a text area, `email` validates an address, `regex` validates against the `regex` field, and `select` offers the `options` as a dropdown. No other value is accepted: `number`, `boolean`, `date` and `phone` are reserved but not implemented, and storing one of them makes the get procedures endpoint fail for the whole workspace.
    - `optional` (boolean, optional)
    - `forceCollection` (boolean, optional) - Ask the visitor even when the AI could infer the value.
    - `regex` (string, nullable, optional) - Pattern the collected value must match.
    - `options` (array of object, nullable, optional) - Fixed set of choices offered to the visitor.
      - `label` (string, optional)
      - `value` (string, optional)
    - `dependsOnVariable` (string, nullable, optional) - Only collect this variable when the named variable has `dependsOnVariableValue`.
    - `dependsOnVariableValue` (string, nullable, optional)
  - `availability` (string, nullable, optional, one of `ALWAYS`, `DURING_OPENING_HOURS`, `OUTSIDE_OPENING_HOURS`)
  - `lifecycleStage` (string, nullable, optional, one of `support`, `newSales`, `retention`, `salesExpansion`)
  - `enabledSegments` (array of string, nullable, optional) - Restrict the procedure to these AI segments. Empty or absent means every segment.
  - `enabledAudiences` (array of string, nullable, optional) - Restrict the procedure to these audiences.
  - `preCollectionMessage` (string, nullable, optional) - Shown before the visitor is asked for the variables.
  - `preExecuteMessage` (string, nullable, optional) - Shown just before the procedure executes.
  - `immediatelyExecute` (boolean, nullable, optional)
  - `triggerAfterResponse` (boolean, optional)
  - `triggerAfterUnanswered` (boolean, optional) - Run the procedure when the AI could not answer the question.
  - `triggerAfterTimeout` (boolean, optional)
  - `triggerAfterTimeoutTime` (number, optional) - Seconds of inactivity before the timeout trigger fires.
  - `skipTranslation` (boolean, nullable, optional)
  - `javascriptDisplayTrigger` (string, nullable, optional) - JavaScript expression that must be truthy for the procedure to be offered.
  - `documentsTriggeringAction` (array of object, optional) - Training documents that reference this procedure, so you can see which knowledge triggers it. Read-only, and only returned by the list operation.

Example `200` response:

```json
{
  "actions": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "actionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "arguments": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "name": "Look up order status",
      "description": "Retrieves the status of an order from the shop backend.",
      "command": "api",
      "enabled": true,
      "smartAction": true,
      "escalate": false,
      "method": "GET",
      "endpoint": "https://api.example.com/orders/{{orderId}}",
      "headers": {
        "x-api-key": "{{shopApiKey}}"
      },
      "body": "{}",
      "bodyType": "json",
      "variables": [
        {
          "name": "orderId",
          "description": "The order number the visitor is asking about",
          "type": "string"
        }
      ],
      "onSuccessPostActions": [
        {
          "command": "display-message",
          "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
          "text": "Here is the status of your order."
        }
      ],
      "onErrorPostActions": [],
      "lifecycleStage": "support",
      "documentsTriggeringAction": []
    }
  ]
}
```

#### upsert-procedure

`POST /api/v1/ai/actions` - Create or update a procedure

Create a new procedure, or update an existing one by passing its `actionId`. The body is the whole procedure, not a patch: fields you leave out are not preserved, so fetch the procedure first and send it back with your changes applied.

**Use cases**

- **Automation**: Give the AI an API call it can make on the visitor's behalf
- **Account setup**: Provision the standard procedures for a new workspace
- **Maintenance**: Enable, disable or retarget an existing procedure by passing its `actionId`

**Notes**

- Send `arguments`. It is the command's input and is required, and for `command: api` it must be the procedure's own `actionId`, because the AI dispatches the procedure as `/api <arguments>`
- Generate the `actionId` yourself when creating a procedure, so `arguments` can carry the same value in the same request. The API generates one when you omit it, which is too late for `arguments` to reference
- `accountId` and `websiteId` are taken from the auth headers; anything you send in the body for them is overwritten
- Nothing in the body is validated on write. A procedure missing a field is stored and returns `200`, then fails the first time the AI runs it, so read it back with the get procedures endpoint instead of trusting the status code
- A value the schema rejects is the worst case: a variable `type` of `number` is stored happily and then makes the get procedures endpoint answer `502` for every procedure in the workspace. Stick to the documented enums
- Set `smartAction` explicitly. It decides whether the AI fills the variables from the conversation or the visitor is asked for each one, and it is stored as sent
- `body` is a string containing JSON, not a nested object
- Credentials belong in `headers` as a `{{placeholder}}` resolved from the visitor's secure profile. A value declared in `variables` ends up in the chat data, which is stored with the conversation and sent to the model
- Deleting a procedure is not available with an API key, only from the dashboard

Request body (required):

The procedure to store. Common fields are described by the Procedure schema; the properties below are the ones an `api` procedure adds.

- `accountId` (string, optional)
- `websiteId` (string, optional)
- `actionId` (string, optional) - ID of the procedure. Pass the ID of an existing procedure to update it. To create one, generate a UUID yourself and send it here, so `arguments` can carry the same value in the same request. Omitting it makes the API generate an ID, which leaves an `api` procedure with no way to reference itself.
- `name` (string, required) - Display name, also what the AI refers to when it decides to run the procedure.
- `description` (string, nullable, optional) - What the procedure does. The AI reads this when deciding whether it applies.
- `command` (string, required, one of `api`, `mcp`, `direct-answer`, `rephrase-and-answer`, `redirect-to-url`, `start-experience`, `start-live-chat`, `execute-custom-javascript`, `switch-segment`, `send-message-to-segment`, `send-message-to-segment-and-switch`, `resend-message-to-segment-and-switch`, `open-image`, `open-video`) - What the procedure executes. `api` calls an HTTP endpoint, `mcp` invokes an MCP tool, `direct-answer` supplies a fixed answer, and the rest drive the component. Each command reads its input from `arguments`.
- `arguments` (string, nullable, required) - The command's input. What it has to contain depends on `command`, and a procedure whose `arguments` is wrong is stored happily but fails when the AI tries to run it. For `api` and `rephrase-and-answer` it must be the procedure's own `actionId`. For `mcp` it is the MCP tool ID, for `direct-answer` the answer text, for `redirect-to-url` the URL, for `start-experience` the component ID, for `execute-custom-javascript` the code, for the segment commands the topic (and message), and for `open-image` and `open-video` the media URL. Only `start-live-chat` takes no input.
- `enabled` (boolean, nullable, optional) - Whether the procedure is live. Procedures saved before this field existed are treated as enabled and backfilled on read.
- `smartAction` (boolean, nullable, optional) - When true the AI fills the variables from the conversation. When false the visitor is asked for each one.
- `escalate` (boolean, optional) - Whether running this procedure hands the conversation to a human.
- `guidance` (string, nullable, optional) - Extra instructions for the AI on how to use this procedure.
- `variables` (array of object, optional) - The values collected before the procedure runs, referenced as `{{name}}` in the request.
  - `name` (string, optional) - Referenced as `{{name}}` in the endpoint, headers or body.
  - `description` (string, optional) - What the value is. Shown to the visitor, and what the AI matches against when filling it itself.
  - `type` (string, optional, one of `string`, `multiline`, `email`, `regex`, `select`) - How the value is collected and validated. `string` is a single-line input, `multiline` a text area, `email` validates an address, `regex` validates against the `regex` field, and `select` offers the `options` as a dropdown. No other value is accepted: `number`, `boolean`, `date` and `phone` are reserved but not implemented, and storing one of them makes the get procedures endpoint fail for the whole workspace.
  - `optional` (boolean, optional)
  - `forceCollection` (boolean, optional) - Ask the visitor even when the AI could infer the value.
  - `regex` (string, nullable, optional) - Pattern the collected value must match.
  - `options` (array of object, nullable, optional) - Fixed set of choices offered to the visitor.
    - `label` (string, optional)
    - `value` (string, optional)
  - `dependsOnVariable` (string, nullable, optional) - Only collect this variable when the named variable has `dependsOnVariableValue`.
  - `dependsOnVariableValue` (string, nullable, optional)
- `availability` (string, nullable, optional, one of `ALWAYS`, `DURING_OPENING_HOURS`, `OUTSIDE_OPENING_HOURS`)
- `lifecycleStage` (string, nullable, optional, one of `support`, `newSales`, `retention`, `salesExpansion`)
- `enabledSegments` (array of string, nullable, optional) - Restrict the procedure to these AI segments. Empty or absent means every segment.
- `enabledAudiences` (array of string, nullable, optional) - Restrict the procedure to these audiences.
- `preCollectionMessage` (string, nullable, optional) - Shown before the visitor is asked for the variables.
- `preExecuteMessage` (string, nullable, optional) - Shown just before the procedure executes.
- `immediatelyExecute` (boolean, nullable, optional)
- `triggerAfterResponse` (boolean, optional)
- `triggerAfterUnanswered` (boolean, optional) - Run the procedure when the AI could not answer the question.
- `triggerAfterTimeout` (boolean, optional)
- `triggerAfterTimeoutTime` (number, optional) - Seconds of inactivity before the timeout trigger fires.
- `skipTranslation` (boolean, nullable, optional)
- `javascriptDisplayTrigger` (string, nullable, optional) - JavaScript expression that must be truthy for the procedure to be offered.
- `documentsTriggeringAction` (array of object, optional) - Training documents that reference this procedure, so you can see which knowledge triggers it. Read-only, and only returned by the list operation.
- `method` (string, optional, one of `GET`, `POST`) - HTTP method of the call the procedure makes.
- `endpoint` (string, nullable, optional) - URL to call. Supports `{{variable}}` placeholders.
- `headers` (object, optional) - Request headers. The only place a credential placeholder may appear.
  - keyed by name, each value is a string
- `body` (string, nullable, optional) - Request body as a string. For a JSON body this is JSON encoded as a string, not a nested object.
- `bodyType` (string, optional, one of `json`, `plain`)
- `bodySchema` (object, nullable, optional) - Schema the dashboard uses to validate the body. Not enforced by the API.
- `transformer` (string, nullable, optional) - JavaScript that reshapes the API response before the AI uses it.
- `onSuccessPostActions` (array of object, optional) - What happens after a successful call.
  - `command` (string, required, one of `display-message`, `call-query`) - What the step does. `display-message` shows text in the chat, translated into the conversation language.
  - `id` (string, optional) - Identifier for the step. Generated when omitted.
  - `text` (string, optional) - The text to show. Only for `display-message`.
  - `query` (string, optional) - The question to send back to the AI. Only for `call-query`.
  - `promptLabel` (string, optional) - Label shown for the follow-up query. Only for `call-query`.
  - `systemPrompt` (string, optional) - System prompt used for the follow-up query. Only for `call-query`.
- `onErrorPostActions` (array of object, optional) - What happens after a failed call.
  - `command` (string, required, one of `display-message`, `call-query`) - What the step does. `display-message` shows text in the chat, translated into the conversation language.
  - `id` (string, optional) - Identifier for the step. Generated when omitted.
  - `text` (string, optional) - The text to show. Only for `display-message`.
  - `query` (string, optional) - The question to send back to the AI. Only for `call-query`.
  - `promptLabel` (string, optional) - Label shown for the follow-up query. Only for `call-query`.
  - `systemPrompt` (string, optional) - System prompt used for the follow-up query. Only for `call-query`.

Example request:

```json
{
  "actionId": "3f6c1b52-9a4e-4c8d-9f1a-2b7d5e0c4a13",
  "arguments": "3f6c1b52-9a4e-4c8d-9f1a-2b7d5e0c4a13",
  "name": "Look up order status",
  "description": "Retrieves the status of an order from the shop backend.",
  "command": "api",
  "enabled": true,
  "smartAction": true,
  "method": "GET",
  "endpoint": "https://api.example.com/orders/{{orderId}}",
  "headers": {
    "x-api-key": "{{shopApiKey}}"
  },
  "body": "{}",
  "bodyType": "json",
  "variables": [
    {
      "name": "orderId",
      "description": "The order number the visitor is asking about",
      "type": "string"
    }
  ],
  "onSuccessPostActions": [
    {
      "command": "display-message",
      "text": "Here is the status of your order."
    }
  ],
  "onErrorPostActions": []
}
```

Responses:

- `200` - The procedure was created or updated.
- `400` - Bad request - missing body or websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `actionId` (string, optional) - ID of the procedure that was written. Echoes the `actionId` you sent, or the generated one when you created a procedure.
- `message` (string, optional)

Example `200` response:

```json
{
  "actionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "message": "AI action upserted"
}
```

### Audiences

#### get-audiences

`GET /api/v1/audiences` - Get audiences

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

Query parameters:

- `ruleId` (string, uuid, optional) - Fetch a single audience by its ID. When provided, the response shape is `{ audience }` instead of `{ audiences }`.
- `state` (string, optional, one of `live`, `stopped`, default `live`) - Filter by audience state. Defaults to `live` (all non-stopped audiences). Use `stopped` to retrieve stopped audiences.

Responses:

- `200` - Successful response with list of audiences.
- `400` - Bad request - invalid parameters.
- `401` - Unauthorized - invalid API key.
- `403` - Forbidden - insufficient permissions.

`200` response body:

- `audiences` (array of object, optional)
  - `accountId` (string, optional) - The account ID that owns this audience.
  - `websiteId` (string, optional) - The website this audience belongs to.
  - `ruleId` (string, optional) - Unique identifier for this audience.
  - `name` (string, optional) - Display name of the audience.
  - `state` (string, optional, one of `draft`, `live`, `stopped`) - Current state of the audience.
  - `rule` (string, optional) - JSON string containing the targeting rule expression.
  - `duration` (one of several shapes, optional) - How long a visitor remains in this audience. Use `session` for session-scoped membership.
    - variant 1 (number)
    - variant 2 (string, one of `session`)
  - `durationType` (string, optional, one of `minutes`, `hours`, `days`) - Unit for the `duration` field.
  - `createdDate` (number, optional) - Unix timestamp (ms) when the audience was created.
  - `updatedDate` (number, optional) - Unix timestamp (ms) when the audience was last updated.
  - `disabled` (boolean, optional) - Whether the audience is disabled. Disabled audiences are excluded from evaluation.
  - `readOnly` (boolean, optional) - Whether this audience can be modified.
  - `audienceTags` (array of string, optional) - Tags used to categorise and filter audiences.

Example `200` response:

```json
{
  "audiences": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "ruleId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "name": "Homepage hero optimization",
      "state": "live",
      "prio": 100,
      "rule": "{\"and\":[{\"field\":\"url\",\"operator\":\"equals\",\"value\":\"/\"}]}",
      "startDate": 1640995200000,
      "createdDate": 1640995200000,
      "updatedDate": 1641081600000,
      "webPagePath": "/",
      "exclusive": false,
      "isComponent": false,
      "variants": [
        {
          "id": "control",
          "name": "Control",
          "weight": 50
        },
        {
          "id": "variation-1",
          "name": "New hero design",
          "weight": 50
        }
      ]
    }
  ]
}
```

#### upsert-audience

`POST /api/v1/audiences` - Create or update an audience

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

Request body (required):

- `ruleId` (string, optional) - ID of an existing audience to update. Omit to create a new audience.
- `name` (string, required) - Display name of the audience.
- `rule` (string, required) - JSON string containing the targeting rule expression.
- `state` (string, required, one of `draft`, `live`, `stopped`) - State of the audience.
- `duration` (one of several shapes, optional) - How long a visitor remains in this audience. Use `session` for session-scoped membership.
  - variant 1 (number)
  - variant 2 (string, one of `session`)
- `durationType` (string, optional, one of `minutes`, `hours`, `days`) - Unit for the `duration` field.
- `audienceTags` (array of string, optional) - Tags used to categorise and filter audiences.

Responses:

- `200` - The created or updated audience.
- `400` - Bad request - missing or invalid body.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `audience` (object, optional)
  - `accountId` (string, optional) - The account ID that owns this audience.
  - `websiteId` (string, optional) - The website this audience belongs to.
  - `ruleId` (string, optional) - Unique identifier for this audience.
  - `name` (string, optional) - Display name of the audience.
  - `state` (string, optional, one of `draft`, `live`, `stopped`) - Current state of the audience.
  - `rule` (string, optional) - JSON string containing the targeting rule expression.
  - `duration` (one of several shapes, optional) - How long a visitor remains in this audience. Use `session` for session-scoped membership.
    - variant 1 (number)
    - variant 2 (string, one of `session`)
  - `durationType` (string, optional, one of `minutes`, `hours`, `days`) - Unit for the `duration` field.
  - `createdDate` (number, optional) - Unix timestamp (ms) when the audience was created.
  - `updatedDate` (number, optional) - Unix timestamp (ms) when the audience was last updated.
  - `disabled` (boolean, optional) - Whether the audience is disabled. Disabled audiences are excluded from evaluation.
  - `readOnly` (boolean, optional) - Whether this audience can be modified.
  - `audienceTags` (array of string, optional) - Tags used to categorise and filter audiences.

Example `200` response:

```json
{
  "audience": {
    "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ruleId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "name": "Returning visitors",
    "state": "live",
    "rule": "{\"and\":[{\"field\":\"visits\",\"operator\":\"greaterThan\",\"value\":\"1\"}]}",
    "duration": 30,
    "durationType": "days",
    "audienceTags": [
      "lifecycle"
    ]
  }
}
```

### Components

#### get-custom-components

`GET /api/v1/components/custom-components` - Get custom components

List the custom components available to the account: its own, plus the public and built-in AI ones.

**Notes**

- Disabled components are left out
- `createdAt` and `updatedAt` are stripped from the response
- Creating, updating and deleting custom components are not available with an API key, only from the dashboard

Responses:

- `200` - The available custom components.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `components` (array of object, optional)

### Conversations

#### get-conversations

`GET /api/v1/visitors/conversations` - Get conversations

List the workspace's conversations, or fetch one conversation's transcript.

**Notes**

- Pass `conversationId` or `conversationUrl` to get a single transcript instead of a page of the list. The response shape differs between the two modes
- Without either, the result is a page of the list, sorted by `createdAt` descending unless you say otherwise
- Use `GET /api/v1/ai/conversations/details` instead when you want the analysis of a conversation rather than its messages

Query parameters:

- `conversationId` (string, optional) - Fetch this conversation's transcript instead of the list.
- `conversationUrl` (string, optional) - Fetch a transcript by its storage key. Takes precedence over `conversationId`.
- `page` (integer, optional)
- `rowsPerPage` (integer, optional)
- `sortField` (string, optional) - Field to sort on. Defaults to `createdAt`.
- `sortDirection` (string, optional, one of `asc`, `desc`, default `desc`)
- `filterField` (string, optional)
- `filterValue` (string, optional)
- `filterOperator` (string, optional, one of `contains`, `is`)

Responses:

- `200` - A page of conversations, or one conversation's transcript.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.
- `500` - The conversationId or conversationUrl did not resolve to a conversation.

#### get-conversation-details

`GET /api/v1/ai/conversations/details` - Get conversation details

The stored analysis of one conversation: its metadata, sentiment, rating, outcome and tags.

**Notes**

- `conversations__conversationId` is required
- A conversation belonging to another account or workspace answers `404`, not `403`
- Use `GET /api/v1/visitors/conversations` with a `conversationId` when you want the messages rather than the analysis

Query parameters:

- `conversations__conversationId` (string, required) - The conversation's id.
- `conversationUrl` (string, optional)

Responses:

- `200` - The conversation's details.
- `400` - Bad request - missing websiteId or conversation id.
- `401` - Unauthorized - invalid API key.
- `404` - The conversation does not belong to this account and workspace.

### Help center

#### publish-help-center

`POST /api/v1/help-center/publish` - Publish the help center

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

Request body (optional):

- `scope` (object, optional) - What to publish. Defaults to a full publish.
  - `type` (string, optional, one of `full`, `partial`, `structural`) - `full` rebuilds everything, `partial` republishes only the FAQs in `faqIds`, `structural` republishes structure and settings without regenerating articles.
  - `faqIds` (array of string, optional) - FAQ IDs to republish, only used with the `partial` type.

Responses:

- `202` - The publish was accepted and is running in the background.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.
- `409` - A publish is already in progress for this website.

`202` response body:

- `job` (object, optional)
  - `jobId` (string, optional) - Unique identifier for this publish job.
  - `accountId` (string, optional)
  - `websiteId` (string, optional)
  - `status` (string, optional) - Current status of the job, e.g. `queued`, `running`, `succeeded` or `failed`.
  - `scope` (object, optional) - What was published, see the publish endpoint for the scope shape.
  - `triggeredBy` (string, optional) - Email address of the user (or API caller) that started the publish.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the job was created.
  - `startedAt` (number, optional) - Unix timestamp (ms) when the job started.
  - `finishedAt` (number, nullable, optional) - Unix timestamp (ms) when the job finished, absent while running.
  - `error` (string, nullable, optional) - Error message when the job failed.
  - `failedFaqIds` (array of string, optional) - FAQ IDs that failed to publish, if any.

Example `202` response:

```json
{
  "job": {
    "jobId": "20260101T120000-abcd",
    "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "status": "queued",
    "scope": {
      "type": "full"
    },
    "triggeredBy": "user@example.com",
    "createdAt": 1641081600000,
    "startedAt": 1641081600000,
    "failedFaqIds": []
  }
}
```

#### get-help-center-publish-status

`GET /api/v1/help-center/publish-status` - Get help center publish status

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

Responses:

- `200` - The publish state and recent jobs.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `jobs` (array of object, optional)
  - `jobId` (string, optional) - Unique identifier for this publish job.
  - `accountId` (string, optional)
  - `websiteId` (string, optional)
  - `status` (string, optional) - Current status of the job, e.g. `queued`, `running`, `succeeded` or `failed`.
  - `scope` (object, optional) - What was published, see the publish endpoint for the scope shape.
  - `triggeredBy` (string, optional) - Email address of the user (or API caller) that started the publish.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the job was created.
  - `startedAt` (number, optional) - Unix timestamp (ms) when the job started.
  - `finishedAt` (number, nullable, optional) - Unix timestamp (ms) when the job finished, absent while running.
  - `error` (string, nullable, optional) - Error message when the job failed.
  - `failedFaqIds` (array of string, optional) - FAQ IDs that failed to publish, if any.
- `state` (object, nullable, optional) - The per-website publish coordinator, `null` when the website has never been published.
  - `runningJobId` (string, nullable, optional) - ID of the currently running job, `null` when idle.
  - `leaseExpiresAt` (number, nullable, optional)

Example `200` response:

```json
{
  "jobs": [
    {
      "jobId": "20260101T120000-abcd",
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "status": "succeeded",
      "scope": {
        "type": "full"
      },
      "triggeredBy": "user@example.com",
      "createdAt": 1641081600000,
      "startedAt": 1641081600000,
      "finishedAt": 1641081900000,
      "failedFaqIds": []
    }
  ],
  "state": {
    "runningJobId": null,
    "leaseExpiresAt": null
  }
}
```

#### upsert-help-center-category

`POST /api/v1/help-center/categories` - Create or update a help center category

Create a category, or update an existing one by passing its `categoryId`.

**Notes**

- `category.websiteId` must be set in the body as well as in the `x-website-id` header
- The URL slug is derived from the name and kept unique within the workspace. An existing category keeps its slug across a rename so published URLs stay stable; pass `slug` explicitly to override
- Changes only reach visitors after `POST /api/v1/help-center/publish`
- Deleting a category is not available with an API key, only from the dashboard

Request body (required):

- `category` (object, required)
  - `categoryId` (string, optional) - ID of an existing category to update. Omit to create a new one.
  - `name` (string, required)
  - `websiteId` (string, required)
  - `description` (string, nullable, optional)
  - `order` (number, optional) - Sort position. Defaults to 0.
  - `slug` (string, optional) - URL slug. Derived from the name when omitted.
  - `translationContext` (string, nullable, optional)

Example request:

```json
{
  "category": {
    "name": "Billing",
    "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "description": "Invoices, plans and payment questions.",
    "order": 1
  }
}
```

Responses:

- `200` - The stored category.
- `400` - Bad request - missing websiteId, category name or category websiteId.
- `401` - Unauthorized - invalid API key.

#### reorder-help-center-categories

`POST /api/v1/help-center/categories/reorder` - Reorder help center categories

Set the sort position of several categories at once.

**Notes**

- Only the categories you list are touched; the rest keep their order
- Changes only reach visitors after `POST /api/v1/help-center/publish`

Request body (required):

- `items` (array of object, required)
  - `categoryId` (string, required)
  - `order` (number, required)

Example request:

```json
{
  "items": [
    {
      "categoryId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "order": 0
    },
    {
      "categoryId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
      "order": 1
    }
  ]
}
```

Responses:

- `200` - The categories were reordered.
- `400` - Bad request - missing websiteId, or an empty items list.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `success` (boolean, optional)

#### upsert-help-center-faq

`POST /api/v1/help-center/faqs` - Create or update a help center FAQ

Create an FAQ, or update an existing one by passing its `faqId`.

**Notes**

- `faq.websiteId` must be set in the body as well as in the `x-website-id` header, and `faq.categoryId` must name an existing category
- You supply the question, not the answer. The FAQ is stored with `answerStatus: pending` and the AI generates the answer from the training data
- The URL slug is derived from the question and kept unique within the category. An existing FAQ keeps its slug when the question is edited; pass `slug` explicitly to override
- `generationPrompt` steers how the answer is written
- Changes only reach visitors after `POST /api/v1/help-center/publish`
- Deleting an FAQ is not available with an API key, only from the dashboard

Request body (required):

- `faq` (object, required)
  - `faqId` (string, optional) - ID of an existing FAQ to update. Omit to create a new one.
  - `question` (string, required)
  - `categoryId` (string, required)
  - `websiteId` (string, required)
  - `generationPrompt` (string, nullable, optional) - Extra instruction for the AI writing the answer.
  - `order` (number, optional) - Sort position within the category. Defaults to 0.
  - `slug` (string, optional) - URL slug. Derived from the question when omitted.

Example request:

```json
{
  "faq": {
    "question": "How long is the refund window?",
    "categoryId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "websiteId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
    "generationPrompt": "Answer in two sentences and mention the contact form."
  }
}
```

Responses:

- `200` - The stored FAQ, with `answerStatus` set to `pending`.
- `400` - Bad request - missing websiteId, question, categoryId or FAQ websiteId.
- `401` - Unauthorized - invalid API key.

#### reorder-help-center-faqs

`POST /api/v1/help-center/faqs/reorder` - Reorder help center FAQs

Set the sort position of several FAQs at once.

**Notes**

- Only the FAQs you list are touched; the rest keep their order
- Changes only reach visitors after `POST /api/v1/help-center/publish`

Request body (required):

- `items` (array of object, required)
  - `faqId` (string, required)
  - `order` (number, required)

Example request:

```json
{
  "items": [
    {
      "faqId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "order": 0
    }
  ]
}
```

Responses:

- `200` - The FAQs were reordered.
- `400` - Bad request - missing websiteId, or an empty items list.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `success` (boolean, optional)

#### retry-help-center-faq

`POST /api/v1/help-center/faqs/retry` - Regenerate a help center FAQ answer

Ask the AI to write an FAQ's answer again, for instance after the training data changed.

**Notes**

- An FAQ already at `answerStatus: pending` is returned unchanged rather than queued twice, so a repeated call is safe
- Answers `404` when the `faqId` does not exist

Request body (required):

- `faqId` (string, required)

Example request:

```json
{
  "faqId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

Responses:

- `200` - The FAQ, with `answerStatus` set to `pending`.
- `400` - Bad request - missing websiteId or faqId.
- `401` - Unauthorized - invalid API key.
- `404` - No FAQ with that id.

### Insights

#### get-insights-pageviews

`GET /api/v1/insights/pageviews` - Get pageview totals

Pageview totals for a date range, split by whether the visitor was new or returning and whether the pageview was personalized.

**Notes**

- `startTime` and `endTime` are required and are `YYYY-MM-DD` days, not timestamps. The range is inclusive
- Results are served from a query cache. `status` tells you whether the numbers are ready, and `lastUpdate` when they were computed
- Pass `forceRefresh=true` to recompute instead of reading the cache. It is slow, so use it only when you know the cache is stale

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - Pageview totals for the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional) - Whether the cached numbers are ready.
- `lastUpdate` (number, optional) - Unix timestamp (ms) when the numbers were computed.
- `day` (string, optional)
- `new` (object, optional)
  - `pageviewsPersonalized` (number, optional)
  - `pageviewsNotPersonalized` (number, optional)
- `isReturning` (object, optional)
  - `pageviewsPersonalized` (number, optional)
  - `pageviewsNotPersonalized` (number, optional)

Example `200` response:

```json
{
  "status": "ready",
  "lastUpdate": 1757462400000,
  "new": {
    "pageviewsPersonalized": 4210,
    "pageviewsNotPersonalized": 18734
  },
  "isReturning": {
    "pageviewsPersonalized": 1980,
    "pageviewsNotPersonalized": 6402
  }
}
```

#### get-insights-pageviews-per-day

`GET /api/v1/insights/pageviews/per-day` - Get pageviews per day

The same pageview split as the totals endpoint, but one entry per day in the range. Days with no traffic are returned with zeroes rather than omitted.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - One entry per day in the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `days` (array of object, optional)
  - `status` (string, optional) - Whether the cached numbers are ready.
  - `lastUpdate` (number, optional) - Unix timestamp (ms) when the numbers were computed.
  - `day` (string, optional)
  - `new` (object, optional)
    - `pageviewsPersonalized` (number, optional)
    - `pageviewsNotPersonalized` (number, optional)
  - `isReturning` (object, optional)
    - `pageviewsPersonalized` (number, optional)
    - `pageviewsNotPersonalized` (number, optional)

#### get-insights-outcomes

`GET /api/v1/insights/outcomes` - Get outcome totals

How many outcomes were recorded in the range. An outcome is a conversion event the AI or a component contributed to.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - Outcome total for the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `outcomes` (number, optional)

Example `200` response:

```json
{
  "status": "ready",
  "lastUpdate": 1757462400000,
  "outcomes": 143
}
```

#### get-insights-outcomes-per-day

`GET /api/v1/insights/outcomes/per-day` - Get outcomes per day

Outcome counts, one entry per day in the range. Filter to a single experience with `personalizationId`.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.
- `personalizationId` (string, optional) - Restrict the counts to one personalization.

Responses:

- `200` - One entry per day in the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `days` (array of object, optional)
  - `day` (string, optional)
  - `outcomes` (number, optional)

#### get-insights-audiences-per-day

`GET /api/v1/insights/audiences/per-day` - Get audience sessions per day

How many sessions one audience matched per day on one domain, split by new and returning visitors. Both `audienceId` and `domainName` are required - there is no unfiltered variant.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.
- `audienceId` (string, required) - The audience to count sessions for.
- `domainName` (string, required) - The domain to count sessions for.

Responses:

- `200` - One entry per day in the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `days` (array of object, optional)
  - `day` (string, optional)
  - `new` (object, optional)
    - `sessions` (number, optional)
    - `name` (string, optional) - Name of the audience the sessions matched.
  - `isReturning` (object, optional)
    - `sessions` (number, optional)
    - `name` (string, optional) - Name of the audience the sessions matched.

#### get-insights-component-events

`GET /api/v1/insights/components/events` - Get component event totals

Engagement totals per personalization variation: views, CTA clicks, closes, and the AI chat counters (responses, unanswered responses, searches, conversations, sessions, thumbs up/down, escalations).

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - Totals per variation.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `personalizations` (array of object, optional)
  - `day` (string, optional)
  - `variationId` (string, optional)
  - `variationName` (string, optional)
  - `new` (object, optional)
    - `views` (number, optional)
    - `primaryctaclicks` (number, optional)
    - `secondaryctaclicks` (number, optional)
    - `closes` (number, optional)
    - `chatresponses` (number, optional)
    - `chatunansweredresponses` (number, optional) - Responses where the AI could not answer.
    - `chatsearches` (number, optional)
    - `chatconversations` (number, optional)
    - `chatstartingresponses` (number, optional)
    - `chatconversationswithoutstartingmessage` (number, optional)
    - `chatsessions` (number, optional) - Derived from chatstartingresponses plus chatconversationswithoutstartingmessage.
    - `chatpositives` (number, optional)
    - `chatnegatives` (number, optional)
    - `chatescalations` (number, optional)
    - `successratio` (number, optional)
  - `isReturning` (object, optional)
    - `views` (number, optional)
    - `primaryctaclicks` (number, optional)
    - `secondaryctaclicks` (number, optional)
    - `closes` (number, optional)
    - `chatresponses` (number, optional)
    - `chatunansweredresponses` (number, optional) - Responses where the AI could not answer.
    - `chatsearches` (number, optional)
    - `chatconversations` (number, optional)
    - `chatstartingresponses` (number, optional)
    - `chatconversationswithoutstartingmessage` (number, optional)
    - `chatsessions` (number, optional) - Derived from chatstartingresponses plus chatconversationswithoutstartingmessage.
    - `chatpositives` (number, optional)
    - `chatnegatives` (number, optional)
    - `chatescalations` (number, optional)
    - `successratio` (number, optional)

#### get-insights-component-events-per-day

`GET /api/v1/insights/components/events/per-day` - Get component events per day

The same engagement counters as the totals endpoint, one entry per variation per day.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.
- `personalizationId` (string, optional) - Restrict the counters to one personalization.

Responses:

- `200` - One entry per variation per day.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `days` (array of object, optional)
  - `day` (string, optional)
  - `variationId` (string, optional)
  - `variationName` (string, optional)
  - `new` (object, optional)
    - `views` (number, optional)
    - `primaryctaclicks` (number, optional)
    - `secondaryctaclicks` (number, optional)
    - `closes` (number, optional)
    - `chatresponses` (number, optional)
    - `chatunansweredresponses` (number, optional) - Responses where the AI could not answer.
    - `chatsearches` (number, optional)
    - `chatconversations` (number, optional)
    - `chatstartingresponses` (number, optional)
    - `chatconversationswithoutstartingmessage` (number, optional)
    - `chatsessions` (number, optional) - Derived from chatstartingresponses plus chatconversationswithoutstartingmessage.
    - `chatpositives` (number, optional)
    - `chatnegatives` (number, optional)
    - `chatescalations` (number, optional)
    - `successratio` (number, optional)
  - `isReturning` (object, optional)
    - `views` (number, optional)
    - `primaryctaclicks` (number, optional)
    - `secondaryctaclicks` (number, optional)
    - `closes` (number, optional)
    - `chatresponses` (number, optional)
    - `chatunansweredresponses` (number, optional) - Responses where the AI could not answer.
    - `chatsearches` (number, optional)
    - `chatconversations` (number, optional)
    - `chatstartingresponses` (number, optional)
    - `chatconversationswithoutstartingmessage` (number, optional)
    - `chatsessions` (number, optional) - Derived from chatstartingresponses plus chatconversationswithoutstartingmessage.
    - `chatpositives` (number, optional)
    - `chatnegatives` (number, optional)
    - `chatescalations` (number, optional)
    - `successratio` (number, optional)

#### get-insights-wiki-events

`GET /api/v1/insights/wiki/events` - Get content library event totals

How many content library (wiki) updates the AI made in the range, and the tokens it spent doing so.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - Totals for the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `total` (object, optional)
  - `updates` (number, optional)
  - `tokensInput` (number, optional)
  - `tokensOutput` (number, optional)

#### get-insights-wiki-events-per-day

`GET /api/v1/insights/wiki/events/per-day` - Get content library events per day

Content library updates and token spend, one entry per day in the range.

Query parameters:

- `startTime` (string, date, required) - First day of the range, inclusive, as `YYYY-MM-DD`.
- `endTime` (string, date, required) - Last day of the range, inclusive, as `YYYY-MM-DD`.
- `forceRefresh` (boolean, optional, default `false`) - Recompute instead of reading the query cache. Slow - only worth it when you know the cache is stale.

Responses:

- `200` - One entry per day in the range.
- `401` - Unauthorized - invalid API key.
- `500` - startTime or endTime was missing.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `days` (array of object, optional)
  - `day` (string, optional)
  - `updates` (number, optional)
  - `tokensInput` (number, optional)
  - `tokensOutput` (number, optional)

#### get-insights-roi

`GET /api/v1/insights/roi` - Get ROI insights

The two halves of the ROI calculation: what the platform did (`UnlessUsageStatistics`, broken down by AI assistant, team assistant and API) and the business figures it is measured against (`WebsiteBusinessStatistics`).

**Notes**

- Needs no date range; it reports over the account's full history
- `WebsiteBusinessStatistics` is `null` until the figures have been entered, which is dashboard-only. Without them there is nothing to measure the usage against

Responses:

- `200` - Usage and business statistics for the workspace.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `UnlessUsageStatistics` (object, optional) - What the platform handled, split by the surface it came through.
  - `aiAssistantInsights` (object, optional)
    - `sessions` (number, optional)
    - `conversations` (number, optional)
    - `questions` (number, optional)
    - `answeredQuestions` (number, optional)
    - `escalations` (number, optional)
  - `teamAssistantInsights` (object, optional)
    - `sessions` (number, optional)
    - `conversations` (number, optional)
    - `questions` (number, optional)
    - `answeredQuestions` (number, optional)
    - `summarizations` (number, optional)
  - `apiInsights` (object, optional)
    - `sessions` (number, optional)
    - `conversations` (number, optional)
    - `questions` (number, optional)
    - `answeredQuestions` (number, optional)
    - `summarizations` (number, optional)
  - `accountWideInsights` (object, optional)
    - `totalConversations` (number, optional)
- `WebsiteBusinessStatistics` (object, nullable, optional) - The business figures the ROI calculation is measured against.
  - `accountId` (string, optional)
  - `websiteId` (string, optional)
  - `averageMonthlyTicketsWithoutAI` (number, optional)
  - `averageHumanHandlingTimePerTicketMinutes` (number, optional)
  - `averageHumanHandlingTimePerTicketWithAIMinutes` (number, optional)
  - `averageHourlyRateForTeamMember` (number, optional)
  - `expectedAnnualCompanyGrowthRate` (number, optional)
  - `countApiAsTeamEfficiency` (boolean, optional)
  - `weeklyDashboardHours` (number, optional)
  - `createdAt` (number, optional)
  - `updatedAt` (number, optional)

#### get-insights-ai-maturity

`GET /api/v1/insights/ai-maturity` - Get AI maturity

The account's AI maturity assessment: an overall score, a per-stage and per-level summary, the full stage-by-level matrix with the individual features behind each cell, and the certificates earned.

**Notes**

- Every feature carries `implemented`, `measurable`, `measurement` and `skipped`, so an agent can tell a genuine gap from something that cannot be measured
- `deeplink` on a feature points at the dashboard page where it is configured
- A cell's `blockingPrerequisite` explains a score of zero that is caused by a missing deployment rather than missing work
- Read-only. Skipping a maturity task and granting a certificate are dashboard-only

Responses:

- `200` - The maturity assessment.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `status` (string, optional)
- `lastUpdate` (number, optional)
- `totalScore` (number, nullable, optional)
- `measurableLevelsCount` (number, optional)
- `measurableFeaturesCount` (number, optional)
- `totalFeaturesCount` (number, optional)
- `levelDistribution` (array of object, optional)
  - `levelId` (string, optional)
  - `levelLabel` (string, optional)
  - `percentage` (number, optional)
  - `score` (number, nullable, optional)
- `stages` (array of object, optional) - One summary per lifecycle stage.
  - `id` (string, optional, one of `acquisition`, `retention`, `sales-expansion`, `customer-support`)
  - `label` (string, optional)
  - `score` (number, nullable, optional)
  - `maturityLevelId` (string, nullable, optional)
  - `maturityLevelLabel` (string, nullable, optional)
  - `description` (string, optional)
- `levels` (array of object, optional) - One summary per maturity level.
  - `id` (string, optional, one of `experimental`, `assisted`, `automated`, `agentic`, `ai-first`)
  - `label` (string, optional)
  - `score` (number, nullable, optional)
  - `achievedStagesCount` (number, optional)
  - `totalStages` (number, optional)
  - `description` (string, optional)
- `matrix` (array of object, optional) - One cell per stage and level combination.
  - `levelId` (string, optional)
  - `levelLabel` (string, optional)
  - `stageId` (string, optional)
  - `stageLabel` (string, optional)
  - `score` (number, nullable, optional)
  - `completedFeatures` (number, optional)
  - `totalFeatures` (number, optional)
  - `measurableFeatures` (number, optional)
  - `certificateAchieved` (boolean, optional)
  - `features` (array of object, optional)
    - `key` (string, optional) - Stable identifier for the feature.
    - `label` (string, optional)
    - `description` (string, optional)
    - `implemented` (boolean, optional)
    - `measurable` (boolean, optional)
    - `measurement` (string, optional, one of `direct`, `approximate`, `not-supported`) - How reliably the feature can be measured.
    - `deeplink` (string, optional) - Dashboard page where the feature is configured.
    - `skipped` (boolean, optional)
    - `perStage` (boolean, optional) - Whether skipping or completing the feature counts per stage rather than globally.
    - `autoDerived` (boolean, optional) - System-derived gating task, excluded from the inbox and not skippable.
  - `blockingPrerequisite` (object, optional) - Set when the cell score is zeroed by a missing prerequisite deployment.
    - `label` (string, optional)
    - `deeplink` (string, optional)
- `certificates` (array of object, optional)
  - `id` (string, optional) - Stable identifier for the certificate.
  - `levelId` (string, optional)
  - `levelLabel` (string, optional)
  - `stageId` (string, optional)
  - `stageLabel` (string, optional)
  - `achieved` (boolean, optional)
  - `eligible` (boolean, optional)
  - `granted` (boolean, optional)
  - `grantedAt` (string, nullable, optional)
  - `score` (number, nullable, optional)
  - `completedFeatures` (number, optional)
  - `totalFeatures` (number, optional)
  - `measurableFeatures` (number, optional)
  - `features` (array of object, optional)
    - `key` (string, optional) - Stable identifier for the feature.
    - `label` (string, optional)
    - `description` (string, optional)
    - `implemented` (boolean, optional)
    - `measurable` (boolean, optional)
    - `measurement` (string, optional, one of `direct`, `approximate`, `not-supported`) - How reliably the feature can be measured.
    - `deeplink` (string, optional) - Dashboard page where the feature is configured.
    - `skipped` (boolean, optional)
    - `perStage` (boolean, optional) - Whether skipping or completing the feature counts per stage rather than globally.
    - `autoDerived` (boolean, optional) - System-derived gating task, excluded from the inbox and not skippable.

### Personalizations

#### get-personalizations

`GET /api/v1/personalizations` - Get personalizations

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

Responses:

- `200` - Successful response with array of personalizations.
- `400` - Bad request - invalid parameters.
- `401` - Unauthorized - invalid API key.
- `403` - Forbidden - insufficient permissions.
- `404` - Not found - invalid account or website ID.

`200` response body:

An array of object.
- `accountId` (string, nullable, optional) - The account ID that owns this personalization.
- `assignee` (string, nullable, optional) - User assigned to this personalization.
- `changed` (boolean, nullable, optional) - Whether the personalization has been modified.
- `componentAccountId` (string, nullable, optional) - Account ID for component-based personalizations.
- `componentPackageName` (string, nullable, optional) - Package name for component-based personalizations.
- `componentSettings` (string, nullable, optional) - JSON string containing component configuration.
- `componentType` (string, nullable, optional) - Type of component used.
- `componentVersion` (number, nullable, optional) - Version of the component.
- `createdAt` (number, nullable, optional) - Timestamp when personalization was created.
- `createdDate` (number, nullable, optional) - Creation date timestamp.
- `delayOption` (string, nullable, optional) - Delay trigger option (e.g., "seconds", "pageviews").
- `delayValue` (number, nullable, optional) - Delay value for trigger timing.
- `diffUrl` (string, nullable, optional) - URL for viewing changes diff.
- `disabled` (boolean, nullable, optional) - Whether the personalization is disabled.
- `endDate` (number, nullable, optional) - Timestamp when personalization should stop running.
- `excludedRule` (string, nullable, optional) - Rules for excluding visitors.
- `exclusive` (boolean, nullable, optional) - Whether this personalization is exclusive.
- `hideOption` (string, nullable, optional) - Option for hiding the personalization.
- `hideValue` (number, nullable, optional) - Value for hide timing.
- `isComponent` (boolean, nullable, optional) - Whether this is a component-based personalization.
- `isHiddenAiComponent` (boolean, nullable, optional) - Whether this is a hidden AI component.
- `javascriptTrigger` (string, nullable, optional) - JavaScript trigger code.
- `javascriptTriggerParsed` (string, nullable, optional) - Parsed JavaScript trigger.
- `longurl` (string, nullable, optional) - Extended URL information.
- `maxValue` (number, nullable, optional) - Maximum value for certain triggers.
- `personalizationTags` (array of string, nullable, optional) - Array of tags associated with this personalization.
- `prio` (number, nullable, optional) - Priority level (higher numbers = higher priority).
- `recipe` (string, nullable, optional) - Personalization recipe/template.
- `redirectUrl` (string, nullable, optional) - URL for redirect-type personalizations.
- `rule` (string, nullable, optional) - Targeting rules in JSON format.
- `startDate` (number, nullable, optional) - Timestamp when personalization should start running.
- `state` (string, nullable, optional) - Current state (e.g., "running", "paused", "draft").
- `targetTemplateDomain` (string, nullable, optional) - Target domain for template.
- `targetTemplatePath` (string, nullable, optional) - Target path for template.
- `testMode` (string, nullable, optional) - Test mode configuration.
- `updatedDate` (number, nullable, optional) - Timestamp of last update.
- `variants` (array of object, nullable, optional) - Array of personalization variants.
- `variationId` (string, nullable, optional) - ID of the variation.
- `variationName` (string, nullable, optional) - Name of the variation.
- `webPagePath` (string, nullable, optional) - Web page path where personalization applies.
- `websiteId` (string, nullable, optional) - Website ID this personalization belongs to.

Example `200` response:

```json
[
  {
    "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "variationId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "variationName": "Homepage Hero Optimization",
    "state": "running",
    "disabled": false,
    "prio": 100,
    "rule": "{\"and\":[{\"field\":\"url\",\"operator\":\"equals\",\"value\":\"/\"}]}",
    "startDate": 1640995200000,
    "endDate": null,
    "createdAt": 1640995200000,
    "updatedDate": 1641081600000,
    "webPagePath": "/",
    "personalizationTags": [
      "homepage",
      "hero",
      "conversion"
    ],
    "testMode": "live",
    "exclusive": false,
    "isComponent": false,
    "variants": [
      {
        "id": "control",
        "name": "Control",
        "weight": 50
      },
      {
        "id": "variation-1",
        "name": "New Hero Design",
        "weight": 50
      }
    ]
  }
]
```

#### upsert-personalization

`POST /api/v1/personalizations` - Create or update a personalization

Create a new personalization (experience), or update an existing one by passing its `variationId`.

**Notes**

- `websiteId` is taken from the auth headers and overwrites whatever the body says
- `variationName` is required; the request is rejected without it
- Deleting a personalization, duplicating one and managing themes are not available with an API key, only from the dashboard

Request body (required):

- `accountId` (string, nullable, optional) - The account ID that owns this personalization.
- `assignee` (string, nullable, optional) - User assigned to this personalization.
- `changed` (boolean, nullable, optional) - Whether the personalization has been modified.
- `componentAccountId` (string, nullable, optional) - Account ID for component-based personalizations.
- `componentPackageName` (string, nullable, optional) - Package name for component-based personalizations.
- `componentSettings` (string, nullable, optional) - JSON string containing component configuration.
- `componentType` (string, nullable, optional) - Type of component used.
- `componentVersion` (number, nullable, optional) - Version of the component.
- `createdAt` (number, nullable, optional) - Timestamp when personalization was created.
- `createdDate` (number, nullable, optional) - Creation date timestamp.
- `delayOption` (string, nullable, optional) - Delay trigger option (e.g., "seconds", "pageviews").
- `delayValue` (number, nullable, optional) - Delay value for trigger timing.
- `diffUrl` (string, nullable, optional) - URL for viewing changes diff.
- `disabled` (boolean, nullable, optional) - Whether the personalization is disabled.
- `endDate` (number, nullable, optional) - Timestamp when personalization should stop running.
- `excludedRule` (string, nullable, optional) - Rules for excluding visitors.
- `exclusive` (boolean, nullable, optional) - Whether this personalization is exclusive.
- `hideOption` (string, nullable, optional) - Option for hiding the personalization.
- `hideValue` (number, nullable, optional) - Value for hide timing.
- `isComponent` (boolean, nullable, optional) - Whether this is a component-based personalization.
- `isHiddenAiComponent` (boolean, nullable, optional) - Whether this is a hidden AI component.
- `javascriptTrigger` (string, nullable, optional) - JavaScript trigger code.
- `javascriptTriggerParsed` (string, nullable, optional) - Parsed JavaScript trigger.
- `longurl` (string, nullable, optional) - Extended URL information.
- `maxValue` (number, nullable, optional) - Maximum value for certain triggers.
- `personalizationTags` (array of string, nullable, optional) - Array of tags associated with this personalization.
- `prio` (number, nullable, optional) - Priority level (higher numbers = higher priority).
- `recipe` (string, nullable, optional) - Personalization recipe/template.
- `redirectUrl` (string, nullable, optional) - URL for redirect-type personalizations.
- `rule` (string, nullable, optional) - Targeting rules in JSON format.
- `startDate` (number, nullable, optional) - Timestamp when personalization should start running.
- `state` (string, nullable, optional) - Current state (e.g., "running", "paused", "draft").
- `targetTemplateDomain` (string, nullable, optional) - Target domain for template.
- `targetTemplatePath` (string, nullable, optional) - Target path for template.
- `testMode` (string, nullable, optional) - Test mode configuration.
- `updatedDate` (number, nullable, optional) - Timestamp of last update.
- `variants` (array of object, nullable, optional) - Array of personalization variants.
- `variationId` (string, optional) - ID of an existing personalization to update. Omit to create a new one.
- `variationName` (string, required) - Name of the variation being saved.
- `webPagePath` (string, nullable, optional) - Web page path where personalization applies.
- `websiteId` (string, nullable, optional) - Website ID this personalization belongs to.

Example request:

```json
{
  "name": "Homepage hero test",
  "variationName": "Control",
  "state": "draft",
  "webPagePath": "/"
}
```

Responses:

- `200` - The created or updated personalization.
- `400` - Bad request - missing websiteId or variationName.
- `401` - Unauthorized - invalid API key.

### Tasks

#### get-tasks

`GET /api/v1/tasks` - Get tasks

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

Query parameters:

- `websiteId` (string, required) - The website to list tasks for.
- `state` (string, optional, one of `open`, `done`, `deleted`) - Only return tasks in this state.

Responses:

- `200` - The matching tasks with a total count.
- `400` - Bad request - missing websiteId.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `tasks` (array of object, optional)
  - `accountId` (string, optional) - The account ID that owns this task.
  - `taskId` (string, optional) - Unique identifier for this task.
  - `name` (string, optional) - Short title of the task.
  - `description` (string, nullable, optional) - Longer explanation of what the task is about.
  - `type` (string, optional, one of `knowledge-suggestion`, `knowledge-gap-detected`, `review-flagged-conversation`, `new-sales-opportunity`, `retention-warning`, `sales-expansion-opportunity`, `wiki-suggestion`, `maturity-task`) - The kind of work this task represents.
  - `source` (string, optional, one of `team-assistant`, `ai`, `dashboard`, `ingestion`, `slack`) - Where the task originated.
  - `state` (string, optional, one of `open`, `done`, `deleted`) - Current state of the task.
  - `data` (object, nullable, optional) - Arbitrary structured payload attached to the task, depends on the task type.
  - `assignee` (string, nullable, optional) - Email address of the user the task is assigned to.
  - `websiteId` (string, nullable, optional) - The website this task belongs to.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the task was created.
  - `updatedAt` (number, optional) - Unix timestamp (ms) when the task was last updated.
- `count` (integer, optional)

Example `200` response:

```json
{
  "tasks": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "taskId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "name": "Add a refund policy article",
      "description": "Several visitors asked about refunds and the AI had no source to answer from.",
      "type": "knowledge-gap-detected",
      "source": "ai",
      "state": "open",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "createdAt": 1640995200000,
      "updatedAt": 1641081600000
    }
  ],
  "count": 1
}
```

#### create-task

`POST /api/v1/tasks` - Create a task

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

Request body (required):

- `name` (string, required) - Short title of the task.
- `description` (string, required) - Longer explanation of what the task is about.
- `type` (string, required, one of `knowledge-suggestion`, `knowledge-gap-detected`, `review-flagged-conversation`, `new-sales-opportunity`, `retention-warning`, `sales-expansion-opportunity`, `wiki-suggestion`, `maturity-task`) - The kind of work this task represents.
- `source` (string, required, one of `team-assistant`, `ai`, `dashboard`, `ingestion`, `slack`) - Where the task originated.
- `data` (object, optional) - Optional structured payload attached to the task.

Responses:

- `200` - The created task.
- `400` - Bad request - missing required fields.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `task` (object, optional)
  - `accountId` (string, optional) - The account ID that owns this task.
  - `taskId` (string, optional) - Unique identifier for this task.
  - `name` (string, optional) - Short title of the task.
  - `description` (string, nullable, optional) - Longer explanation of what the task is about.
  - `type` (string, optional, one of `knowledge-suggestion`, `knowledge-gap-detected`, `review-flagged-conversation`, `new-sales-opportunity`, `retention-warning`, `sales-expansion-opportunity`, `wiki-suggestion`, `maturity-task`) - The kind of work this task represents.
  - `source` (string, optional, one of `team-assistant`, `ai`, `dashboard`, `ingestion`, `slack`) - Where the task originated.
  - `state` (string, optional, one of `open`, `done`, `deleted`) - Current state of the task.
  - `data` (object, nullable, optional) - Arbitrary structured payload attached to the task, depends on the task type.
  - `assignee` (string, nullable, optional) - Email address of the user the task is assigned to.
  - `websiteId` (string, nullable, optional) - The website this task belongs to.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the task was created.
  - `updatedAt` (number, optional) - Unix timestamp (ms) when the task was last updated.

#### update-task

`PUT /api/v1/tasks` - Update a task

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`

Request body (required):

- `taskId` (string, required) - ID of the task to update.
- `websiteId` (string, required) - The website the task belongs to.
- `state` (string, optional, one of `open`, `done`, `deleted`) - New state of the task.
- `name` (string, optional) - New title of the task.
- `description` (string, optional) - New description of the task.
- `assignee` (string, optional) - Email address to assign the task to. Pass an empty string to unassign.
- `data` (object, optional) - Replacement structured payload for the task.

Responses:

- `200` - The updated task.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `task` (object, optional)
  - `accountId` (string, optional) - The account ID that owns this task.
  - `taskId` (string, optional) - Unique identifier for this task.
  - `name` (string, optional) - Short title of the task.
  - `description` (string, nullable, optional) - Longer explanation of what the task is about.
  - `type` (string, optional, one of `knowledge-suggestion`, `knowledge-gap-detected`, `review-flagged-conversation`, `new-sales-opportunity`, `retention-warning`, `sales-expansion-opportunity`, `wiki-suggestion`, `maturity-task`) - The kind of work this task represents.
  - `source` (string, optional, one of `team-assistant`, `ai`, `dashboard`, `ingestion`, `slack`) - Where the task originated.
  - `state` (string, optional, one of `open`, `done`, `deleted`) - Current state of the task.
  - `data` (object, nullable, optional) - Arbitrary structured payload attached to the task, depends on the task type.
  - `assignee` (string, nullable, optional) - Email address of the user the task is assigned to.
  - `websiteId` (string, nullable, optional) - The website this task belongs to.
  - `createdAt` (number, optional) - Unix timestamp (ms) when the task was created.
  - `updatedAt` (number, optional) - Unix timestamp (ms) when the task was last updated.

### Training

#### get-training-data

`GET /api/v1/ai/training` - Get training data

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

Query parameters:

- `trainingDataId` (string, optional) - Fetch a single training data source by its ID.

Responses:

- `200` - The training data sources.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `trainingData` (array of object, optional)

Example `200` response:

```json
{
  "trainingData": [
    {
      "accountId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "trainingDataId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "type": "url",
      "method": "all",
      "url": "https://www.example.com",
      "name": "Example website",
      "isPublic": true,
      "schedule": "Weekly",
      "indexedUrlsCount": 124,
      "totalUrlsCount": 130,
      "lastScanTimestamp": 1641081600000
    }
  ]
}
```

#### ingest-training-data

`POST /api/v1/ai/training/data` - Ingest a training data source

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

Request body (required):

- `trainingData` (object, required)
  - `trainingDataId` (string, optional) - ID of an existing source to update. Omit to create a new source.
  - `accountId` (string, required) - Must match the `x-account-id` header.
  - `websiteId` (string, required) - Must match the `x-website-id` header.
  - `type` (string, required) - Kind of source. Use `url` for website ingestion, other values include `faq`, `pdf`, `docx`, `md` and `csv`.
  - `method` (string, required, one of `single`, `all`, `sitemap`) - How to crawl a `url` source. `all` follows links from the entry point, `single` ingests one page, `sitemap` reads the sitemap.
  - `url` (string, optional) - Entry point URL for a `url` source.
  - `name` (string, optional) - Display name of the source.
  - `isPublic` (boolean, required) - Whether content from this source may be shown to anonymous visitors.
  - `include` (array of string, optional) - Path prefixes to include while crawling.
  - `exclude` (array of string, optional) - Path prefixes to exclude while crawling.
  - `schedule` (string, optional, one of `Daily`, `Weekly`, `Monthly`, `Never`) - How often the source is rescanned.
- `skipRetrain` (boolean, optional) - Skip retraining after saving, only store the source configuration.

Responses:

- `200` - The source was saved and ingestion was started.
- `401` - Unauthorized - invalid API key.

`200` response body:

- `message` (string, optional)

Example `200` response:

```json
{
  "message": "Training data upserted"
}
```

### Wiki

#### get-wiki-tree

`GET /api/v1/wiki/tree` - Browse the content library

List the content library (wiki) entries directly under a path. Call it repeatedly to walk the tree.

**Notes**

- Scoped to the workspace in `x-website-id`
- A very wide directory is cut short and the response carries `truncated: true`

Query parameters:

- `path` (string, optional, default `/`) - Directory to list. Defaults to the root, `/`.

Responses:

- `200` - The entries under the path.
- `401` - Unauthorized - invalid API key.
- `404` - Not found - the website does not belong to the account.
- `500` - Bad request - missing websiteId.

`200` response body:

- `path` (string, optional)
- `children` (array of object, optional)
- `truncated` (boolean, optional) - Present and true only when the listing was cut short.

#### get-wiki-page

`GET /api/v1/wiki/pages/{path}` - Read a content library page

Read one content library page by its path. Use the browse endpoint to discover paths.

Path parameters:

- `path` (string, required) - Page path without the leading slash, e.g. `billing/refunds`.

Responses:

- `200` - The page.
- `401` - Unauthorized - invalid API key.
- `404` - Not found - no page at that path, or the website does not belong to the account.
- `500` - Bad request - missing websiteId or path.

#### get-wiki-change-log

`GET /api/v1/wiki/change-log` - Get the content library change log

The history of content library edits, newest first, with the before and after content of each change.

**Notes**

- The before and after snapshots are returned raw; nothing computes a diff for you
- `rolledBackAt` is set on a change that has been restored away
- `limit` defaults to 50 and is capped at 200. An unparseable `limit` or `offset` falls back to the default rather than erroring

Query parameters:

- `path` (string, optional) - Only return changes to this page path.
- `limit` (integer, optional, default `50`) - How many entries to return. Capped at 200.
- `offset` (integer, optional, default `0`)

Responses:

- `200` - Change log entries.
- `401` - Unauthorized - invalid API key.
- `404` - Not found - the website does not belong to the account.
- `500` - Bad request - missing websiteId.

`200` response body:

- `entries` (array of object, optional)
  - `id` (integer, optional) - Pass this to the restore endpoint.
  - `wikiPath` (string, optional)
  - `operation` (string, optional)
  - `semanticOperation` (string, optional)
  - `beforeContent` (string, nullable, optional)
  - `afterContent` (string, nullable, optional)
  - `beforeVersion` (string, nullable, optional)
  - `afterVersion` (string, nullable, optional)
  - `sourceUrl` (string, nullable, optional)
  - `agentSummary` (string, nullable, optional) - The agent's own account of why it made the change.
  - `trigger` (string, optional) - What caused the change, e.g. an import or an agent instruction.
  - `triggeredByEmail` (string, nullable, optional)
  - `changeGroupId` (string, nullable, optional) - Changes applied together share a group id.
  - `appliedAt` (string, nullable, optional)
  - `rolledBackAt` (string, nullable, optional) - Set once the change has been restored away.
- `limit` (integer, optional)
- `offset` (integer, optional)

#### restore-wiki-change

`POST /api/v1/wiki/change-log/{id}/restore` - Restore a content library change

Roll a page back to one side of a logged change.

**Notes**

- `target` picks which snapshot to write back: `before` undoes the change, `after` re-applies it. Anything other than `after` is treated as `before`
- The restore is itself recorded in the change log

Path parameters:

- `id` (integer, required) - Numeric change-log entry id.

Request body (optional):

- `target` (string, optional, one of `before`, `after`, default `before`)

Example request:

```json
{
  "target": "before"
}
```

Responses:

- `200` - The change was restored.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - missing websiteId, or an invalid change-log id.

`200` response body:

- `id` (integer, optional)
- `restored` (boolean, optional)

#### wiki-chat

`POST /api/v1/wiki/chat` - Instruct the content library agent

Give the content library agent an instruction in natural language. It reads and edits the library, streams its reasoning back as plain text, and stages the resulting changes as a task for review.

**Notes**

- This endpoint streams. The body is plain text, not JSON, and arrives incrementally
- The final line is the marker `__WIKI_CHAT_RESULT__` followed by a JSON object with `taskId`, `changesStaged`, `agentSummary` and `sessionId`. Split on the marker to separate the narration from the result
- Changes are staged, not applied. Accept them with the task endpoints below
- Pass the previous `sessionId` back to continue a conversation. Omit it and a new one is generated

Request body (required):

- `instruction` (string, required) - What the agent should do.
- `sessionId` (string, optional) - Continue an earlier session. Generated when omitted.

Example request:

```json
{
  "instruction": "Add a page explaining our refund window under billing."
}
```

Responses:

- `200` - The agent's narration, streamed as plain text, ending in the result marker.
- `400` - Bad request - missing instruction, accountId or websiteId.
- `401` - Unauthorized - invalid API key.

#### force-wiki-import

`POST /api/v1/wiki/force-import` - Force a content library import

Re-import a source URL into the content library, ignoring whatever the importer already thinks it knows about that page.

**Notes**

- Answers `202` as soon as the import is handed off; it runs asynchronously. Watch the change log for the result
- A `500` here means the import could not be started, and the failure is recorded against the source

Request body (required):

- `sourceUrl` (string, required) - The URL to re-import.

Example request:

```json
{
  "sourceUrl": "https://www.example.com/help/refunds"
}
```

Responses:

- `202` - The import was started.
- `400` - Bad request - missing websiteId or sourceUrl.
- `401` - Unauthorized - invalid API key.
- `500` - The import could not be started.

`202` response body:

- `status` (string, optional)

Example `202` response:

```json
{
  "status": "importing"
}
```

#### update-wiki-task-change

`PATCH /api/v1/wiki/tasks/{taskId}/changes/{changeId}` - Accept, deny or retry one staged change

Act on a single change inside a content library suggestion task.

**Notes**

- `accept` writes the change to the library, `deny` discards it, `retry` re-runs a change that failed to apply
- `accept` and `deny` answer `200` with the task's new state. `retry` answers `202`, because it runs asynchronously

Path parameters:

- `taskId` (string, required)
- `changeId` (string, required)

Request body (required):

- `action` (string, required, one of `accept`, `deny`, `retry`)

Example request:

```json
{
  "action": "accept"
}
```

Responses:

- `200` - The change was accepted or denied.
- `202` - A retry was started.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - missing ids, an invalid action, or the task is not a content library suggestion.

`200` response body:

- `changeId` (string, optional)
- `changeStatus` (string, optional, one of `accepted`, `denied`)
- `taskState` (string, optional)

`202` response body:

- `changeId` (string, optional)
- `changeStatus` (string, optional, one of `retrying`)

#### accept-all-wiki-task-changes

`POST /api/v1/wiki/tasks/{taskId}/accept-all` - Accept every staged change in a task

Accept all unresolved changes in one content library suggestion task.

**Notes**

- Answers `202` and applies the changes asynchronously; poll the task to see the result

Path parameters:

- `taskId` (string, required)

Responses:

- `202` - The accept-all run was started.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - missing taskId.

#### deny-all-wiki-task-changes

`POST /api/v1/wiki/tasks/{taskId}/deny-all` - Deny every staged change in a task

Discard all unresolved changes in one content library suggestion task.

**Notes**

- Runs synchronously and reports how many changes it denied and the task's resulting state
- Grouped changes are denied once per group, so `changesDenied` counts groups rather than individual edits

Path parameters:

- `taskId` (string, required)

Responses:

- `200` - The changes were denied.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - missing taskId, or the task is not a content library suggestion.

`200` response body:

- `changesDenied` (number, optional)
- `taskState` (string, optional)

#### bulk-accept-wiki-tasks

`POST /api/v1/wiki/tasks/bulk-accept` - Accept several suggestion tasks

Accept every unresolved change across a list of content library suggestion tasks.

**Notes**

- Answers `202` and runs asynchronously

Request body (required):

- `taskIds` (array of string, required)

Example request:

```json
{
  "taskIds": [
    "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  ]
}
```

Responses:

- `202` - The bulk accept was started.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - taskIds was missing or empty.

#### bulk-deny-wiki-tasks

`POST /api/v1/wiki/tasks/bulk-deny` - Deny several suggestion tasks

Discard every unresolved change across a list of content library suggestion tasks.

**Notes**

- Runs synchronously and reports how many tasks it processed
- A task id that is not a content library suggestion is skipped rather than rejected, so `tasksDenied` can be lower than the number of ids you sent

Request body (required):

- `taskIds` (array of string, required)

Example request:

```json
{
  "taskIds": [
    "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  ]
}
```

Responses:

- `200` - The tasks were processed.
- `401` - Unauthorized - invalid API key.
- `500` - Bad request - taskIds was missing or empty.

`200` response body:

- `tasksDenied` (number, optional)
