Skip to content

Defects API

Defects are project-scoped bug records with per-project sequential numbers (defectNumber, shown as D-<n> in the UI). The reporter is always derived from the authenticated actor on create and cannot be set in the request body.

POST/api/v1/projects/{projectId}/defects

{projectId} is the project code (for example ACME).

FieldRequiredNotes
titleyesNon-empty string
descriptionnoMarkdown text
assigneeUlidnoOrganization member ULID
tagsnoString array
milestoneUlidnoMilestone in the same project
customFieldValuesnoArray of { fieldUlid, value } writes for visible defect fields. The seeded Severity and Priority system fields are required with org defaults (major / medium option ULIDs); omit unchanged defaults and the server persists them. Other omitted keys are satisfied at create time when the field has a non-empty stored default; otherwise create returns 422 validation_failed.

reporter, resolvedAt, and closedAt are rejected if sent by the client.

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Login fails with expired password"}' \
"https://probara.net/api/v1/projects/ACME/defects"
GET/api/v1/projects/{projectId}/defects

Page-based pagination (page, pageSize), newest first. Response shape matches other table list endpoints (items, page, pageSize, total), where total counts every defect matching the active filters and q. Each list item includes the defect fields plus occurrenceCount, lastSeenAt (aggregated from linked run attempts), and systemFieldValues (severity/priority system entries with option enrichment only). When fields is omitted, list items do not include customFieldValues.

Pass fields as a comma-separated list of defect custom-field ULIDs visible in the project to embed customFieldValues on each item (stored value or materialized default, same shape as the detail endpoint). Requesting a field outside the project scope returns 422 validation_failed.

Filters compose with AND semantics across parameters and are evaluated server-side. The set-valued parameters severity, priority, and assigneeUlid take a comma-separated set of ULIDs and OR-combine within each parameter; a single value is accepted as a one-element set, so older single-value URLs keep working.

ParameterNotes
statusComma-separated set, e.g. open,in_progress
severityComma-separated set of option ULIDs of the org’s defect_severity system field (OR within; matches defects holding any)
priorityComma-separated set of option ULIDs of the org’s defect_priority system field (OR within)
assigneeUlidComma-separated set of organization member ULIDs (OR within); a ULID that resolves to no member is ignored, and the filter matches nothing only when none resolve
qTitle substring, or defect number via D-<n> or a bare integer
agingOpen-defect age bucket: d0_7, d7_14, d14_30, d30plus (by created_at)
fieldsComma-separated custom-field ULIDs to embed per item (customFieldValues)
cfRepeatable custom-field filter: <fieldUlid>:<value[,value…]> (see below)

Invalid status values return 422 validation_failed.

Repeat the cf query parameter once per field. Multiple cf entries AND-compose. Within one field, comma-separated values OR-compose.

Supported field types: select, select_multi, checkbox, and user_picker. Other types (including paragraph) return 422 validation_failed. Filters match stored value rows only — a materialized default on read does not satisfy an option filter until persisted.

Value tokenMeaning
Option ULIDDefect stored with that option (select) or containing it (select_multi)
true / falseCheckbox stored value
Member ULIDuser_picker stored member
emptyNo stored row for the field (including defects that would show a default on detail)

Example — defects in Staging or Production (same field, comma OR):

GET /api/v1/projects/ACME/defects?cf=01HENVFIELD:01HSTAGING,01HPROD

Example — defects with no Environment value or Staging, and checkbox Risk accepted:

GET /api/v1/projects/ACME/defects?cf=01HENVFIELD:empty,01HSTAGING&cf=01HRISKFIELD:true

Example — embed Environment and Owner columns:

GET /api/v1/projects/ACME/defects?fields=01HENVFIELD,01HOWNERFIELD
GET/api/v1/projects/{projectId}/defects/metrics

Single response for the defects list strip and sidebar badge. All figures use the active organization and project scope; cross-tenant requests return 404 not_found.

FieldDefinition
totalOpenDefects in open or in_progress
openBySeverityObject with options (array of { optionUlid, systemKey, name, icon, color, count } in option order, zero counts included) and unset (open defects with no severity value row)
agingOpen population bucketed by age since created_at: d0_7, d7_14, d14_30, d30plus
reopenRateDistinct defects with ≥1 defect.reopened audit event ÷ distinct defects ever resolved (defect.status_changed with to of resolved or closed); null when the denominator is 0
mttrMsMean resolved_at − created_at over currently resolved/closed defects with non-null resolved_at; null when none
byMilestoneOpen population per milestone (ulid, name, count) plus unassigned
GET/api/v1/runs/{runUlid}/defects

