---
title: Comments
description: List, add and delete comments on Goals, Objectives, Meetings, Actions and Ideas.
product: gps
section: api
order: 17
---

# Comments

Comments belong to a parent record: a [Goal](goals.md), [Objective](objectives.md), [Meeting](meetings.md), [Action](actions.md) or [Idea](ideas.md). Each parent has the same three operations.

| Method | Path | Scope |
|--------|------|-------|
| `GET` | [`/goals/{goalId}/comments`](#list-comments) | `comments:read` |
| `GET` | [`/objectives/{objectiveId}/comments`](#list-comments) | `comments:read` |
| `GET` | [`/meetings/{meetingId}/comments`](#list-comments) | `comments:read` |
| `GET` | [`/actions/{actionId}/comments`](#list-comments) | `comments:read` |
| `GET` | [`/ideas/{ideaId}/comments`](#list-comments) | `comments:read` |
| `POST` | [`/goals/{goalId}/comments`](#add-a-comment) | `comments:write` |
| `POST` | [`/objectives/{objectiveId}/comments`](#add-a-comment) | `comments:write` |
| `POST` | [`/meetings/{meetingId}/comments`](#add-a-comment) | `comments:write` |
| `POST` | [`/actions/{actionId}/comments`](#add-a-comment) | `comments:write` |
| `POST` | [`/ideas/{ideaId}/comments`](#add-a-comment) | `comments:write` |
| `DELETE` | [`/goals/{goalId}/comments/{commentId}`](#delete-a-comment) | `comments:write` |
| `DELETE` | [`/objectives/{objectiveId}/comments/{commentId}`](#delete-a-comment) | `comments:write` |
| `DELETE` | [`/meetings/{meetingId}/comments/{commentId}`](#delete-a-comment) | `comments:write` |
| `DELETE` | [`/actions/{actionId}/comments/{commentId}`](#delete-a-comment) | `comments:write` |
| `DELETE` | [`/comments/{id}?type=…`](#delete-a-comment-by-id) | `comments:write` |

Comment changes don't fire webhooks.

The parent must exist in your organization and be visible to the API (not an archived Meeting, a hidden Idea, or an Action whose Goal or Objective is archived), or every comment endpoint for it returns `404`.

The JavaScript examples assume a `headers` object holding your key:

```js
const headers = {
  'X-API-Key': process.env.GPS_API_KEY_ID,
  'X-API-Secret': process.env.GPS_API_SECRET,
};
```

## The comment object

Comments on every kind of parent come back in the same shape.

| Field | Type | Notes |
|-------|------|-------|
| `id` | string | Comment ID. |
| `content` | string | The comment text. |
| `authorId` | string or null | Member ID of the author. |
| `createdAt` | string | ISO 8601 timestamp. |
| `updatedAt` | string | ISO 8601 timestamp. For comments on Actions and Ideas this equals `createdAt`. |

## List comments

`GET /goals/{goalId}/comments`
`GET /objectives/{objectiveId}/comments`
`GET /meetings/{meetingId}/comments`
`GET /actions/{actionId}/comments`
`GET /ideas/{ideaId}/comments`

Returns the parent's comments. Comments on Ideas come oldest first; for other parents order isn't guaranteed, so sort by `createdAt`.

**Scope:** `comments:read`

| Parameter | In | Notes |
|-----------|----|-------|
| `goalId`, `objectiveId`, `meetingId`, `actionId` or `ideaId` | path | The parent's ID. |
| `limit`, `offset` | query | See [Pagination](../conventions.md#pagination). |

```bash
curl https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e/comments -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d/comments -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl https://plan.empiraa.app/api/v1/meetings/7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c/comments -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl https://plan.empiraa.app/api/v1/actions/e5a7c9e1-6f8b-4c2d-8e9f-3a4b5c6d7e8f/comments -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl https://plan.empiraa.app/api/v1/ideas/2c4e6a8c-0d1f-4a3b-9c5d-6e7f8a9b0c1d/comments -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
```

```js
// parent is 'goals', 'objectives', 'meetings', 'actions' or 'ideas'
async function listComments(parent, parentId) {
  const res = await fetch(`https://plan.empiraa.app/api/v1/${parent}/${parentId}/comments`, { headers });
  const { data, meta } = await res.json();
  return data;
}

const comments = await listComments('goals', 'a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e');
```

**Response `200`**

```json
{
  "data": [
    {
      "id": "8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b",
      "content": "Renewals are tracking ahead of plan this month.",
      "authorId": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
      "createdAt": "2026-11-11T21:04:13.000Z",
      "updatedAt": "2026-11-11T21:04:13.000Z"
    }
  ],
  "meta": { "total": 1, "limit": 50, "offset": 0 }
}
```

**Errors:** `404 NOT_FOUND` if the parent doesn't exist, is in another organization, or isn't visible to the API; `422 VALIDATION_ERROR` for a bad `limit` or `offset`.

## Add a comment

`POST /goals/{goalId}/comments`
`POST /objectives/{objectiveId}/comments`
`POST /meetings/{meetingId}/comments`
`POST /actions/{actionId}/comments`
`POST /ideas/{ideaId}/comments`

**Scope:** `comments:write`

| Path parameter | Notes |
|----------------|-------|
| `goalId`, `objectiveId`, `meetingId`, `actionId` or `ideaId` | The parent's ID. |

**Request body for Goals, Objectives, Meetings and Actions**

| Field | Type | Required | Allowed values | Notes |
|-------|------|----------|----------------|-------|
| `content` | string | Yes | At least 1 character | The comment text. |
| `onBehalfOf` | string | No | Active member ID | The author. Default: the key's creator. See [Attribution](../conventions.md#attribution-with-onbehalfof). |

**Request body for Ideas**: the text goes in `body`, not `content`.

| Field | Type | Required | Allowed values | Notes |
|-------|------|----------|----------------|-------|
| `body` | string | Yes | 1–2,000 characters | The comment text. Returned as `content`. |
| `onBehalfOf` | string | No | Active member ID | The author. |

```bash
curl -X POST https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e/comments \
  -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET" -H "Content-Type: application/json" \
  -d '{ "content": "Renewals are tracking ahead of plan this month." }'

curl -X POST https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d/comments \
  -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET" -H "Content-Type: application/json" \
  -d '{ "content": "October figures are in." }'

curl -X POST https://plan.empiraa.app/api/v1/meetings/7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c/comments \
  -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET" -H "Content-Type: application/json" \
  -d '{ "content": "Please add the churn report to the agenda." }'

curl -X POST https://plan.empiraa.app/api/v1/actions/e5a7c9e1-6f8b-4c2d-8e9f-3a4b5c6d7e8f/comments \
  -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET" -H "Content-Type: application/json" \
  -d '{ "content": "Twelve offers sent so far.", "onBehalfOf": "6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81" }'

curl -X POST https://plan.empiraa.app/api/v1/ideas/2c4e6a8c-0d1f-4a3b-9c5d-6e7f8a9b0c1d/comments \
  -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET" -H "Content-Type: application/json" \
  -d '{ "body": "We could start with the three largest clients." }'
```

```js
async function addComment(parent, parentId, text, onBehalfOf) {
  // Ideas take the text as `body`; every other parent takes `content`.
  const payload = parent === 'ideas' ? { body: text } : { content: text };
  if (onBehalfOf) payload.onBehalfOf = onBehalfOf;
  const res = await fetch(`https://plan.empiraa.app/api/v1/${parent}/${parentId}/comments`, {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  const { data } = await res.json();
  return data;
}

await addComment('goals', 'a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e', 'Renewals are tracking ahead of plan this month.');
await addComment('ideas', '2c4e6a8c-0d1f-4a3b-9c5d-6e7f8a9b0c1d', 'We could start with the three largest clients.');
```

**Response `201`**

```json
{
  "data": {
    "id": "8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b",
    "content": "Renewals are tracking ahead of plan this month.",
    "authorId": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
    "createdAt": "2026-11-12T03:21:09.402Z",
    "updatedAt": "2026-11-12T03:21:09.402Z"
  }
}
```

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 404 | `NOT_FOUND` | The parent doesn't exist, is in another organization, or isn't visible to the API. |
| 422 | `VALIDATION_ERROR` | Missing or empty `content` (or `body` for Ideas), or an Idea comment over 2,000 characters. |
| 422 | `INVALID_USERS` | `onBehalfOf` isn't an active member. |
| 422 | `NO_KEY_OWNER` | No `onBehalfOf`, and the key's creator is no longer a member. |

## Delete a comment

`DELETE /goals/{goalId}/comments/{commentId}`
`DELETE /objectives/{objectiveId}/comments/{commentId}`
`DELETE /meetings/{meetingId}/comments/{commentId}`
`DELETE /actions/{actionId}/comments/{commentId}`

Permanently deletes a comment from the parent you name. A key with `comments:write` can delete any comment in the organization, not only ones it added. To delete a comment on an Idea, use [Delete a comment by ID](#delete-a-comment-by-id).

**Scope:** `comments:write`

| Path parameter | Notes |
|----------------|-------|
| `goalId`, `objectiveId`, `meetingId` or `actionId` | The parent's ID. |
| `commentId` | The comment's ID. It must belong to that parent. |

```bash
curl -X DELETE https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e/comments/8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl -X DELETE https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d/comments/8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl -X DELETE https://plan.empiraa.app/api/v1/meetings/7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c/comments/8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
curl -X DELETE https://plan.empiraa.app/api/v1/actions/e5a7c9e1-6f8b-4c2d-8e9f-3a4b5c6d7e8f/comments/8e0a2c4e-6f7b-4d9c-a1b2-7c8d9e0f1a2b -H "X-API-Key: $GPS_API_KEY_ID" -H "X-API-Secret: $GPS_API_SECRET"
```

```js
async function deleteComment(parent, parentId, commentId) {
  const res = await fetch(
    `https://plan.empiraa.app/api/v1/${parent}/${parentId}/comments/${commentId}`,
    { method: 'DELETE', headers },
  );
  return res.status === 204;
}
```

**Response `204`**: no body.

**Errors:** `404 NOT_FOUND` if the parent or the comment doesn't exist, the comment belongs to a different parent, or either is in another organization.

## Delete a comment by ID

`DELETE /comments/{id}?type={type}`

Deletes a comment when you know its ID and the kind of parent it's on. This is the only way to delete a comment on an Idea.

**Scope:** `comments:write`

| Parameter | In | Required | Allowed values |
|-----------|----|----------|----------------|
| `id` | path | Yes | The comment's ID. |
| `type` | query | Yes | `goal`, `objective`, `meeting`, `action`, `idea` |

```bash
curl -X DELETE "https://plan.empiraa.app/api/v1/comments/9f1b3d5f-7a8c-4e0d-b2c3-8d9e0f1a2b3c?type=idea" \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch(
  'https://plan.empiraa.app/api/v1/comments/9f1b3d5f-7a8c-4e0d-b2c3-8d9e0f1a2b3c?type=idea',
  { method: 'DELETE', headers },
);
console.log(res.status); // 204
```

**Response `204`**: no body.

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 422 | `VALIDATION_ERROR` | `type` is missing or not one of the allowed values. |
| 404 | `NOT_FOUND` | No comment of that `type` has this ID in your organization. |
