Comments

The REST API endpoints for managing comment threads on a company or deal record.

Endpoints

All endpoints require a valid API key in the Authorization header. See Authentication for details.

Base URL: /api/v1

A comment thread hangs off exactly one parent record: a company or a deal. Every endpoint below that names companyId accepts dealId as an alternative — one or the other, never both.


List Comments

GET /api/v1/comments?companyId=UUID_HERE
GET /api/v1/comments?dealId=UUID_HERE

Query parameters:

ParameterTypeRequiredDescription
companyIdstringOne ofThe company whose comment thread to list
dealIdstringOne ofThe deal whose comment thread to list

Exactly one of companyId or dealId must be present.

This endpoint has no pagination: it returns the whole thread for one parent record — every root comment, each carrying its own replies nested one level under it — with no limit/offset and nothing to page. The response is { "data": [...] }, with no total, unlike the paginated list endpoints on other resource pages.

Responds 404 with { "error": "The record this comment belongs to was not found." } when the record named by companyId or dealId does not exist or belongs to another organization. Responds 422 when neither companyId nor dealId is a valid UUID.


Get Comment

GET /api/v1/comments/:id

Responds 400 when :id is not a UUID, 404 with { "error": "Comment not found." } when no comment with that ID exists in your organization.


Create Comment

POST /api/v1/comments
Content-Type: application/json
FieldTypeRequiredDescription
parentobjectYesThe record the comment hangs off: { "companyId": "UUID" } or { "dealId": "UUID" }
contentstringYesPlain text, 1-10,000 characters
mentionedUserIdsstring[]NoUp to 50 organization member UUIDs to mention (default: [])
parentCommentIdstringNoThe UUID of a root comment to reply to

content is plain text, not HTML. There is no rich-text editor on this surface: the server builds the @mention markup itself from mentionedUserIds, and any HTML or markup you send in content is escaped rather than interpreted — it shows up as literal text, never as formatting or an executable tag.

A reply is one level deep: parentCommentId must name a root comment, not another reply. Only member IDs that actually belong to the organization are turned into mentions — an ID from another organization, or one that does not resolve to a member, is silently dropped rather than written.

On success, responds 201 with { "data": {...} } — see Response shape.

Error cases:

  • 404 with { "error": "The record this comment belongs to was not found." } when the record named in parent (companyId or dealId) or parentCommentId does not exist, or belongs to another organization
  • 400 with { "error": "You can only reply to a top-level comment." } when parentCommentId names a comment that is itself a reply
  • 400 with { "error": "A comment cannot be empty." } when content is blank after trimming
  • 422 with { "error": "Validation failed", "details": {...} } for any other invalid input

Update Comment

PATCH /api/v1/comments/:id
Content-Type: application/json
FieldTypeRequiredDescription
contentstringYesPlain text, 1-10,000 characters
mentionedUserIdsstring[]NoUp to 50 organization member UUIDs to mention (default: [])

The same plain-text-in rule as Create applies: content is escaped, never interpreted, and the mention markup is rebuilt server-side from mentionedUserIds.

Only the comment's author may update it, and only while it has no replies.

Error cases:

  • 404 with { "error": "Comment not found." } when :id does not exist or belongs to another organization
  • 400 with { "error": "This comment already has replies and can no longer be edited or deleted." } when the comment already has one or more replies
  • 403 with { "error": "You do not have permission to perform this action." } when the API key's user is not the comment's author
  • 400 with { "error": "A comment cannot be empty." } when content is blank after trimming
  • 422 with { "error": "Validation failed", "details": {...} } for any other invalid input

Delete Comment

DELETE /api/v1/comments/:id

Returns { "success": true } on success. Only the comment's author, or an owner or admin of the organization, may delete it, and only while it has no replies.

Error cases:

  • 400 when :id is not a UUID
  • 404 with { "error": "Comment not found." } when :id does not exist or belongs to another organization
  • 400 with { "error": "This comment already has replies and can no longer be edited or deleted." } when the comment already has one or more replies
  • 403 with { "error": "You can only delete your own comments, unless you are an owner or admin of this organization." }

Response shape

{
  "id": "0c1f5b8e-...",
  "content": "<p>Following up after the call.</p><p><span data-mention-user-id=\"9a2d...\" class=\"rich-text-mention\">@Ada Lovelace</span></p>",
  "author": { "id": "3f7e...", "name": "Grace Hopper" },
  "parent": { "companyId": "9a2d..." },
  "parentCommentId": null,
  "mentionedUserIds": ["9a2d..."],
  "createdAt": "2026-09-01T14:54:01.000Z",
  "editedAt": null,
  "replies": []
}

content is the stored HTML — the mention markup the server built from mentionedUserIds at write time, not the plain text you sent. author carries only id and name, never an image. parent names the record the thread hangs off — { "companyId": "..." } or { "dealId": "..." }, depending on what the comment is attached to; parentCommentId is null on a root comment and the root's UUID on a reply. replies is only ever populated on a root comment returned from List Comments or Get Comment — it holds that comment's replies in the same shape, minus their own replies key, since a reply cannot itself have replies.

createdAt is ISO 8601 in UTC; editedAt is null until the comment is updated, then the ISO 8601 timestamp of the last edit. The API is not localized, whatever language the app itself is showing you — see Data Formats.


Examples

cURL

# Create a comment
curl -X POST https://your-app.com/api/v1/comments \
  -H "Authorization: Bearer ba_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{"parent":{"companyId":"UUID_HERE"},"content":"Following up after the call."}'

# Reply to a comment
curl -X POST https://your-app.com/api/v1/comments \
  -H "Authorization: Bearer ba_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{"parent":{"companyId":"UUID_HERE"},"content":"Thanks for the update.","parentCommentId":"COMMENT_UUID_HERE"}'

# Update a comment
curl -X PATCH https://your-app.com/api/v1/comments/COMMENT_UUID_HERE \
  -H "Authorization: Bearer ba_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{"content":"Following up after the call — now closed."}'

# Delete a comment
curl -X DELETE https://your-app.com/api/v1/comments/COMMENT_UUID_HERE \
  -H "Authorization: Bearer ba_a1b2c3d4e5f6..."

JavaScript / TypeScript

const API_KEY = "ba_a1b2c3d4e5f6...";
const BASE_URL = "https://your-app.com/api/v1";

// Create a comment
const createRes = await fetch(`${BASE_URL}/comments`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    parent: { companyId: "UUID_HERE" },
    content: "Following up after the call.",
    mentionedUserIds: ["MEMBER_UUID_HERE"],
  }),
});
const { data: newComment } = await createRes.json();

// Delete a comment
await fetch(`${BASE_URL}/comments/${newComment.id}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${API_KEY}` },
});

Python

import requests

API_KEY = "ba_a1b2c3d4e5f6..."
BASE_URL = "https://your-app.com/api/v1"

# Create a comment
response = requests.post(
    f"{BASE_URL}/comments",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "parent": {"companyId": "UUID_HERE"},
        "content": "Following up after the call.",
        "mentionedUserIds": ["MEMBER_UUID_HERE"],
    },
)
new_comment = response.json()["data"]

# Delete a comment
requests.delete(
    f"{BASE_URL}/comments/{new_comment['id']}",
    headers={"Authorization": f"Bearer {API_KEY}"},
)

On this page