> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cativa.digital/llms.txt
> Use this file to discover all available pages before exploring further.

# Posts and Comments

> The Post model, feed vs group scope, who can create, and how to comment and react via API.

Posts are the community's unit of content. Each Post has an **author**, a **content** body, and a **scope** (the tenant feed or a specific group). Comments hang off a Post, and reactions (like/unlike) hang off both Posts and Comments. All routes below belong to the Cativa API, authenticated with an API Key in the `Authorization: Bearer cativa_live_...` header.

Picture it as a bulletin board: the **Post** is the poster someone pins up, the **comments** are the notes stuck underneath it, and the **reactions** are the thumbs-up anyone gives to the poster or a note. The board can be the whole courtyard (feed) or a closed room (group).

## The Post model

```
Post
├── author   (user tied to the authenticated credential)
├── content  (the publication text)
└── scope
    ├── feed    → shows in the tenant-wide feed
    └── group   → shows only inside a specific group
```

<Note>
  The post author is always the user tied to the authenticated credential. You do not (and cannot) send `authorId` in the body. See [Identity and Users](/en/concepts/identity-and-users) to understand how the credential resolves the author.
</Note>

### Scope: feed vs group

A **feed** Post is published at the community level and shows for everyone who can see the tenant feed. A **group** Post lives inside a specific group and shows only to users with access to that group. The difference is the creation endpoint: feed uses `POST /community/posts`, group uses `POST /community/groups/{groupId}/posts`.

## Who can create

Creating a Post uses the **`CreatePost`** permission. An admin-scoped key creates in any scope (feed or any group). A partner key with default scope creates on behalf of the user tied to the credential, and that user must **have access** to the target group (at least one matching badge, or membership in an open group). Without access, the call returns `403 forbidden`.

<Note>
  Editing and deleting a Post are done **by the author**. An admin key can also moderate. A regular user cannot edit or delete someone else's post.
</Note>

## Posts

### List the feed

```bash theme={null}
curl "https://apis.cativalab.digital/tenant/api/v2/community/posts?page=1&pageSize=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Returns the tenant feed, paginated. Use the paging parameters to walk the pages. The full schema of each response field is in the **API Reference** tab (tag "Post").

### Get a single post

```bash theme={null}
curl https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Flow: post to a group and follow up

<Steps>
  <Step title="Find the groupId">
    List the groups the user can see (see [Communities and Spaces](/en/concepts/communities-and-spaces)) and keep the target `groupId`. Without access to the group, the next step returns `403 forbidden`.
  </Step>

  <Step title="Create the post in the group">
    `POST /community/groups/{groupId}/posts` with the `content`. The response carries the `id` of the new post.
  </Step>

  <Step title="Comment and react">
    Use the post `id` to `POST .../comments` (comment) and `POST .../reactions` (like). Reacting is idempotent, so you can repeat it without duplicating.
  </Step>
</Steps>

### Create a post in the feed

The body carries the post `content`. The example below is the minimal plausible case. The exact contract (optional fields like attachments, media, or metadata) is in the **API Reference** tab (tag "Post").

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://apis.cativalab.digital/tenant/api/v2/community/posts \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Welcome to the community feed!"
    }'
  ```

  ```js Node theme={null}
  const res = await fetch('https://apis.cativalab.digital/tenant/api/v2/community/posts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    // content is the post body. Extra fields (media, metadata) in the API Reference.
    body: JSON.stringify({
      content: 'Welcome to the community feed!'
    })
  });
  const post = await res.json();
  ```
</CodeGroup>

Illustrative response (the authoritative schema is in the **API Reference** tab, tag "Post"):

```json theme={null}
{
  "post": {
    "id": "01HQ7Z3X4Y5Z6A7B8C9D0E1F2G",
    "userId": "01HQ0USER1234567890ABCDEF",
    "content": "Welcome to the community feed!",
    "htmlContent": null,
    "title": null,
    "isPrivate": false,
    "allowComments": true,
    "groupId": null,
    "createdAt": "2026-07-10T14:32:00Z",
    "tags": []
  }
}
```

### Create a post in a group

Same body, but the `groupId` goes in the route. The post stays scoped to the group.

```bash theme={null}
curl -X POST https://apis.cativalab.digital/tenant/api/v2/community/groups/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Group-only announcement."
  }'
```

### Edit a post

Only the author (or an admin key) edits. The `postId` goes in the route.

```bash theme={null}
curl -X PUT https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Updated content."
  }'
```

### Delete a post

```bash theme={null}
curl -X DELETE https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Comments