Lists distinct defects linked to any attempt in the run (join defect_result_linkstest_results for the run). Each item includes public defect fields plus linkCountInRun (links within this run only — links on other runs are excluded), and systemFieldValues for severity/priority. Returns { items: [...] } (no pagination). Cross-tenant or unknown runs return 404 not_found. Works for open and closed runs.

GET/api/v1/defects/{defectUlid}

Returns the full defect payload including vocabulary fields, people ULIDs, tags, milestone, duplicate-of reference, lifecycle timestamps, derived occurrence summary fields (occurrenceCount, lastSeenAt, affectedCaseCount), and embedded customFieldValues for every defect-scoped field visible in the project (stored value or materialized default, with option enrichment when applicable). The project defects list includes customFieldValues only when requested via fields.

Defect-scoped definitions are managed under GET/POST/PATCH/DELETE /api/v1/orgs/{orgUlid}/custom-fields with entity: defect. Values are stored per (field, defect) pair.

PUT/api/v1/defects/{defectUlid}/custom-field-values

Body: { "values": [ { "fieldUlid": "...", "value": ... }, ... ] } — one entry per visible field. Required fields must be present in the write set. Emits defect.updated with metadata.customFieldChanges (not a separate custom_field.* event).

PATCH/api/v1/defects/{defectUlid}/custom-field-values/{fieldUlid}

Body: { "value": ... }. Clears the value when null is sent for nullable types; required fields reject empty clears with 422 validation_failed. Returns the full embedded customFieldValues array for the defect.

PATCH/api/v1/defects/{defectUlid}

Partial update of title, description, status, resolution, assigneeUlid, tags, milestoneUlid, and duplicateOfUlid. Severity and priority are edited via the custom-field value endpoints, not this patch.

Status transitions follow a guided map (open/in_progressresolved/closed; resolvedclosed/open; closedopen). Illegal transitions return 422 validation_failed with field path status. Same-status patches apply other fields without emitting a status event.

  • Transitioning to resolved or closed requires a resolution.
  • resolution = duplicate requires duplicateOfUlid; chains flatten to the canonical root on write.
  • Reopening to open from resolved or closed clears resolution, duplicateOfUlid, and managed timestamps; emits defect.reopened instead of defect.status_changed.
  • duplicateOfUlid must reference another defect in the same project when used.

Requires organization role owner, admin, or member; a viewer receives 403 forbidden before any write.

Returns 404 not_found when the defect is outside the active organization.

DELETE/api/v1/defects/{defectUlid}

Hard-deletes the defect. Returns 204 on success; subsequent GET returns 404 not_found.

Defects can be linked to immutable run attempts (test_results rows). Links are unique per (defect, result) pair; repeating the same request returns the existing link with 200.

POST/api/v1/defects/{defectUlid}/result-links
FieldRequiredNotes
resultUlidyesResult from a run in the same project as the defect
stepSnapshotUlidnoFrozen step snapshot belonging to that result

Returns 201 on first link, 200 when the pair already exists.

GET/api/v1/defects/{defectUlid}/result-links

Cursor pagination (limit, cursor). Each item includes the link, linker, result status/executedAt, and run/test-case context.

DELETE/api/v1/defects/{defectUlid}/result-links/{linkUlid}

Returns 204. The link must belong to the referenced defect.

GET/api/v1/test-cases/{caseUlid}/defects

Lists defects previously linked to any result of the test case, with per-case occurrenceCount, lastSeenAt, and systemFieldValues (severity/priority). Open/in-progress defects are ordered before resolved/closed ones.

GET/api/v1/defects/{defectUlid}/affected-cases

Cursor pagination (limit, cursor). Returns distinct test cases linked through result associations. Each item includes testCaseUlid, testCaseTitle, caseNumber, displayId (the case’s repository reference, {PROJECT_CODE}-{caseNumber}), occurrenceCount, lastLinkedAt, latestResult (overall newest execution for the case, or null when never executed), and verification.

