Anomalies API
This page documents the REST endpoints for working with anomalies in Qualytics. All endpoints share the base URL of your deployment (for example, https://your-instance.qualytics.io/api) and require a Bearer token.
Sections below are grouped by what you are trying to do. The same anomaly object is exchanged across reads and writes: see the Anomaly schema section for the field reference.
Tip
For complete API documentation, including request/response schemas, visit the API docs.
Permissions
All endpoints require the Member role. Most reads need the Viewer team permission on the anomaly's datastore (reading comments needs only the Reporter team permission); writes need the Author team permission (or higher), including description edits. Ticket-link writes need the Manager role plus the Author team permission on the datastore. Per-endpoint requirements are called out below.
Anomaly schema
Every read endpoint that returns an anomaly emits the same shape. The most relevant fields are:
| Field | Type | Notes |
|---|---|---|
id |
int |
Numeric primary identifier. |
uuid |
string (UUID) |
Stable, globally unique identifier. |
type |
"record" \| "shape" |
Anomaly category. See Types. |
status |
string |
One of Active, Acknowledged, Resolved, Duplicate, Invalid, Discarded. See Status. |
weight |
int |
Severity score. |
anomalous_records_count |
int |
Number of records flagged for record anomalies. |
description |
string |
User-editable business description. |
created |
string (ISO 8601) |
When the anomaly was detected. |
fingerprint |
int |
Used to deduplicate recurring anomalies. See Fingerprints. |
global_tags |
Tag[] |
Tags applied to the anomaly. |
assignees |
UserStub[] |
Users assigned to the anomaly. See Assignees. |
failed_checks |
FailedCheck[] |
Checks that produced the anomaly. |
datastore, container, partition_scan |
refs | Source location of the anomaly. |
Example response (abbreviated)
{
"id": 12345,
"uuid": "8e8b9f8b-1234-4abc-9def-012345678901",
"type": "record",
"status": "Active",
"weight": 54,
"anomalous_records_count": 1,
"description": "Customer ID is missing for new orders",
"created": "2026-05-15T17:22:08Z",
"fingerprint": 9182736455,
"global_tags": [{ "id": 7, "name": "High" }],
"assignees": [
{ "id": 42, "name": "Alice Lee", "email": "alice@example.com" }
],
"failed_checks": [{ "id": 778, "rule_type": "notNull", "message": "..." }]
}
List and retrieve
List anomalies
Returns a paginated list of anomalies sorted by creation timestamp and severity (most recent first).
Endpoint: GET /api/anomalies
Permission: Member. Results are scoped to datastores the caller can see.
Common query parameters (all optional, most accept multiple values):
| Parameter | Type | Description |
|---|---|---|
search |
string |
Substring match on the anomaly message or ID. |
status |
AnomalyStatusType[] |
Filter by status (Active, Acknowledged, Resolved, Duplicate, Invalid, Discarded). |
anomaly_type |
"record" \| "shape" |
Filter by anomaly type. |
datastore, container |
int[] |
Scope to specific datastores or containers. |
field, quality_check, rule_type |
varies | Scope to specific fields, checks, or rule types. |
tag |
string[] |
Filter by tag names. |
assignee |
int[] |
Filter by anomaly assignee user IDs. |
archived |
"include" \| "only" |
Include or restrict to archived anomalies. |
timeframe, created_date, start_date, end_date |
varies | Time-based filters. |
related_to_id |
int |
Return anomalies that share a fingerprint with the given anomaly. |
sort_id, sort_created, sort_weight, sort_anomalous_records_count |
"asc" \| "desc" |
Sort options. |
Example request
curl -X GET "https://your-instance.qualytics.io/api/anomalies?status=Active&assignee=42&tag=High&sort_weight=desc" \
-H "Authorization: Bearer YOUR_TOKEN"
Returns active anomalies assigned to user 42 with the tag High, sorted by severity descending.
Get a single anomaly
Endpoint: GET /api/anomalies/{id}
{id} accepts either the numeric ID or the UUID. Pass include_deleted=true to surface anomalies that have been soft-deleted.
Example
Get failed checks for an anomaly
Returns the list of checks that produced the anomaly, with the message text and the rule type for each.
Endpoint: GET /api/anomalies/{id}/failed-checks
Update
Update a single anomaly
Updates the writable fields of an anomaly. Only the fields you include in the body are changed; omitted fields are left untouched.
Endpoint: PUT /api/anomalies/{id}
Permission: Member role + Author team permission on the anomaly's datastore. This covers status, tags, assignee, and description updates.
Body fields:
| Field | Type | Description |
|---|---|---|
status |
"Active" \| "Acknowledged" |
Change the open-state of the anomaly. Setting "Acknowledged" on an archived anomaly restores it to the Acknowledged state (direct restore to Active is not allowed). Use the delete endpoint to archive. |
description |
string |
User-editable business description. |
tags |
string[] |
Tag names. Replaces the current set; pass [] to clear. |
assignee_ids |
int[] |
User IDs assigned to the anomaly. Replaces the current set; pass [] to unassign everyone. Each user must have at least the Viewer team permission on the datastore. |
Example request
curl -X PUT "https://your-instance.qualytics.io/api/anomalies/12345" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "Acknowledged",
"description": "Investigating with the data engineering team",
"tags": ["High", "Customer-Impact"],
"assignee_ids": [42, 57]
}'
Bulk update anomalies
Applies the same changes to many anomalies in one call.
Endpoint: PATCH /api/anomalies
Permission: Member role + Author team permission on each anomaly's datastore. This covers status, tags, assignee, and description updates.
Body: A list of bulk-update entries, each with the anomaly identifier plus the fields to change. Per-entry fields match the single-update body, with the addition of:
| Field | Type | Description |
|---|---|---|
id |
int or UUID |
The anomaly to update. |
Example
Manage assignees
Assignees are managed via the standard update endpoints. The assignee_ids field is replace-only, so pass the full target set on every write rather than the changes you want to apply.
Endpoints:
PUT /api/anomalies/{id}withassignee_idsin the body for a single anomaly.PATCH /api/anomalieswithassignee_idsper entry for many anomalies.GET /api/anomalies?assignee=<id>to filter the list by assignee.
Behavior:
- Each user passed in
assignee_idsmust have at least the Viewer team permission on the anomaly's datastore. Users without access are rejected. - Pass
assignee_ids: []to unassign everyone. - To add a user to an existing set, read
assigneesfirst and submit the union. The API does not support append. - Auto-assignment from a check's default assignee runs only at anomaly creation and is not reachable through this endpoint. See Deep Dive · Anomaly Assignees · Inheritance.
Set or replace the assignee list
Clear all assignees
Bulk-replace assignees across many anomalies
See Deep Dive · Anomaly Assignees for inheritance rules, notification behavior, and history tracking.
Archive and delete
The single-anomaly endpoint covers both the archive flow (mark with a resolution status and keep for audit) and the hard-delete flow (remove the record entirely). Bulk versions follow the same pattern.
Archive or delete a single anomaly
Endpoint: DELETE /api/anomalies/{id}
Permission: Member role + Author team permission on the anomaly's datastore.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status |
"Resolved" \| "Duplicate" \| "Invalid" \| "Discarded" |
The archived status to apply. Required when archive=true. |
archive |
bool |
true (default) archives the anomaly with the resolution status; false permanently deletes the anomaly. The hard-delete is irreversible. |
Body (optional): Lets you attach a comment and tag teammates when archiving.
| Field | Type | Description |
|---|---|---|
status |
ArchivedAnomalyStatusType |
Same as the query parameter; either may be used. |
comment |
string |
Comment posted to the anomaly's history when archiving. |
mentioned_user_ids |
int[] |
Users to notify via @mention in the comment. |
Example request: archive with a resolution comment
Example request: hard-delete an archived anomaly
Bulk archive or delete
Endpoint: DELETE /api/anomalies
Permission: Member role + Author team permission on each anomaly's datastore.
Body: A list of entries combining the anomaly ID with the same fields used in the single-anomaly delete (status, comment, mentioned_user_ids, archive).
Example
curl -X DELETE "https://your-instance.qualytics.io/api/anomalies" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{ "id": 12345, "archive": true, "status": "Resolved" },
{ "id": 12346, "archive": true, "status": "Duplicate" },
{ "id": 12347, "archive": false }
]'
Timeline and comments
Get the anomaly history
Returns the paginated change log behind the anomaly's Timeline section: status changes, description edits, tag updates, assignee changes, and external ticket links and unlinks. Each entry carries the user who made the change and a timestamp.
Endpoint: GET /api/anomalies/{id}/history
Query parameters:
| Parameter | Type | Description |
|---|---|---|
page |
int (default 1) |
Page number to return, starting at 1. |
size |
int (default 50, max 100) |
Number of entries per page. |
include_deleted |
bool (default false) |
When true, also returns history for soft-deleted anomalies. |
Example response (abbreviated)
{
"items": [
{
"changeset": {
"status": ["Active", "Acknowledged"],
"assignees": [
[{ "id": 42, "name": "Alice Lee" }],
[{ "id": 42, "name": "Alice Lee" }, { "id": 57, "name": "Bob Patel" }]
],
"ticket_link": [
null,
{
"ticket_number": "INC0010001",
"ticket_url": "https://example.service-now.com/incident.do?sys_id=abc123sys",
"integration_type": "servicenow"
}
]
},
"transaction": {
"id": 9876,
"issued_at": "2026-05-15T17:22:08Z",
"user": { "id": 1, "name": "Alice Lee" }
},
"operation": "update"
}
],
"total": 124,
"page": 1,
"size": 50,
"pages": 3
}
Each changeset field inside items[].changeset is a [before, after] tuple, so you can reconstruct the audit trail offline. The transaction object on each item carries the id, the issued_at timestamp, and the user who made the change. The operation field reports whether the entry is an insert, update, or delete. To load successive windows, increment page until it reaches pages.
Get comments
Returns the comments on an anomaly, newest first, as a plain array rather than a paginated envelope. Replies are returned together with the comment that starts their thread, and a comment that was deleted while it still had replies is returned with an empty message and its mentions cleared.
Endpoint: GET /api/anomalies/{id}/comments
Permission: Reporter team permission on the anomaly's datastore or above.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
start_date |
datetime (ISO 8601, inclusive) |
Lower bound on created. Applies to comments that are not anchored to a Timeline entry. |
end_date |
datetime (ISO 8601, exclusive) |
Upper bound on created. Lets you fetch back-to-back ranges without overlap. |
transaction_id |
int (repeatable) |
Returns the comments anchored to these Timeline entries, regardless of the date window. |
Each comment carries the following fields alongside its message and created timestamp:
| Field | Type | Description |
|---|---|---|
entity_type |
string |
The kind of asset the comment belongs to: anomaly, container, quality_check, or partition. |
entity_id |
int |
Identifier of that asset. |
transaction_id |
int (nullable) |
The Timeline entry the comment is anchored to. null when the comment is not anchored to a change. |
parent_id |
int (nullable) |
The comment this one replies to. null when the comment starts a thread. |
deleted_at |
datetime (nullable) |
Set when the comment was deleted but kept as a placeholder because it still has replies. |
Response change
Comment responses now identify the asset with entity_type and entity_id. The previous anomaly_id and partition_id fields are no longer returned by GET /api/anomalies/{id}/comments, GET /api/comments, or GET /api/comments/{id}. Update any integration that reads those fields. Requests are unaffected: the previous anomaly_id and partition_id payload shapes are still accepted when creating comments.
Post a comment
Endpoint: POST /api/comments
Permission: Viewer team permission on the anomaly's datastore or above.
Body fields:
| Field | Type | Description |
|---|---|---|
entity_type |
string |
anomaly, container, quality_check (also used for check templates), or partition. Partitions have no Timeline, so their comments cannot use transaction_id. |
entity_id |
int |
Identifier of the asset. |
message |
string |
Comment text. Use @[Full Name](user_id) to mention a user. |
transaction_id |
int (optional) |
Anchors the comment to a Timeline entry of that same asset. |
parent_id |
int (optional) |
Posts the comment as a reply to an existing comment on that same asset. |
mentioned_user_ids |
array[int] (optional) |
Users to notify. Must be active users. |
Example
curl -X POST "https://your-instance.qualytics.io/api/comments" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entity_type": "anomaly",
"entity_id": 12345,
"transaction_id": 9876,
"message": "Expected during the migration window. @[Alice Lee](42) please confirm.",
"mentioned_user_ids": [42]
}'
Use PUT /api/comments/{id} to update a comment (author only) and DELETE /api/comments/{id} to delete it (author, or any user with the Admin role). Deleting a comment that still has replies keeps it as a placeholder so the replies survive; a comment with no replies is removed outright. Deleting the last remaining reply also removes the placeholder it hung from, so placeholders do not pile up in the thread.
The create and update responses carry the comment's message, author, and mentions only; to read entity_type, entity_id, transaction_id, parent_id, or deleted_at back, fetch the comment again through one of the GET endpoints.
Rejected writes come back with a status code that tells you what to correct:
| Status | Returned when |
|---|---|
422 |
The comment replies to a reply (threads are one level deep), the transaction_id is not a Timeline entry of that same asset, the parent_id belongs to a different asset, a mentioned user does not exist or is no longer active, or commenting is not supported for that asset type. |
409 |
The asset is archived, so comments cannot be added, edited, or deleted until it is unarchived. Also returned when replying to a deleted comment or editing a deleted comment. |
403 |
You lack the required team permission on the asset's datastore, or you are trying to edit another user's comment. Only the author can edit a comment. |
404 |
The comment or the asset does not exist. |
Reading comments on an archived anomaly still works. Only writes are blocked while it stays archived.
The same comment endpoints serve containers, quality checks, and check templates. Container comments are read through GET /api/containers/{id}/comments. A check template is a quality check for commenting purposes, so both quality checks and check templates are read through GET /api/quality-checks/{id}/comments and posted with entity_type: quality_check. Both endpoints accept the same query parameters as the anomaly endpoint above. A check template does not belong to a datastore, so its comments are not gated by team permissions: any user with the Member role can read and post them. The paginated GET /api/comments feed spans every asset type only for users with the Admin role; for everyone else it returns anomaly comments alone.
Source records
Source records are the raw rows from the source data that contributed to a record anomaly. Two formats are available: JSON for inline display and CSV for export.
Get source records as JSON
Endpoint: GET /api/anomalies/{id}/source-record
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int (≥1, default 10) |
Maximum number of rows returned. |
include_masked |
bool (default false) |
When true, returns raw values for masked fields. Requires the Editor team permission on the container, and the platform writes an audit-log entry naming the masked fields you accessed. |
Download source records as CSV
Endpoint: GET /api/anomalies/{id}/source-record/download
Streams a CSV file (source_records_<id>.csv). Same query parameters as the JSON endpoint.
Note
Source records are cached for up to 8 hours. If you need fresher data, see the Refresh Source Records section.
Tickets
Manage the link between an anomaly and an external ticket (Jira, ServiceNow). Linking does not call the external system; it just records the association so the UI can render the linked ticket.
List ticket links
Endpoint: GET /api/anomalies/{anomaly_id}/ticket-links
Create a ticket link
Endpoint: POST /api/anomalies/{anomaly_id}/ticket-links
Permission: Manager role + Author team permission on the anomaly's datastore.
Body fields:
| Field | Type | Description |
|---|---|---|
integration_id |
int |
The ticketing integration that owns this ticket. |
ticket_id |
string |
External ticket identifier (e.g., the ServiceNow sys_id). |
ticket_number |
string |
Human-readable ticket number (e.g., INC0010001). |
ticket_url |
string (optional) |
Direct URL to the ticket. |
status |
string |
Current external status of the ticket. |
ticket_metadata |
object (optional) |
Additional metadata to store alongside the link. |
Example
curl -X POST "https://your-instance.qualytics.io/api/anomalies/12345/ticket-links" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"integration_id": 3,
"ticket_id": "abc123sys",
"ticket_number": "INC0010001",
"ticket_url": "https://example.service-now.com/incident.do?sys_id=abc123sys",
"status": "New"
}'
Delete a ticket link
Endpoint: DELETE /api/anomalies/{anomaly_id}/ticket-links/{link_id}
Permission: Manager role + Author team permission on the anomaly's datastore. Removes the association only; the external ticket is left untouched.