Comments hang off a Post. The `postId` is always in the route. Same access rule: the user tied to the credential must be able to see the Post to comment on it.

### List a post's comments

```bash theme={null}
curl https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/comments \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Comment on a post

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/comments \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Loved this post!"
    }'
  ```

  ```js Node theme={null}
  const postId = '01HQ7Z3X4Y5Z6A7B8C9D0E1F2G';
  const res = await fetch(
    `https://apis.cativalab.digital/tenant/api/v2/community/posts/${postId}/comments`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ content: 'Loved this post!' })
    }
  );
  const comment = await res.json();
  ```
</CodeGroup>

Illustrative response (full schema in the **API Reference** tab, tag "Comment"):

```json theme={null}
{
  "comment": {
    "id": "01HQ8A1B2C3D4E5F6G7H8J9K0L",
    "userId": "01HQ0USER1234567890ABCDEF",
    "postId": "01HQ7Z3X4Y5Z6A7B8C9D0E1F2G",
    "replyToId": null,
    "content": "Loved this post!",
    "gifUrl": null,
    "createdAt": "2026-07-10T14:40:00Z"
  }
}
```

### Edit and delete a comment

Same rule as the Post: only the author (or admin) changes it. Both `postId` and `commentId` go in the route.

```bash theme={null}
# Edit
curl -X PUT https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/comments/01HQ8A1B2C3D4E5F6G7H8J9K0L \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Fixed comment." }'

# Delete
curl -X DELETE https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/comments/01HQ8A1B2C3D4E5F6G7H8J9K0L \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Reactions

Reacting (like) and un-reacting (unlike) is an **idempotent pair**. Liking twice leaves the state the same as liking once. Unliking something that isn't liked does not error. This simplifies retries in jobs and webhook handlers. The reaction schema is in the **API Reference** tab (tag "Reaction").

### Like and unlike a post

```bash theme={null}
# Like
curl -X POST https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/reactions \
  -H "Authorization: Bearer YOUR_API_KEY"

# Unlike
curl -X DELETE https://apis.cativalab.digital/tenant/api/v2/community/posts/01HQ7Z3X4Y5Z6A7B8C9D0E1F2G/reactions \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### React to a comment

Comment reactions use the `commentId` in the route, not the `postId`.

```bash theme={null}
# React
curl -X POST https://apis.cativalab.digital/tenant/api/v2/community/comments/01HQ8A1B2C3D4E5F6G7H8J9K0L/reactions \
  -H "Authorization: Bearer YOUR_API_KEY"

# Remove reaction
curl -X DELETE https://apis.cativalab.digital/tenant/api/v2/community/comments/01HQ8A1B2C3D4E5F6G7H8J9K0L/reactions \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Common errors and questions

<AccordionGroup>
  <Accordion title="I got 403 forbidden creating a post in a group">
    The user tied to the credential has no access to that group. Group access comes from a **badge** (or from the group being open). Assign the badge the group requires to the user (see [Badges as Permissions](/en/concepts/badges-as-permissions)) and retry. An admin key bypasses this rule and creates in any group.
  </Accordion>

  <Accordion title="Does liking twice count as two likes?">
    No. Reacting and un-reacting are an **idempotent pair**: liking again leaves the state the same as liking once, and unliking something that wasn't liked does not error. That makes it safe to reprocess a webhook or retry a job without inflating the count.
  </Accordion>

  <Accordion title="Can I edit or delete another user's post?">
    Not with a default-scope key. Editing and deleting belong **to the author**. Trying to change another user's post returns `403 forbidden`. Only an admin key moderates someone else's content.
  </Accordion>

  <Accordion title="How do I send the post author?">
    You don't. The author is always the user resolved from the authenticated credential; there is no `authorId` field in the body. To post on someone else's behalf, use their admin key or the identity mechanism (see [Identity and Users](/en/concepts/identity-and-users)).
  </Accordion>

  <Accordion title="I deleted a post and the comments vanished with it?">
    Yes. Comments and reactions hang off the Post: deleting the Post makes whatever hung off it inaccessible. There is no "undo" through the public API, so treat deletion as final from the integration's point of view.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={3}>
  <Card title="Communities and Spaces" icon="layer-group" href="/en/concepts/communities-and-spaces">
    Where groups live and how to discover the `groupId` before posting.
  </Card>

  <Card title="Identity and Users" icon="user" href="/en/concepts/identity-and-users">
    How the credential resolves the post and comment author.
  </Card>

  <Card title="post.created event" icon="webhook" href="/en/webhooks/events/post-created">
    Receive a webhook every time a post is created.
  </Card>
</CardGroup>
