# Comments and Collaboration

Learn how to work with comments on assets and boards for team collaboration and feedback.

## Overview[​](#overview "Direct link to Overview")

Comments enable team collaboration directly on assets and boards. They support:

* Threaded discussions
* @mentions (via UI)
* Comment resolution
* Timestamps and authorship

***

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

* **Access Token**: API token with read permissions
* **Organization Slug**: Your organization identifier
* **Asset/Board Tokens**: IDs of content to comment on

***

## Viewing Asset Comments[​](#viewing-asset-comments "Direct link to Viewing Asset Comments")

Get all comments for a specific asset:

```
curl "https://api.playbook.com/v1/my-org/assets/product-photo/comments?page=1&per_page=20" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response:**

```
{

  "data": [

    {

      "token": "comment-abc123",

      "description": "Love the lighting in this shot!",

      "user_token": "user-xyz789",

      "created_at": "2024-10-09T14:30:00Z",

      "updated_at": "2024-10-09T14:30:00Z",

      "resolved_at": null,

      "replies": [

        {

          "token": "comment-def456",

          "description": "Thanks! Used a softbox for the effect",

          "user_token": "user-abc123",

          "created_at": "2024-10-09T15:00:00Z",

          "updated_at": "2024-10-09T15:00:00Z",

          "resolved_at": null,

          "replies": []

        }

      ]

    }

  ],

  "pagy": {

    "current_page": 1,

    "page_items": 1,

    "total_pages": 1,

    "total_count": 1

  }

}
```

***

## Viewing Board Comments[​](#viewing-board-comments "Direct link to Viewing Board Comments")

Get all comments for a board:

```
curl "https://api.playbook.com/v1/my-org/boards/main-collection/comments" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response structure is similar to asset comments.**

***

## Working with Comment Threads[​](#working-with-comment-threads "Direct link to Working with Comment Threads")

### Understanding Replies[​](#understanding-replies "Direct link to Understanding Replies")

Comments can have nested replies:

```
Comment (parent)

└── Reply 1 (child)

    └── Reply to Reply 1 (grandchild)

└── Reply 2 (child)
```

### Accessing Replies[​](#accessing-replies "Direct link to Accessing Replies")

```
async function getAllCommentsFlat(assetToken) {

  const response = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  );



  const { data } = await response.json();



  // Flatten replies

  const allComments = [];



  function flatten(comments, depth = 0) {

    for (const comment of comments) {

      allComments.push({ ...comment, depth });

      if (comment.replies?.length > 0) {

        flatten(comment.replies, depth + 1);

      }

    }

  }



  flatten(data);

  return allComments;

}
```

***

## Use Cases[​](#use-cases "Direct link to Use Cases")

### 1. Review and Approval Workflow[​](#1-review-and-approval-workflow "Direct link to 1. Review and Approval Workflow")

Display comments in a review interface:

```
class ReviewInterface {

  async loadAssetForReview(assetToken) {

    // Get asset details

    const asset = await fetch(

      `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}`,

      { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

    ).then(r => r.json());



    // Get comments

    const comments = await fetch(

      `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments`,

      { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

    ).then(r => r.json());



    return {

      asset: asset.data,

      comments: comments.data,

      status: asset.data.fields?.['Approval Status'],

      needsResolution: comments.data.some(c => !c.resolved_at)

    };

  }



  async canApprove(assetToken) {

    const review = await this.loadAssetForReview(assetToken);



    // Check all comments are resolved

    return !review.needsResolution;

  }

}
```

### 2. Comment Notifications[​](#2-comment-notifications "Direct link to 2. Comment Notifications")

Notify users when comments are added:

```
async function checkNewComments(assetToken, lastCheck) {

  const response = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  );



  const { data } = await response.json();



  // Find comments since last check

  const newComments = [];



  function findNew(comments) {

    for (const comment of comments) {

      const commentDate = new Date(comment.created_at);

      if (commentDate > lastCheck) {

        newComments.push(comment);

      }

      if (comment.replies) {

        findNew(comment.replies);

      }

    }

  }



  findNew(data);

  return newComments;

}



// Usage

const lastChecked = new Date('2024-10-09T12:00:00Z');

const newComments = await checkNewComments('product-photo', lastChecked);



if (newComments.length > 0) {

  console.log(`${newComments.length} new comments`);

  // Send notification

}
```

### 3. Comment Analytics[​](#3-comment-analytics "Direct link to 3. Comment Analytics")

Track comment activity:

```
class CommentAnalytics {

  async getCommentStats(assetToken) {

    const response = await fetch(

      `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments`,

      { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

    );



    const { data } = await response.json();



    const stats = {

      totalComments: 0,

      resolvedComments: 0,

      unresolvedComments: 0,

      totalReplies: 0,

      uniqueUsers: new Set(),

      commentsByUser: {},

      averageResponseTime: 0

    };



    function analyze(comments, isReply = false) {

      for (const comment of comments) {

        if (isReply) {

          stats.totalReplies++;

        } else {

          stats.totalComments++;

        }



        if (comment.resolved_at) {

          stats.resolvedComments++;

        } else {

          stats.unresolvedComments++;

        }



        stats.uniqueUsers.add(comment.user_token);



        stats.commentsByUser[comment.user_token] =

          (stats.commentsByUser[comment.user_token] || 0) + 1;



        if (comment.replies) {

          analyze(comment.replies, true);

        }

      }

    }



    analyze(data);



    stats.uniqueUsers = stats.uniqueUsers.size;



    return stats;

  }



  async getMostActiveAssets(assetTokens) {

    const results = await Promise.all(

      assetTokens.map(async token => ({

        token,

        stats: await this.getCommentStats(token)

      }))

    );



    return results.sort((a, b) =>

      b.stats.totalComments - a.stats.totalComments

    );

  }

}
```

### 4. Export Comments[​](#4-export-comments "Direct link to 4. Export Comments")

Export comments for reporting or archival:

```
async function exportCommentsToCSV(assetToken) {

  const response = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  );



  const { data } = await response.json();



  const rows = [

    ['Comment ID', 'User', 'Text', 'Created', 'Resolved', 'Depth']

  ];



  function addRows(comments, depth = 0) {

    for (const comment of comments) {

      rows.push([

        comment.token,

        comment.user_token,

        comment.description,

        comment.created_at,

        comment.resolved_at || 'No',

        depth

      ]);



      if (comment.replies) {

        addRows(comment.replies, depth + 1);

      }

    }

  }



  addRows(data);



  return rows.map(row => row.join(',')).join('\n');

}



// Usage

const csv = await exportCommentsToCSV('product-photo');

console.log(csv);
```

***

## Pagination[​](#pagination "Direct link to Pagination")

Handle large comment threads with pagination:

```
async function getAllComments(assetToken) {

  let allComments = [];

  let page = 1;

  let hasMore = true;



  while (hasMore) {

    const response = await fetch(

      `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/comments?` +

      `page=${page}&per_page=50`,

      { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

    );



    const { data, pagy } = await response.json();



    allComments = allComments.concat(data);



    hasMore = page < pagy.total_pages;

    page++;

  }



  return allComments;

}
```

***

## Comment Resolution Tracking[​](#comment-resolution-tracking "Direct link to Comment Resolution Tracking")

Track which comments have been resolved:

```
class CommentResolutionTracker {

  async getUnresolvedComments(assetToken) {

    const comments = await getAllComments(assetToken);



    const unresolved = [];



    function findUnresolved(commentList) {

      for (const comment of commentList) {

        if (!comment.resolved_at) {

          unresolved.push(comment);

        }

        if (comment.replies) {

          findUnresolved(comment.replies);

        }

      }

    }



    findUnresolved(comments);

    return unresolved;

  }



  async generateResolutionReport(assetTokens) {

    const report = {};



    for (const token of assetTokens) {

      const unresolved = await this.getUnresolvedComments(token);

      report[token] = {

        unresolvedCount: unresolved.length,

        comments: unresolved

      };

    }



    return report;

  }



  async assetsNeedingAttention(assetTokens, threshold = 5) {

    const report = await this.generateResolutionReport(assetTokens);



    return Object.entries(report)

      .filter(([_, data]) => data.unresolvedCount >= threshold)

      .map(([token, data]) => ({ token, ...data }));

  }

}



// Usage

const tracker = new CommentResolutionTracker();



const needsAttention = await tracker.assetsNeedingAttention(

  ['asset-1', 'asset-2', 'asset-3'],

  3 // Alert if 3+ unresolved comments

);



console.log('Assets needing attention:', needsAttention);
```

***

## Complete Comment Management System[​](#complete-comment-management-system "Direct link to Complete Comment Management System")

Here's a full implementation:

```
class CommentManager {

  constructor(orgSlug, accessToken) {

    this.orgSlug = orgSlug;

    this.accessToken = accessToken;

    this.baseUrl = 'https://api.playbook.com/v1';

  }



  async getAssetComments(assetToken, options = {}) {

    const params = new URLSearchParams({

      page: options.page || 1,

      per_page: options.perPage || 20

    });



    const response = await fetch(

      `${this.baseUrl}/${this.orgSlug}/assets/${assetToken}/comments?${params}`,

      { headers: { Authorization: `Bearer ${this.accessToken}` } }

    );



    return await response.json();

  }



  async getBoardComments(boardToken, options = {}) {

    const params = new URLSearchParams({

      page: options.page || 1,

      per_page: options.perPage || 20

    });



    const response = await fetch(

      `${this.baseUrl}/${this.orgSlug}/boards/${boardToken}/comments?${params}`,

      { headers: { Authorization: `Bearer ${this.accessToken}` } }

    );



    return await response.json();

  }



  async getAllCommentsFlat(resourceType, token) {

    const getComments = resourceType === 'asset'

      ? this.getAssetComments.bind(this)

      : this.getBoardComments.bind(this);



    let allComments = [];

    let page = 1;



    while (true) {

      const { data, pagy } = await getComments(token, { page });



      // Flatten comments and replies

      const flatComments = this.flattenComments(data);

      allComments = allComments.concat(flatComments);



      if (page >= pagy.total_pages) break;

      page++;

    }



    return allComments;

  }



  flattenComments(comments, depth = 0, parentToken = null) {

    const result = [];



    for (const comment of comments) {

      result.push({

        ...comment,

        depth,

        parentToken,

        hasReplies: comment.replies && comment.replies.length > 0

      });



      if (comment.replies) {

        const replies = this.flattenComments(

          comment.replies,

          depth + 1,

          comment.token

        );

        result.push(...replies);

      }

    }



    return result;

  }



  async getCommentStatistics(resourceType, token) {

    const comments = await this.getAllCommentsFlat(resourceType, token);



    const stats = {

      total: comments.length,

      topLevel: comments.filter(c => c.depth === 0).length,

      replies: comments.filter(c => c.depth > 0).length,

      resolved: comments.filter(c => c.resolved_at).length,

      unresolved: comments.filter(c => !c.resolved_at).length,

      users: [...new Set(comments.map(c => c.user_token))].length,

      avgDepth: comments.reduce((sum, c) => sum + c.depth, 0) / comments.length || 0,

      oldestUnresolved: this.findOldestUnresolved(comments)

    };



    return stats;

  }



  findOldestUnresolved(comments) {

    const unresolved = comments.filter(c => !c.resolved_at);



    if (unresolved.length === 0) return null;



    return unresolved.reduce((oldest, current) =>

      new Date(current.created_at) < new Date(oldest.created_at)

        ? current

        : oldest

    );

  }



  async generateReport(resourceType, tokens) {

    const report = [];



    for (const token of tokens) {

      const stats = await this.getCommentStatistics(resourceType, token);

      report.push({ token, ...stats });

    }



    return report;

  }

}



// Usage

const manager = new CommentManager('my-org', 'your_token');



// Get all comments for an asset

const comments = await manager.getAllCommentsFlat('asset', 'product-photo');

console.log(`Found ${comments.length} total comments`);



// Get statistics

const stats = await manager.getCommentStatistics('asset', 'product-photo');

console.log('Comment stats:', stats);



// Generate report for multiple assets

const report = await manager.generateReport('asset', [

  'product-photo',

  'banner-image',

  'logo-design'

]);

console.log('Report:', report);
```

***

## Best Practices[​](#best-practices "Direct link to Best Practices")

### 1. Monitor Unresolved Comments[​](#1-monitor-unresolved-comments "Direct link to 1. Monitor Unresolved Comments")

Set up alerts for assets with many unresolved comments:

```
async function checkUnresolvedAlerts() {

  const assets = await getAssetsInReview();



  for (const asset of assets) {

    const stats = await manager.getCommentStatistics('asset', asset.token);



    if (stats.unresolved > 5) {

      await sendAlert({

        to: 'team@company.com',

        subject: `Action needed: ${asset.title}`,

        body: `This asset has ${stats.unresolved} unresolved comments.`

      });

    }

  }

}
```

### 2. Track Response Times[​](#2-track-response-times "Direct link to 2. Track Response Times")

Measure how quickly teams respond to comments:

```
function calculateResponseTime(comments) {

  const topLevel = comments.filter(c => c.depth === 0);



  const responseTimes = topLevel

    .filter(c => c.hasReplies)

    .map(c => {

      const firstReply = c.replies[0];

      const parentTime = new Date(c.created_at);

      const replyTime = new Date(firstReply.created_at);

      return replyTime - parentTime;

    });



  const avgResponseTime = responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length;



  return {

    average: avgResponseTime / 1000 / 60, // minutes

    count: responseTimes.length

  };

}
```

### 3. Archive Old Threads[​](#3-archive-old-threads "Direct link to 3. Archive Old Threads")

Export resolved comment threads for archival:

```
async function archiveResolvedComments(assetToken, olderThan) {

  const comments = await manager.getAllCommentsFlat('asset', assetToken);



  const toArchive = comments.filter(c =>

    c.resolved_at && new Date(c.resolved_at) < olderThan

  );



  if (toArchive.length > 0) {

    await saveToArchive(assetToken, toArchive);

    console.log(`Archived ${toArchive.length} comments`);

  }

}
```

***

## Related API Endpoints[​](#related-api-endpoints "Direct link to Related API Endpoints")

* [Asset Comments](/docs/api/asset-comments.md)
* [Board Comments](/docs/api/get-organization-collection-comments.md)

***

## Next Steps[​](#next-steps "Direct link to Next Steps")

* Learn about [Sharing and Publishing](/docs/guides/sharing_and_publishing.md) to share assets with external reviewers
* Explore [Webhooks](/docs/guides/webhooks.md) to get notified of new comments
* Read about [Asset Management](/docs/guides/asset_management.md) for organizing commented assets