When the defect status is resolved, verification is one of:

ValueMeaning
verifiedLatest result is passed with executedAt after resolvedAt
failed_after_resolveLatest result is failed or blocked after resolvedAt
pendingNo qualifying execution after resolution

For non-resolved defects, verification is null. Returns 404 not_found cross-tenant.

Defect mutations emit defect.created, defect.updated, defect.status_changed, defect.reopened, defect.deleted, defect.linked, and defect.unlinked on the organization activity feed (link events also appear on the run feed via run_id). Evidence and discussion mutations also emit defect.attachment_added, defect.attachment_deleted, defect.commented, defect.comment_updated, and defect.comment_deleted. Duplicate resolutions include duplicateOfUlid and a D-<n> label in defect.status_changed metadata. See Audit events.

GET/api/v1/defects/{defectUlid}/audit-events

Cursor pagination (limit, cursor) over events whose entityType is defect and whose entityUlid matches the defect. Response shape matches the run audit endpoint (events, nextCursor). Returns 404 not_found when the defect is outside the active organization.

Image evidence is stored under the organization R2 prefix. Allowed types: PNG, JPEG, WebP. Max 10 MiB per file, up to 10 files per upload request. Uploads charge the organization defect_attachment storage counter.

Viewers may list attachments; only members and above may upload or delete.

GET/api/v1/defects/{defectUlid}/attachments

Returns { attachments: [...] } in upload order (oldest first). Each item includes ulid, mime, dimensions, byteSize, originalFilename, uploadedBy, url, thumbUrl, and createdAt.

POST/api/v1/defects/{defectUlid}/attachments

multipart/form-data with one or more file parts. Returns 201 with { attachments: [...] }. Disallowed MIME types or oversize files return 422 validation_failed. Emits defect.attachment_added.

DELETE/api/v1/defects/{defectUlid}/attachments/{attachmentUlid}

Returns 204. Removes the row, both R2 objects, and refunds storage bytes. Emits defect.attachment_deleted. An attachment ULID belonging to another defect returns 404 not_found.

Defect deletion removes all evidence objects and refunds summed bytes before the cascade.

Comments are plain text (trimmed, non-empty, max 5000 characters) and support one level of threaded replies, pinning, and @mention resolution. The defect comment surface is structurally identical to test case comments — see that page for the full reference (reply semantics, pinning rules, mention resolution, response shape, and cascade delete). This section lists only the defect-scoped paths and audit actions.

Viewers may list comments; only members and above may create, edit, delete, or pin/unpin. Listing is cursor-paginated over top-level comments (parentUlid absent), defaulting to oldest-first; each item’s inline replies[] stays chronological regardless of sort.

GET/api/v1/defects/{defectUlid}/comments

Query: limit (default 50, max 200), cursor, sort (oldest | newest, default oldest). Each item carries parentUlid, pinnedAt, pinnedBy, replyCount, mentions, and inline replies[]; the response’s pinned[] sibling holds the complete pinned set for the defect. Replies are not enumerated in the flat items[] — see test case comments for the full response shape and the replies caveat.

POST/api/v1/defects/{defectUlid}/comments
FieldRequiredNotes
bodyyesTrimmed text, 1–5000 characters
parentUlidnoReplies to an existing top-level comment on the same defect (depth-1 only)

Returns 201. Emits defect.commented with a 140-character excerpt.

PATCH/api/v1/defects/{defectUlid}/comments/{commentUlid}
FieldRequiredNotes
bodyyesSame validation as create

Author only — admins cannot edit another member’s comment (403 forbidden). Sets editedAt on success and re-resolves mentions. Emits defect.comment_updated.

DELETE/api/v1/defects/{defectUlid}/comments/{commentUlid}

Returns 204. The author may delete their own comment. Organization admins may delete any comment. Other members receive 403. Deleting a top-level comment cascades to its replies, purging their attachments. Emits defect.comment_deleted (once for the deleted comment, plus once per cascaded reply).

POST / DELETE /api/v1/defects/{defectUlid}/comments/{commentUlid}/pin

Any member or above may pin or unpin any top-level comment, regardless of authorship; viewers receive 403. Pinning a reply returns 422 validation_failed. There is no cap on pinned comments per defect. Emits defect.comment_pinned / defect.comment_unpinned.