Skip to content

Test case comments API

Test case comments are attached to a specific test case using a (entity_type='test_case', entity_id) generic comments store, shared with defect comments — both surfaces are structurally identical, differing only in the entity path. Comments are scoped to the organization via the test case ULID.

Comments support one level of threaded replies, pinning, and @mention resolution. See Comment attachments for the image attachment workflow.

MethodPathDescription
GET/api/v1/test-cases/{caseUlid}/commentsList top-level comments (cursor pagination)
POST/api/v1/test-cases/{caseUlid}/commentsAdd a comment, or a reply via parentUlid
PATCH/api/v1/test-cases/{caseUlid}/comments/{commentUlid}Edit a comment or reply
DELETE/api/v1/test-cases/{caseUlid}/comments/{commentUlid}Delete a comment (cascades to its replies)
POST/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pinPin a top-level comment
DELETE/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pinUnpin a top-level comment

{caseUlid} must belong to the active organization. Cross-tenant access returns 404 not_found.

All endpoints require an authenticated session or a Bearer API token with at least viewer access.

OperationMinimum role
GETViewer
POST (comment or reply)Member
PATCHMember (own comment only)
DELETEMember (own) or Admin/Owner (any)
POST / DELETE .../pinMember (any top-level comment)
GET/api/v1/test-cases/{caseUlid}/comments

Returns top-level comments (parentUlid: null) with cursor pagination.

ParameterTypeDescription
cursorstringULID cursor from the previous page (nextCursor).
limitintegerPage size. Default 50, max 200.
sortoldest | newestTop-level ordering. Default oldest (unchanged public default). Replies always stay chronological under their parent regardless of this value.
{
"items": [
{
"ulid": "01J…",
"body": "Reproduced on staging with a fresh browser session.",
"author": {
"ulid": "01J…",
"displayName": "Ada Lovelace",
"avatarKey": null,
"avatarVersion": 0
},
"createdAt": 1700000000000,
"editedAt": null,
"attachments": [],
"parentUlid": null,
"pinnedAt": null,
"pinnedBy": null,
"mentions": [],
"replyCount": 1,
"replies": [
{
"ulid": "01J…RPLY",
"body": "Confirmed — same result on my end. @[01J…GRACE]",
"author": {
"ulid": "01J…",
"displayName": "Grace Hopper",
"avatarKey": null,
"avatarVersion": 0
},
"createdAt": 1700000100000,
"editedAt": null,
"attachments": [],
"parentUlid": "01J…",
"pinnedAt": null,
"pinnedBy": null,
"mentions": [{ "ulid": "01J…GRACE", "displayName": "Grace Hopper" }]
}
]
}
],
"nextCursor": "01J…",
"pinned": []
}
  • author is null when the member was removed from the organization.
  • editedAt is null when the comment has never been edited.
  • nextCursor is null on the last page.
  • pinned is the complete pinned top-level set for this test case, ordered by pinnedAt ascending, independent of the paginated items cursor — render it above the feed. It is identical regardless of sort.
  • A reply object never carries replyCount or replies — thread depth is capped at one level.
  • A top-level item’s replies array is chronological ascending and capped at 100 inline entries.

Important: replies do not appear in the flat items[] enumeration — they are only reachable via their parent’s replies[]. This is additive-compatible but not byte-compatible with clients that assumed items[] enumerated every comment: iterate replies on each item if you need reply content, and use replyCount if you only need a count.

POST/api/v1/test-cases/{caseUlid}/comments
FieldRequiredNotes
bodyyesNon-empty string, max 5000 characters. Whitespace-only values are rejected. May contain @[ulid] mention tokens (see Mentions).
parentUlidnoULID of an existing top-level comment on the same test case. Creates a depth-1 reply. Omit to create a top-level comment (unchanged behavior).
attachmentsnoSee Comment attachments.

