Audit events API
Probara exposes two read-only audit listing endpoints:
| Endpoint | Scope | Minimum role |
|---|---|---|
GET /api/v1/orgs/{orgUlid}/audit-events | Entire organization | Owner or admin |
GET /api/v1/runs/{runUlid}/audit-events | Single test run | Any member who can read the run (viewer+) |
Both return the same AuditEventOut envelope with cursor pagination. Events are ordered by ULID descending (newest first).
Organization-wide listing
Section titled “Organization-wide listing”GET /api/v1/orgs/{orgUlid}/audit-events returns the append-only audit log for an organization.
Authorization
Section titled “Authorization”- Session: sign in in the browser and send the session cookie plus
X-Organization-Idwhen calling from custom scripts. - Bearer token: organization-scoped API token with owner or admin membership on that org.
Members and viewers receive 403 Forbidden.
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
limit | integer | Page size. Default 50. Values above 100 are silently capped to 100. |
cursor | ULID | When set, returns events with ulid < cursor (older page). |
Response shape
Section titled “Response shape”{ "events": [ { "ulid": "01J…", "action": "project.created", "actor": { "kind": "user", "ulid": "01J…", "email": "alice@example.com", "displayName": null }, "entityType": "project", "entityUlid": "01J…", "entityLabel": "Acme Web", "runUlid": null, "metadata": {}, "createdAt": 1700000000000 } ], "nextCursor": "01J…"}nextCursor is the ULID of the last event in events when another page exists; otherwise null.
runUlid is present on run-scoped events (and on org-wide listings when the parent run still exists). Non-run events omit the field or set it to null.
Run-scoped listing
Section titled “Run-scoped listing”GET /api/v1/runs/{runUlid}/audit-events returns only events whose run_id matches the resolved run inside the active organization. Use the same session cookie or bearer token as other run APIs; send X-Organization-Id for browser sessions.
Authorization matches GET /api/v1/runs/{runUlid} — any organization member with access to the run may list its history. There is no owner/admin gate on this route.
Query parameters and the response envelope are identical to the organization-wide endpoint (limit, cursor, events, nextCursor). A 404 means the run ULID does not exist in the active organization.
Example:
export PROBARA_API_TOKEN="probara_…"export RUN_ULID="01J…"
curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/runs/${RUN_ULID}/audit-events?limit=50"Actor variants
Section titled “Actor variants”{ "kind": "api_token", "ulid": "01J…", "name": "CI deploy bot" }{ "kind": "system" }When a user or token was deleted, reference fields are null but the event remains in the list:
{ "kind": "user", "ulid": null, "email": null, "displayName": null }Example
Section titled “Example”export PROBARA_API_TOKEN="probara_…"export ORG_ULID="01J…"
curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/orgs/${ORG_ULID}/audit-events?limit=50"Next page:
curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/orgs/${ORG_ULID}/audit-events?cursor=${NEXT_CURSOR}"Metadata and structured diffs
Section titled “Metadata and structured diffs”metadata is an object that may include shared diff buckets (documented in the OpenAPI components AuditEventDiff, AuditAssigneeChange, and AuditRunAggregates) plus action-specific snapshot keys. Integrators SHOULD treat unknown keys as forward-compatible.
Common diff buckets:
| Key | Used by | Shape |
|---|---|---|
changes | *.updated, run.case_marked, run.case_step_marked, run.case_retried, run.case_step_actual_result_set, run.result_attachment_committed (when text also changed) | AuditFieldChange[] — scalar from / to with optional fromLabel / toLabel for reference fields |
assigneeChange | run.case_assigned | { from: { ulid, name } | null, to: { ulid, name } | null, via: 'open' | 'patch' | 'reconcile' } |
bulkCases | run.cases_bulk_* | { cases: AuditBulkCaseEntry[], omittedCount?, summary: { affected, unchanged, statusFrom?, statusTo?, assigneeFrom?, assigneeTo? } } — up to 20 per-case entries at emission; remainder in omittedCount |
runAggregates | run.closed, run.aborted | { countPassed, countFailed, countBlocked, countSkipped, countUntested, totalDurationMs, projectedStatus: 'passed' | 'failed' | 'aborted' } — run.aborted always reports projectedStatus: 'aborted', even when the case counts alone would read as 'passed'/'failed' |
String snapshots longer than 240 graphemes are clipped at emission time; affected objects set truncated: true per the shared audit convention.
entity.label formats for runs
Section titled “entity.label formats for runs”entityType | Canonical label |
|---|---|
test_run | Run name at emission time |
test_run_case | ${caseDisplayId} · ${titleSnapshot} — caseDisplayId is ${projectCode}-${caseNumber} (e.g. ACME-1) |
test_run_case_step | ${caseDisplayId} · ${titleSnapshot} — ${stepActionSnapshot ?? `step ${stepPosition}`} |
List responses may add link metadata (runUlid, projectCode, caseDisplayId, titleSnapshot, runName) when the stored row omitted them; stored snapshots always win.
run.* metadata by action
Section titled “run.* metadata by action”| Action | Notable metadata keys |
|---|---|
run.created | caseCount, optional environment, defaultAssignee, milestone |
run.updated | changes (name, description, defaultAssigneeUlid, environment, milestoneUlid) |
run.closed | runAggregates (replaces legacy flat status) |
run.aborted | runAggregates (same shape as run.closed; projectedStatus is always 'aborted') |
run.deleted | runAggregates, wasState (open | closed), closedAt (null when still open) |
run.cloned | caseCount, sourceRun, cloneAssignees, statusFilter (null when every case was cloned) |
run.case_added | added, cases (first 20), optional omittedCount |
run.case_marked | caseDisplayId, titleSnapshot, changes (status), optional elapsedMs |
run.case_step_marked | caseDisplayId, titleSnapshot, stepUlid, stepPosition, stepActionSnapshot, changes (status), caseStatus |
run.case_assigned | caseDisplayId, titleSnapshot, assigneeChange |
run.case_retried | caseDisplayId, titleSnapshot, changes (status → untested), optional previousElapsedMs |
run.case_removed | caseDisplayId, titleSnapshot, statusAtRemoval |
run.cases_bulk_marked | bulkCases (per-case fromStatus / toStatus; homogeneous summary.statusFrom / statusTo when applicable) |
run.cases_bulk_assigned | bulkCases (per-case fromAssignee / toAssignee; homogeneous assignee summary when applicable) |
run.cases_bulk_retried | bulkCases (per-case retry transitions; summary.unchanged for cases already untested) |
run.cases_bulk_removed | bulkCases (per-case statusAtRemoval) |
run.case_step_actual_result_set | caseDisplayId, titleSnapshot, stepUlid, stepPosition, stepActionSnapshot, changes (actual_result from → to) |
run.result_attachment_committed | caseDisplayId, titleSnapshot, stepUlid, stepPosition, attachments; optional changes when actual_result text changed in the same request |
run.result_attachment_deleted | Same shape as run.result_attachment_committed |
run.reopened and run.reran_failed remain valid values of the action enum so historical rows keep parsing, but neither is emitted anymore — Reopen was removed and rerun-failed was generalized into run.cloned (POST .../runs/{runUlid}/clone).
Example — close scorecard:
{ "action": "run.closed", "entityType": "test_run", "entityLabel": "Sprint 24", "metadata": { "runAggregates": { "countPassed": 7, "countFailed": 2, "countBlocked": 1, "countSkipped": 0, "countUntested": 0, "totalDurationMs": 935000, "projectedStatus": "failed" } }}Example — case reassignment:
{ "action": "run.case_assigned", "entityType": "test_run_case", "entityLabel": "ACME-1 · Login flow", "metadata": { "caseDisplayId": "ACME-1", "titleSnapshot": "Login flow", "assigneeChange": { "from": { "ulid": "01J…", "name": "María López" }, "to": { "ulid": "01J…", "name": "Pedro García" }, "via": "patch" } }}Run lifecycle mutations (abort, clone, and delete)
Section titled “Run lifecycle mutations (abort, clone, and delete)”These endpoints mutate runs and emit the audit actions documented above. They are not on the audit listing routes themselves.
POST /api/v1/runs/{runUlid}/abort
Section titled “POST /api/v1/runs/{runUlid}/abort”- Role: member, admin, or owner (
viewer→403) - Body: none
- Response:
200with the updatedRunResponse(state: "closed",abortedAtset to the abort time) - Errors:
409when the run is not currently open (already closed or already aborted) - Audit:
run.abortedwithmetadata.runAggregates(projectedStatusalways'aborted') - Untested cases are left untested — abort records no case results. This is terminal: an aborted run stays frozen (case mutations return
409); there is no reopen. Use.../clone(below) to start a fresh run from it.
POST /api/v1/projects/{projectId}/runs/{runUlid}/clone
Section titled “POST /api/v1/projects/{projectId}/runs/{runUlid}/clone”“Run again”: generalizes the former rerun-failed endpoint (removed).
- Role: member, admin, or owner (
viewer→403) - Body:
{ "title": "string, required, max 200", "cloneAssignees"?: boolean, "statusFilter"?: TestOutcomeStatus[] }—statusFilterselects which source cases carry over; omitted or empty means every case. - Response:
201with the newRunResponse(state: "open") - Errors:
422whentitleis empty orstatusFiltercontains an unknown status - Audit:
run.clonedwithmetadata.caseCount,metadata.sourceRun,metadata.cloneAssignees,metadata.statusFilter - Works on any closed source run — completed or aborted. When
cloneAssigneesistrue, each carried-over case keeps the source case’s assignee; otherwise every case in the new run starts unassigned.
DELETE /api/v1/runs/{runUlid}
Section titled “DELETE /api/v1/runs/{runUlid}”- Role: admin or owner only (
member/viewer→403) - Response:
204 No Content - Audit:
run.deletedwithmetadata.runAggregates,wasState, andclosedAtcaptured before the cascade. After delete,run_idon audit rows (including this one) is set tonullby FK cascade; the event remains visible in org-wide listings.
Mutations
Section titled “Mutations”Audit listing endpoints are read-only. Use the run routes above (and other product APIs) to produce new audit rows.
Comment actions
Section titled “Comment actions”These actions are emitted for threaded comments on entity pages, including replies (parentUlid set) and pin/unpin. The commented, comment_updated, and comment_deleted actions include metadata.commentUlid and metadata.excerpt (first 240 characters of the comment body); comment_pinned and comment_unpinned include only metadata.commentUlid (pinning carries no body excerpt).
| Action | Emitted when | Entity type |
|---|---|---|
defect.commented | A comment or reply is added to a defect | defect |
defect.comment_updated | A comment on a defect is edited | defect |
defect.comment_deleted | A comment on a defect is deleted (once per cascaded reply too) | defect |
defect.comment_pinned | A top-level comment on a defect is pinned | defect |
defect.comment_unpinned | A top-level comment on a defect is unpinned | defect |
test_case.commented | A comment or reply is added to a test case | test_case |
test_case.comment_updated | A comment on a test case is edited | test_case |
test_case.comment_deleted | A comment on a test case is deleted (once per cascaded reply too) | test_case |
test_case.comment_pinned | A top-level comment on a test case is pinned | test_case |
test_case.comment_unpinned | A top-level comment on a test case is unpinned | test_case |
Example:
{ "action": "test_case.commented", "entityType": "test_case", "entityLabel": "TC-1 Login with valid credentials", "metadata": { "commentUlid": "01J…", "excerpt": "Reproduced on staging with fresh session." }}Token management
Section titled “Token management”Create and revoke API tokens under API authentication (/workspace/api-tokens in the product UI).