# Add a comment

> Adds a comment to a plant, explant, contamination log, task, media recipe or SOP, as the key's owner. It is the same comment the app makes: members it names are notified, the author of the comment it replies to is told, the people following a task hear about it, and it appears in the record's activity.

Source: https://docs.xplantpro.com/docs/api/comments/create-comment

`POST https://app.xplantpro.com/api/v1/comments`

- Required scope: `write:comments`
- Credentials: workspace API key (`xpk_`)
- Idempotency-Key: honoured (24 hours)

The key's owner needs the member role or above in the workspace, as in the app.

Send an `Idempotency-Key` header to make retries safe: a repeat with the same key within 24 hours returns the first response instead of writing twice. See [Idempotency](https://docs.xplantpro.com/docs/idempotency.md).

## Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Any unique string you choose per logical write. A retry carrying the same key within 24 hours returns the first result instead of writing again. Scoped to your API key and this operation. 8–255 characters. Must match `^[A-Za-z0-9._:~-]+$`. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entity_type` | `"plant"` \| `"explant"` \| `"contamination"` \| `"task"` \| `"media_recipe"` \| `"sop"` | Yes | The kind of record to comment on. |
| `entity_id` | string (uuid) | Yes | The id of the record to comment on. |
| `body` | string | Yes | The comment, in Markdown. Stored exactly as sent. To mention someone, write `@` and their username in the text and add their user id to `mentioned_user_ids`. To link a record, write `#plant:` (or `#explant:`, `#contamination:`, `#task:`, `#media:`, `#sop:`) followed by its name, and list it in `references`. 1–5000 characters. |
| `parent_id` | string (uuid) | No | Reply to this comment. It must be on the same record. |
| `mentioned_user_ids` | string (uuid)[] | No | Workspace members this comment names. Each is notified, exactly as when someone is mentioned in the app. Every id must be an active member of the workspace. Up to 25 items. |
| `references` | object[] | No | Records in this workspace that the body links to. Up to 25 items. |
| `references[].entity_type` | `"plant"` \| `"explant"` \| `"contamination"` \| `"task"` \| `"media_recipe"` \| `"sop"` | Yes | The kind of record the comment links to. |
| `references[].entity_id` | string (uuid) | Yes | The linked record's id. |
| `references[].label` | string | Yes | The record's name as it follows `#type:` in the body, where spaces are written as hyphens; either spelling is accepted here. The link is drawn over the matching text. 1–200 characters. |

## Example

**curl**

```bash
curl -X POST https://app.xplantpro.com/api/v1/comments \
  -H "Authorization: Bearer $XPLANT_API_KEY" \
  -H "Idempotency-Key: bench-3-line-0412-note-1" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "explant",
    "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
    "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
    "mentioned_user_ids": [
      "4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"
    ],
    "references": [
      {
        "entity_type": "task",
        "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
        "label": "Subculture Alocasia line"
      }
    ]
  }'
```

**JavaScript**

```js
import { XPlantClient } from "@shmaplex/xplant-sdk";

const client = new XPlantClient({ apiKey: process.env.XPLANT_API_KEY });

const comment = await client.comments.create(
  {
    entity_type: "explant",
    entity_id: "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
    body: "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
    mentioned_user_ids: ["4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"],
    references: [
      {
        entity_type: "task",
        entity_id: "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
        label: "Subculture Alocasia line",
      },
    ],
  },
  { idempotencyKey: "bench-3-line-0412-note-1" },
);
```

**Python**

```python
import os
import requests

resp = requests.post(
    "https://app.xplantpro.com/api/v1/comments",
    headers={
        "Authorization": f"Bearer {os.environ['XPLANT_API_KEY']}",
        "Idempotency-Key": "bench-3-line-0412-note-1",
    },
    json={
        "entity_type": "explant",
        "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
        "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
        "mentioned_user_ids": ["4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"],
        "references": [
            {
                "entity_type": "task",
                "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                "label": "Subculture Alocasia line",
            },
        ],
    },
    timeout=10,
)
body = resp.json()
if not body["ok"]:
    raise RuntimeError(f"{resp.status_code} {body['code']}: {body['error']}")
comment = body["data"]
```

## Response

`201` with `{ "ok": true, "data": … }`. `data` holds the result.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes | — |
| `entity_type` | `"plant"` \| `"explant"` \| `"contamination"` \| `"task"` \| `"media_recipe"` \| `"sop"` | Yes | The kind of record the comment is on. |
| `entity_id` | string (uuid) | Yes | The record the comment is on. |
| `parent_id` | string \| null (uuid) | Yes | The comment this one replies to. |
| `body` | string | Yes | The comment in Markdown, exactly as written — mentions and record links included as the `@name` and `#type:Label` text the author typed. |
| `status` | string | Yes | `active`; `edited` once changed after posting; or `deleted`, which keeps the comment's place in the thread but not its text. |
| `is_pinned` | boolean | Yes | Pinned to the top of the record's discussion. |
| `author` | object | Yes | — |
| `author.id` | string (uuid) | Yes | User id of the author. |
| `author.name` | string \| null | Yes | The name shown beside the comment in the app, or null when there is none to show. |
| `mentioned_user_ids` | string (uuid)[] | Yes | Workspace members the comment named. Each was notified. |
| `references` | object[] | Yes | Records in the workspace the body links to. A record removed since is left out. |
| `references[].entity_type` | `"plant"` \| `"explant"` \| `"contamination"` \| `"task"` \| `"media_recipe"` \| `"sop"` | Yes | — |
| `references[].entity_id` | string (uuid) | Yes | — |
| `references[].label` | string | Yes | The name the body links from. |
| `created_at` | string \| null | Yes | ISO 8601. |
| `updated_at` | string \| null | Yes | ISO 8601. |
| `edited_at` | string \| null | Yes | When the text was last changed. ISO 8601. |

```json title="Response"
{
  "ok": true,
  "data": {
    "id": "9c4e2a71-3d5b-4f86-b1e0-7a2d6c8f3e45",
    "entity_type": "explant",
    "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
    "parent_id": null,
    "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
    "status": "active",
    "is_pinned": false,
    "author": {
      "id": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
      "name": "Sam Okafor"
    },
    "mentioned_user_ids": [
      "4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"
    ],
    "references": [
      {
        "entity_type": "task",
        "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
        "label": "Subculture Alocasia line"
      }
    ],
    "created_at": "2026-09-25T09:15:40.000Z",
    "updated_at": "2026-09-25T09:15:40.000Z",
    "edited_at": null
  }
}
```

| Response header | Meaning |
| --- | --- |
| `Idempotent-Replay` | `true` when this response is a replay of an earlier request with the same `Idempotency-Key`. |
| `X-Request-Id` | Identifies this request. Include it when you contact support. |

## Errors

| Status | Code | When |
| --- | --- | --- |
| `400` | `VALIDATION_ERROR` | The request body is not valid JSON. |
| `401` | `UNAUTHORIZED` | The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. |
| `402` | `PAID_PLAN_REQUIRED` | The workspace's plan does not include the API. The full API is included with xPlant+ Teams and Enterprise; on Hobby and Pro Lab, keys can connect devices only. |
| `403` | `FORBIDDEN` | The key lacks a scope this operation requires, or its owner's current role in the workspace cannot use it — a key never does more than its owner can in xPlant. `error` names the scope, and for a role, the role it needs. |
| `403` | `DEVICE_TOKEN_NOT_ACCEPTED` | A device token was sent; this operation needs a workspace API key. |
| `403` | `FORBIDDEN` | The key's owner is below the member role in the workspace. A key never does more than its owner can in xPlant; `error` names the role needed. |
| `404` | `NOT_FOUND` | `entity_id` names no record of that `entity_type` in the workspace, or `parent_id` names no comment on it; `error` says which. |
| `409` | `IDEMPOTENCY_IN_FLIGHT` | A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds. |
| `409` | `CONFLICT` | The task's discussion is locked. Only the task's creator or a lab manager can comment. |
| `422` | `VALIDATION_ERROR` | The `Idempotency-Key` header is malformed. |
| `422` | `VALIDATION_ERROR` | A field failed validation. `error` names the first one, for example `title: title is required`. |
| `422` | `VALIDATION_ERROR` | A mentioned user is not an active member of the workspace, or a linked record is not in it. |
| `429` | `RATE_LIMIT_EXCEEDED` | Too many requests for this key, device token or workspace. Wait `Retry-After` seconds. |
| `500` | `COMMENT_CREATE_FAILED` | The comment could not be saved. Retry with the same `Idempotency-Key`. |

Branch on `code`, never on the `error` text. See [Errors](https://docs.xplantpro.com/docs/errors.md).
