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/v1A 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_HEREGET /api/v1/comments?dealId=UUID_HEREQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
companyId | string | One of | The company whose comment thread to list |
dealId | string | One of | The 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/:idResponds 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| Field | Type | Required | Description |
|---|---|---|---|
parent | object | Yes | The record the comment hangs off: { "companyId": "UUID" } or { "dealId": "UUID" } |
content | string | Yes | Plain text, 1-10,000 characters |
mentionedUserIds | string[] | No | Up to 50 organization member UUIDs to mention (default: []) |
parentCommentId | string | No | The 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 inparent(companyIdordealId) orparentCommentIddoes not exist, or belongs to another organization - 400 with
{ "error": "You can only reply to a top-level comment." }whenparentCommentIdnames a comment that is itself a reply - 400 with
{ "error": "A comment cannot be empty." }whencontentis 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| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Plain text, 1-10,000 characters |
mentionedUserIds | string[] | No | Up 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:iddoes 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." }whencontentis blank after trimming - 422 with
{ "error": "Validation failed", "details": {...} }for any other invalid input
Delete Comment
DELETE /api/v1/comments/:idReturns { "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
:idis not a UUID - 404 with
{ "error": "Comment not found." }when:iddoes 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}"},
)