Errors:

  • 422 validation_failedbody is empty, whitespace-only, exceeds 5000 characters, or parentUlid points to a comment that is itself a reply (reply-to-reply is rejected).
  • 404 not_foundparentUlid does not resolve to a top-level comment on this test case (wrong entity or wrong organization).
  • 403 — viewer role.
{
"ulid": "01J…",
"body": "Looks good on mobile too.",
"author": { "ulid": "01J…", "displayName": "Ada", "avatarKey": null, "avatarVersion": 0 },
"createdAt": 1700000000000,
"editedAt": null,
"attachments": [],
"parentUlid": null,
"pinnedAt": null,
"pinnedBy": null,
"mentions": [],
"replyCount": 0,
"replies": []
}

Audit: emits test_case.commented with metadata.excerpt (first 240 characters of the body).

PATCH/api/v1/test-cases/{caseUlid}/comments/{commentUlid}

Only the comment’s original author may edit it (author-only, regardless of top-level or reply). Admins and owners cannot edit another member’s comment. Mentions are re-resolved from the new body on every edit.

FieldRequiredNotes
bodyyesReplacement body, same validation as POST.
attachmentsnoFull desired attachment list; see Comment attachments.

Updated comment with editedAt set to the current timestamp and mentions[] reflecting the new body.

Errors: 403 if not the author. 404 if the comment does not exist on this case (wrong caseUlid / wrong org).

Audit: emits test_case.comment_updated with metadata.excerpt.

DELETE/api/v1/test-cases/{caseUlid}/comments/{commentUlid}

Authors may delete their own comment. Admins and owners may delete any comment. Deleting a top-level comment cascades to delete all of its replies too, purging each cascaded reply’s attachments and refunding storage before the rows are removed.

Errors: 403 if the caller is a member trying to delete another member’s comment.

Audit: emits test_case.comment_deleted for the deleted comment, plus one additional test_case.comment_deleted per cascaded reply.

POST/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pin

Pins a top-level comment so it always appears in the pinned[] array of the list response, rendered ahead of the paginated feed regardless of sort. Any member or above may pin any top-level comment (not just their own). There is no cap on the number of pinned comments per test case.

Errors: 422 validation_failed if commentUlid refers to a reply — only top-level comments can be pinned. 403 for viewer role. 404 if the comment does not exist on this case.

Updated comment with pinnedAt (epoch ms) and pinnedBy (actor summary of who pinned it) set.

Audit: emits test_case.comment_pinned.

DELETE/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pin

Clears pinnedAt/pinnedBy. Any member or above may unpin.

Updated comment with pinnedAt: null, pinnedBy: null.

Errors: 403 for viewer role. 404 if the comment does not exist on this case.

Audit: emits test_case.comment_unpinned.

Comment bodies may embed @[ulid] tokens referencing an organization member by ULID (the ULID form, not a display-name string, so renames never break a mention). On create/edit, the server:

  1. Extracts every @[ulid] token from the body.
  2. Resolves each token against the active organization’s membership only — a ulid from another organization, or one that doesn’t resolve to a member, is silently left as plain, unstyled text and creates no mention row.
  3. Persists one row per distinct valid mention, and reconciles the set on every edit (removed mentions are cleared, new ones are added).

The response’s mentions[] array always reflects the mentioned member’s current display name — renaming a member updates every past mention that references them. Mentions never trigger a notification or email.

When a test case is deleted (DELETE /api/v1/test-cases/{caseUlid}), all comments and replies for that test case are deleted first, along with their attachments. No additional API call is required; the cascade is atomic within the delete handler.

Terminal window
# List the newest-first page of comments on a test case
curl -sS \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://probara.net/api/v1/test-cases/01J...CASE/comments?sort=newest"
# Add a top-level comment
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body":"Verified on Safari 17 — passes."}' \
"https://probara.net/api/v1/test-cases/01J...CASE/comments"
# Reply to a top-level comment, mentioning a teammate
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body":"Thanks @[01J...GRACE] — confirmed here too.","parentUlid":"01J...CMT"}' \
"https://probara.net/api/v1/test-cases/01J...CASE/comments"
# Pin a comment
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT/pin"
# Unpin a comment
curl -sS -X DELETE \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT/pin"
# Edit a comment
curl -sS -X PATCH \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body":"Updated note after re-testing."}' \
"https://probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT"
# Delete a comment
curl -sS -X DELETE \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT"