---
title: "Collaboration & Comments"
description: "Enable threaded, element-linked comments in the Landing Page Editor, backed by conversation endpoints on your own backend."
url: https://docs.topol.io/landing-page-editor/guide/collaboration.html
---

# Collaboration & Comments

**Comments add threaded discussions to specific sections and blocks, so a page can be reviewed where the work happens.** The editor renders the threads, mentions, and reactions; your backend stores them through the conversation and comment endpoints described below.

## Enabling comments

One option turns the feature on (`boolean`, default `false`):

```typescript
{
  enableComments: true;
}
```

With comments enabled, users can comment on specific elements or sections, reply in threaded discussions, mention teammates with `@mentions`, and resolve threads once addressed. With it off, the commenting UI and related collaboration actions are not available.

Comments need to know who is talking. A working setup pairs `enableComments` with the [user options](https://docs.topol.io/landing-page-editor/guide/user-management.html): `currentUser` identifies the author, `teamUsers` populates the mention list, and `templateId` scopes the threads to a page:

```typescript
{
  enableComments: true,
  currentUser: {
    userId: "user-123",
    name: "John Doe",
    profilePhotoUrl: "https://example.com/photos/john.jpg"
  },
  templateId: "template-456",
  teamUsers: [
    { userId: "user-123", name: "John Doe", profilePhotoUrl: "..." },
    { userId: "user-456", name: "Jane Smith", profilePhotoUrl: "..." }
  ]
}
```

## API Implementation

Before implementing the endpoints, check [how to work with API endpoints](https://docs.topol.io/landing-page-editor/guide/api.html).

In the URLs below, `{API.CONVERSATIONS}` and `{API.COMMENTS}` stand for the base URLs you configured under the [`api` option](https://docs.topol.io/landing-page-editor/guide/api.html#setting-up-api-endpoints); the editor appends the conversation key or comment id to them. Every read endpoint must answer with a `{ "success": true, "data": ... }` envelope — a response without `success: true` is treated as a failure and no comments render.

### List Conversations

-   URL: `/{API.CONVERSATIONS}`
-   Method: `GET`
-   Content-Type: `application/json`

**Params:**

| key | value |
| --- | --- |
| key | authorization api key |
| hostname | hostname |
| template\_id | id of the template |
| entity\_id | the `authorize.userId` from your options (not `currentUser.userId`) |
| current\_user\_id | user id of current user (`currentUser.userId`) |
| sort\_by | "date" or "unread" |
| show\_resolved | "true" or "false" |

This endpoint is called when the editor is initially loaded, when sorting or display preference changes, automatically after any comment mutation (add, edit, react, delete, resolve), and whenever `LPE.refreshComments("conversation_key")` is called on the editor instance.

**Response:**

```json
{
  "success": true,
  "data": [
    {
      "key": "conversation_key",
      "type": "block",
      "resolved": false,
      "delete": false,
      "involved_users": [
        {
          "user_id": "user_id",
          "profile_photo_url": "link to the user's profile picture",
          "name": "user name"
        }
      ],
      "comments": [
        {
          "id": "comment_id",
          "created_by": {
            "user_id": "user_id",
            "profile_photo_url": "link to the user's profile picture",
            "name": "user name"
          },
          "created_at": "ISO 8601 time",
          "content": "text content of the comment",
          "read_by": ["user_id of the user who saw the comment"],
          "reactions": [
            {
              "created_by": {
                "user_id": "user_id",
                "profile_photo_url": "link to the user's profile picture",
                "name": "user name"
              },
              "type": "thumb-up"
            }
          ]
        }
      ]
    }
  ]
}
```

Notes on the response fields:

-   `type` is **required** and must be `"block"` or `"section"`.
-   `resolved` accepts `true`/`false` or `1`/`0` (handy for MySQL-backed integrations).
-   `created_by` and `read_by` are **required** on every comment; if either is missing, the response fails to parse and no comments are shown.
-   `user_id` values in responses may be strings or numbers.

### Detail Conversation

-   URL: `/{API.CONVERSATIONS}/{conversation_key}`
-   Method: `GET`
-   Content-Type: `application/json`
-   Params: `key`, `hostname`, `template_id`, `entity_id`, `current_user_id`

This endpoint is called in two situations:

-   When a user opens the conversation
-   When the conversation is currently open and `LPE.refreshComments("conversation_key")` is called on the editor instance

Unread messages should be marked as read.

**Response:**

```json
{
  "success": true,
  "data": {
    "key": "conversation_key",
    "type": "block",
    "resolved": false,
    "delete": false,
    "involved_users": [
      {
        "user_id": "user_id",
        "profile_photo_url": "link to the user's profile picture",
        "name": "user name"
      }
    ],
    "comments": [
      {
        "id": "comment_id",
        "created_by": {
          "user_id": "user_id",
          "profile_photo_url": "link to the user's profile picture",
          "name": "user name"
        },
        "created_at": "ISO 8601 time",
        "content": "text content of the comment",
        "read_by": ["user_id of the user who saw the comment"],
        "reactions": [
          {
            "created_by": {
              "user_id": "user_id",
              "profile_photo_url": "link to the user's profile picture",
              "name": "user name"
            },
            "type": "thumb-up"
          }
        ]
      }
    ]
  }
}
```

### Resolve or Unresolve Conversation

-   URL: `/{API.CONVERSATIONS}/{conversation_key}`
-   Method: `PATCH`

This endpoint is called when the resolve button is clicked.

**Request:**

```json
{
  "template_id": "id of the template",
  "entity_id": "entity user_id",
  "current_user_id": "current user user_id",
  "hostname": "origin",
  "key": "api key",
  "resolved": true
}
```

**Response:**

HTTP `200 OK`

### Delete Conversation

-   URL: `/{API.CONVERSATIONS}/{conversation_key}`
-   Method: `DELETE`
-   Params: `key`, `hostname`, `template_id`, `entity_id`, `current_user_id`

This endpoint is called when a user deletes a conversation.

Consider whether to delete the conversation or just soft delete.

**Response:**

HTTP `200 OK`

### Add Comment

-   URL: `/{API.CONVERSATIONS}/{conversation_key}`
-   Method: `POST`

This endpoint is called both when starting a new conversation on an element (the editor generates a new `conversation_key`) and when replying to an existing one; treat it as an upsert on `conversation_key`.

**Request:**

```json
{
  "template_id": "id of the template",
  "entity_id": "entity user_id",
  "current_user_id": "current user user_id",
  "hostname": "origin",
  "key": "api key",
  "type": "block",
  "content": "text content of comment",
  "mentions": ["user_id of the user who is mentioned in the comment"]
}
```

**Response:**

HTTP `200 OK`

### Edit, React, Read or Unread the Comment

-   URL: `/{API.COMMENTS}/{comment_id}`
-   Method: `POST`

This endpoint is called when editing a specific comment, reacting to a specific comment, or marking a conversation as read/unread (called for the last comment of the conversation). Each action sends its own request shape; the three variants are mutually exclusive and always include the common fields `template_id`, `entity_id`, `current_user_id`, `hostname`, and `key`.

**Edit comment:**

```json
{
  "content": "new text content of comment",
  "mentions": ["user_id of the user who is mentioned in the comment"]
}
```

**React to comment:**

```json
{
  "react": "thumb-up"
}
```

**Mark conversation as read or unread:**

```json
{
  "action": "mark_as_read"
}
```

`action` is either `"mark_as_read"` or `"mark_as_unread"`.

**Response:**

HTTP `200 OK`

### Delete User's Comment

-   URL: `/{API.COMMENTS}/{comment_id}`
-   Method: `DELETE`
-   Params: `key`, `hostname`, `template_id`, `entity_id`, `current_user_id`

This endpoint is called when a user deletes a comment.

**Response:**

HTTP `200 OK`

### refreshComments

Changes made inside the editor refresh automatically. Call **refreshComments** on the editor instance when comments change _outside_ the editor, typically when your backend receives a new comment from another user and pushes a realtime notification to your app:

```js
const LPE = LandingPageEditor({ config: TOPOL_OPTIONS });
LPE.render("#landing-page-editor");

// when your backend signals that a conversation changed:
LPE.refreshComments("conversation_key");
```

This reloads the full conversation list and refreshes the currently opened conversation detail.

## Troubleshooting

**Comments not appearing:** confirm that `enableComments` is `true` and that the `CONVERSATIONS` endpoint returns `{ "success": true, "data": [...] }`. Comments render even without `currentUser`, but every request then sends an empty `current_user_id` and read/unread tracking does not work, so always set `currentUser`.

**Mentions not working:** the `teamUsers` array has to include every mentionable user, and each entry requires all three of `userId` (a **string**), `name`, and `profilePhotoUrl`. A missing value prevents the mention system from identifying the user.
