# Anomalies API

Source: https://userguide.qualytics.io/anomalies/api/

You can list, read, update, archive, and delete anomalies programmatically, and work with their history, comments, source records, and tickets.

> **Tip**
>
> For complete API documentation, including request/response schemas, visit the [API docs](https://userguide.qualytics.io/api/docs).

All endpoints use the base URL of your Qualytics deployment (e.g., `https://your-instance.qualytics.io/api`) and require a Personal API Token. Every endpoint requires the **Member** role. Reads also need the **Reporter** team permission on the anomaly's datastore, and changes need the **Author** team permission. Source records need the **Viewer** team permission, unmasked values the **Editor** team permission, and ticket links the **Manager** role on top of **Author**. Admins are exempt from team permissions. See [Anomaly Permissions](https://userguide.qualytics.io/anomalies/deep-dive/permissions/) for the full matrix.

---

## Error Responses

| Status Code | Description |
| --- | --- |
| `400 Bad Request` | The change is not allowed in the anomaly's current status, such as moving an archived anomaly straight back to `Active`, or giving an archived anomaly a different archived status. |
| `401 Unauthorized` | Missing or invalid API token. |
| `403 Forbidden` | The user does not have the required role, or lacks the required team permission on the anomaly's datastore. |
| `404 Not Found` | No anomaly, ticket link, or integration exists with the specified ID. |
| `409 Conflict` | The anomaly has no source records, the ticket is already linked to the anomaly, or the anomaly is archived and its comments cannot change. |
| `422 Unprocessable Entity` | Invalid request body or parameters, such as an unknown status or an invalid `filters` value. |

> **Example: Error response examples**
>
> **404 Not Found**:
>
> ```json
> { "detail": "Anomaly id: 999 not found" }
> ```
>
> **400 Bad Request** (restoring an archived anomaly to `Active`):
>
> ```json
> { "detail": "Cannot change archived anomaly back to active" }
> ```
>
> **409 Conflict** (reading the source records of an anomaly that has none):
>
> ```json
> { "detail": "Anomaly id: 12345 does not have a source record" }
> ```

---

## Anomaly Fields

Getting a single anomaly returns the full set of fields below. Items in the list response carry the same fields except `operation` and `fingerprint_source`, so get the anomaly by ID when you need them.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | `int` | Numeric primary identifier. |
| `uuid` | `string (UUID)` | Stable, globally unique identifier. |
| `type` | `"record" \| "shape"` | Anomaly category. See [Record Anomalies](https://userguide.qualytics.io/anomalies/deep-dive/concepts/record-anomalies/) and [Shape Anomalies](https://userguide.qualytics.io/anomalies/deep-dive/concepts/shape-anomalies/). |
| `status` | `string` | One of `Active`, `Acknowledged`, `Resolved`, `Duplicate`, `Invalid`, `Discarded`. See [Status](https://userguide.qualytics.io/anomalies/deep-dive/triage/status/). |
| `impact_level` | `"low" \| "medium" \| "high" \| "critical" \| null` | The anomaly's impact: the highest impact among its failed checks. See [Impact](https://userguide.qualytics.io/anomalies/deep-dive/triage/impact/). |
| `impact_reason` | `string \| null` | Why the anomaly has its impact, in one line (for example, `High: across this scan, some of the checked records failed, on a High-importance check.`). |
| `weight` | `float` | **Deprecated**: use `impact_level`. Still returned for existing integrations, and no longer used for sorting or Flow filters. |
| `anomalous_records_count` | `int \| null` | Number of records associated with the anomaly. Set to 1 for record anomalies. For shape anomalies it holds the number of affected rows when Qualytics can count them, otherwise `null`. |
| `description` | `string` | User-editable business description. The value is Markdown and has no length cap. |
| `created` | `string (ISO 8601)` | When the anomaly was detected. |
| `fingerprint` | `string` | Used to deduplicate recurring anomalies. Returned as a quoted string so that large values survive JavaScript clients. See [Fingerprints](https://userguide.qualytics.io/anomalies/deep-dive/triage/fingerprints/). |
| `fingerprint_source` | `AnomalyStub \| null` | The earliest other anomaly that shares this anomaly's fingerprint, with its `id`, `uuid`, `type`, `status`, and `fingerprint`. `null` when the anomaly has no fingerprint or no other anomaly shares it. Single-anomaly response only. |
| `global_tags` | `Tag[]` | Tags applied to the anomaly. Each tag carries `type`, `name`, `color`, `description`, `category`, and `weight_modifier`. Tags have no `id`, so identify them by `name`. |
| `assignees` | `UserStub[]` | Users assigned to the anomaly. See [Assignees](https://userguide.qualytics.io/anomalies/deep-dive/triage/assignees/). |
| `failed_checks` | `FailedCheck[]` | Checks that produced the anomaly. Each entry nests the check under `quality_check` (with its `id` and `rule_type`), next to the `message`, the `suggested_field`, `suggested_value`, and `suggested_score` fields, and `parameters`. Each entry also carries its levels: `failure_level` (how much failed, before the check's importance adjusts it), `quality_check_importance_level` (the check's importance that adjusted it), `impact_level` (the result), and `impact_reason`. |
| `datastore`, `container`, `partition` | refs | Source location of the anomaly. `partition` carries the partition's name and location. |
| `operation` | ref | The scan that produced the anomaly, with its type, start and end times, result, and who triggered it. Single-anomaly response only. |

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "id": 12345,
>   "uuid": "8e8b9f8b-1234-4abc-9def-012345678901",
>   "type": "record",
>   "status": "Active",
>   "impact_level": "high",
>   "impact_reason": "High: across this scan, some of the checked records failed, on a High-importance check.",
>   "weight": 54.0,
>   "anomalous_records_count": 1,
>   "description": "Customer ID is missing for new orders",
>   "created": "2026-05-15T17:22:08Z",
>   "fingerprint": "9182736455",
>   "fingerprint_source": null,
>   "global_tags": [
>     { "type": "global", "name": "High", "color": "#E53935", "description": null, "category": null, "weight_modifier": 10 }
>   ],
>   "assignees": [
>     { "id": 42, "name": "Alice Lee", "email": "alice@example.com" }
>   ],
>   "failed_checks": [
>     {
>       "quality_check": { "id": 778, "rule_type": "notNull" },
>       "message": "...",
>       "suggested_field": null,
>       "suggested_value": null,
>       "suggested_score": null,
>       "parameters": null,
>       "failure_level": "medium",
>       "quality_check_importance_level": "high",
>       "impact_level": "high",
>       "impact_reason": "High: across this scan, some of the checked records failed, on a High-importance check."
>     }
>   ]
> }
> ```

---

## List Anomalies

Returns a paginated list of anomalies, scoped to the datastores the caller can see. Without a sort parameter, anomalies are returned by ID, oldest first. With `sort_impact_level`, anomalies at the same Impact are sorted by `weight` in the same direction, then by ID. Ties in the other sorts are broken by ID. To match the default order of the anomaly lists in the app, pass `sort_impact_level=desc` (highest [Impact](https://userguide.qualytics.io/anomalies/deep-dive/triage/impact/) first).

**Endpoint**: `GET /anomalies`

**Permission**: Member role. Only the anomalies of datastores the caller has access to are returned.

| Query Parameter | Type | Description |
| --- | --- | --- |
| `id` | `list of integers or UUIDs` | Return the anomalies with these IDs or UUIDs. |
| `search` | `string` | Match the anomaly ID or UUID, or a piece of a failed check's message, ignoring case. |
| `message` | `string` | Only anomalies with a failed check whose message contains this text, ignoring case. |
| `status` | `list of strings` | Only anomalies with these statuses: `Active`, `Acknowledged`, `Resolved`, `Duplicate`, `Invalid`, `Discarded`. |
| `archived` | `include` / `only` | Include archived anomalies, or return only archived anomalies. |
| `anomaly_type` | `record` / `shape` | Only anomalies of this type. |
| `datastore`, `container` | `list of integers` | Only anomalies of these datastores or tables and files. |
| `field`, `field_profile` | `list of strings` / `list of integers` | Only anomalies of these field names or field profile IDs. |
| `quality_check`, `template`, `rule_type` | `list of integers` / `list of integers` / `list of strings` | Only anomalies produced by these checks, by checks built from these templates, or by checks of these rule types. |
| `operation`, `container_scan`, `partition_scan` | `list of integers` | Only anomalies found by these Scan operations, container scans, or partition scans. |
| `resolved_by_operation`, `resolved_by_container_scan` | `list of integers` | Only anomalies auto-resolved by these Scan operations or container scans. |
| `tag` | `list of strings` | Only anomalies with any of these tag names. |
| `assignee` | `list of integers` | Only anomalies assigned to any of these user IDs. |
| `impact_level` | `list of strings` | Only anomalies at these [impact levels](https://userguide.qualytics.io/anomalies/deep-dive/triage/impact/): `low`, `medium`, `high`, or `critical`. Pass it once per level, for example `impact_level=critical&impact_level=high`. Inside `filters`, it takes `in` and `not_in`. Any other value returns a `422`. |
| `related_to_id` | `integer` | The ID of the earliest anomaly with a fingerprint. Returns the later anomalies that share that fingerprint. Passing the ID of a later anomaly returns no results. |
| `segment` | `integer` | Only anomalies of this segment. |
| `source_enriched` | `boolean` | Only anomalies with (`true`) or without (`false`) source records. |
| `timeframe`, `created_date`, `start_date`, `end_date` | varies | Only anomalies created in this period. |
| `filters` | `string` (JSON) | Narrows the list by one or more filters, written as a JSON object. See [Filtering Lists](https://userguide.qualytics.io/anomalies/api/#filtering-lists). |
| `sort_id`, `sort_created`, `sort_impact_level`, `sort_anomalous_records_count` | `asc` / `desc` | Sort by ID, creation date, Impact, or anomalous record count. `sort_weight` is a deprecated alias of `sort_impact_level`. |

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies?status=Active&assignee=42&tag=High&sort_impact_level=desc" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> **Response** (abbreviated). Each item carries the anomaly's fields, except `operation` and `fingerprint_source`:
>
> ```json
> {
>   "items": [
>     {
>       "id": 12345,
>       "type": "record",
>       "status": "Active",
>       "weight": 54.0,
>       "anomalous_records_count": 1,
>       "global_tags": [{ "name": "High", "weight_modifier": 10 }],
>       "assignees": [{ "id": 42, "name": "Alice Lee" }]
>     }
>   ],
>   "total": 1,
>   "page": 1,
>   "size": 50,
>   "pages": 1
> }
> ```

### Filtering Lists

`GET /api/anomalies` accepts a `filters` parameter: a JSON object with one entry per filter, saying how that filter's values should match.

```bash
curl -G "https://your-instance.qualytics.io/api/anomalies" \
  --data-urlencode 'filters={"tag":{"not_in":["PII"]},"assignee":{"empty":true}}' \
  -H "Authorization: Bearer YOUR_TOKEN"
```

This returns the anomalies that aren't tagged `PII` and have no one assigned.

| Key | Returns |
| --- | --- |
| `in` | The items that match at least one of the values. |
| `not_in` | The items that match none of the values. |
| `all_of` | The items that match every value, on filters an item can have several of, such as tags. |
| `empty` | The items with no value (`true`) or at least one (`false`), on filters an item can lack. |

- Different filters combine with AND, and so do keys on the same filter.
- `empty` can't be combined with other keys on the same filter.
- A filter sent both as its own parameter and inside `filters` returns a `422`. Put all of that filter's conditions in `filters`.
- `in`, `not_in` and `all_of` each take between 1 and 20 values.
- A filter or key the endpoint doesn't accept returns a `422` that names it. The [API docs](https://userguide.qualytics.io/api/docs) list what each endpoint accepts.

---

## Get an Anomaly

Returns the details of one anomaly, including the Scan that found it and the earliest anomaly that shares its fingerprint.

**Endpoint**: `GET /anomalies/{id}`

**Permission**: Member role, with the **Reporter** team permission on the anomaly's datastore

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `integer` or `UUID` | The anomaly ID or UUID. |
| `include_deleted` | `boolean` | Also return the anomaly when it is archived. Defaults to `false`. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345?include_deleted=true" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> The response carries the fields listed in [Anomaly Fields](https://userguide.qualytics.io/anomalies/api/#anomaly-fields).

---

## Get an Anomaly's Failed Checks

Returns the checks that failed in the anomaly, each with its message, as they were when the anomaly was created. Archived anomalies are included.

**Endpoint**: `GET /anomalies/{id}/failed-checks`

**Permission**: Member role, with the **Reporter** team permission on the anomaly's datastore

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `integer` or `UUID` | The anomaly ID or UUID. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345/failed-checks" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

---

## Update an Anomaly

Changes the status, description, tags, or assignees of an anomaly. Only the fields you send are changed.

**Endpoint**: `PUT /anomalies/{id}`

**Permission**: Member role, with the **Author** team permission on the anomaly's datastore

| Body Field | Type | Description |
| --- | --- | --- |
| `status` | `Active` / `Acknowledged` | Acknowledge or reactivate an open anomaly. Sending `Acknowledged` for an archived anomaly restores it. An archived anomaly cannot go straight back to `Active`. To archive an anomaly, use [Archive or Delete an Anomaly](https://userguide.qualytics.io/anomalies/api/#archive-or-delete-an-anomaly). |
| `description` | `string` | The anomaly's description, in Markdown, with no length limit. |
| `tags` | `list of strings` | Tag names. Replaces the current tags, and `[]` removes them all. Tags on the anomaly do not change its `impact_level`. Only the tags on its failed checks do, through the checks' Importance. |
| `assignee_ids` | `list of integers` | User IDs. Replaces the current assignees, and `[]` unassigns everyone. To add a user, read `assignees` first and send the full list. Each user needs at least the **Viewer** team permission on the datastore. |

Assigning from a check's default assignee happens only when the anomaly is created, so it cannot be triggered through this endpoint. See [Anomaly Assignees](https://userguide.qualytics.io/anomalies/deep-dive/triage/assignees/).

> **Example: Example request**
>
> ```bash
> 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]
>   }'
> ```
>
> The response carries the updated anomaly, with the fields listed in [Anomaly Fields](https://userguide.qualytics.io/anomalies/api/#anomaly-fields).

---

## Update Anomalies in Bulk

Applies changes to many anomalies in one call. Each entry names an anomaly and the fields to change on it.

**Endpoint**: `PATCH /anomalies`

**Permission**: Member role, with the **Author** team permission on the datastore of every anomaly

The body is a list of entries. Each entry accepts the body fields of [Update an Anomaly](https://userguide.qualytics.io/anomalies/api/#update-an-anomaly), plus:

| Body Field | Type | Description |
| --- | --- | --- |
| `id` | `integer` or `UUID` | The anomaly to update. |
| `tags_mode` | `replace` / `add` | How `tags` is applied. `replace` (the default) makes `tags` the anomaly's only tags. `add` keeps the current tags and adds the given ones, without repeating a tag. |

> **Example: Example request**
>
> ```bash
> curl -X PATCH "https://your-instance.qualytics.io/api/anomalies" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '[
>     { "id": 12345, "status": "Acknowledged" },
>     { "id": 12346, "assignee_ids": [42] },
>     { "id": 12347, "tags": [] },
>     { "id": 12348, "tags": ["Critical"], "tags_mode": "add" }
>   ]'
> ```
>
> The response carries the list of updated anomalies.

---

## Archive or Delete an Anomaly

Archives an anomaly with a resolution, or deletes it permanently. Returns `204 No Content`.

**Endpoint**: `DELETE /anomalies/{id}`

**Permission**: Member role, with the **Author** team permission on the anomaly's datastore

| Query Parameter | Type | Description |
| --- | --- | --- |
| `archive` | `boolean` | `true` (the default) archives the anomaly. `false` deletes it permanently, whatever its status, and cannot be undone. |
| `status` | `Resolved` / `Duplicate` / `Invalid` / `Discarded` | The resolution to archive with. When it is left out, an `Active` anomaly is archived as `Resolved`. An archived anomaly cannot be given a different resolution. |

| Body Field | Type | Description |
| --- | --- | --- |
| `status` | `string` | The resolution, as an alternative to the query parameter. |
| `comment` | `string` | A comment added to the anomaly's Timeline when it is archived. |
| `mentioned_user_ids` | `list of integers` | Users mentioned in the comment, who receive a **Mention** notification. |

> **Example: Example requests**
>
> **Archive with a resolution and a comment**:
>
> ```bash
> curl -X DELETE "https://your-instance.qualytics.io/api/anomalies/12345?archive=true&status=Resolved" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "comment": "Resolved after backfill, see @[Alice Lee](42)",
>     "mentioned_user_ids": [42]
>   }'
> ```
>
> **Delete permanently**:
>
> ```bash
> curl -X DELETE "https://your-instance.qualytics.io/api/anomalies/12345?archive=false" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

---

## Archive or Delete Anomalies in Bulk

Archives or deletes many anomalies in one call. Returns `204 No Content`.

**Endpoint**: `DELETE /anomalies`

**Permission**: Member role, with the **Author** team permission on the datastore of every anomaly

The body is a list of entries. Each entry takes the anomaly's `id` and the same fields as [Archive or Delete an Anomaly](https://userguide.qualytics.io/anomalies/api/#archive-or-delete-an-anomaly): `archive`, `status`, `comment`, and `mentioned_user_ids`.

> **Example: Example request**
>
> ```bash
> 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 }
>   ]'
> ```

---

## Get an Anomaly's Change History

Returns the paginated change log behind the anomaly's [Timeline](https://userguide.qualytics.io/anomalies/deep-dive/anomaly-details/timeline/): status changes, description edits, tag and assignee changes, and ticket links and unlinks, each with the user who made it and when.

**Endpoint**: `GET /anomalies/{id}/history`

**Permission**: Member role, with the **Reporter** team permission on the anomaly's datastore

| Query Parameter | Type | Description |
| --- | --- | --- |
| `include_deleted` | `boolean` | Also return the history of an archived anomaly. Defaults to `false`. |
| `page` | `integer` | The page to return, starting at 1. |
| `size` | `integer` | Entries per page. Defaults to 50, up to 100. |

Each field in `changeset` is a `[before, after]` pair. `transaction` holds the change's `id`, `issued_at` time, and `user`, and `operation` is `insert`, `update`, or `delete`. Increase `page` until it reaches `pages` to read the whole log.

> **Example: Example response**
>
> **Response** (abbreviated):
>
> ```json
> {
>   "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
> }
> ```

---

## List an Anomaly's Comments

Returns the comments on an anomaly, newest first, as a list. Replies are returned together with the comment that starts their thread. A comment deleted while it still had replies is returned with an empty `message`.

**Endpoint**: `GET /anomalies/{id}/comments`

**Permission**: Member role, with the **Reporter** team permission on the anomaly's datastore

| Query Parameter | Type | Description |
| --- | --- | --- |
| `start_date` | `datetime` (ISO 8601) | Inclusive lower bound on `created`, for comments not attached to a Timeline entry. |
| `end_date` | `datetime` (ISO 8601) | Exclusive upper bound on `created`, so back-to-back ranges do not overlap. |
| `transaction_id` | `list of integers` | Return the comments attached to these Timeline entries, whatever the dates. |

Besides its `message` and `created` time, each comment carries:

| Field | Type | Description |
| --- | --- | --- |
| `entity_type` | `string` | The kind of asset the comment belongs to: `anomaly`, `container`, `quality_check`, `partition`, `datastore`, or `field`. |
| `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. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345/comments?start_date=2026-05-01T00:00:00Z" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

> **Warning: Response change**
>
> Comments identify their asset with `entity_type` and `entity_id`. The previous `anomaly_id` and `partition_id` fields are no longer returned. Requests are unaffected: the previous payload shapes are still accepted when creating comments.

---

## Post a Comment

Adds a comment to an anomaly, as a new comment, a reply, or a comment on one Timeline entry.

**Endpoint**: `POST /comments`

**Permission**: Member role, with the **Viewer** team permission on the anomaly's datastore

| Body Field | Type | Description |
| --- | --- | --- |
| `entity_type` | `string` | `anomaly` for an anomaly comment. The same endpoint also takes `container`, `quality_check` (for checks and check templates), `partition`, `datastore`, and `field`. |
| `entity_id` | `integer` | The anomaly ID. |
| `message` | `string` | The comment text. Use `@[Full Name](user_id)` to mention a user. |
| `transaction_id` | `integer` | Optional. Attaches the comment to this Timeline entry of the same anomaly. |
| `parent_id` | `integer` | Optional. Posts the comment as a reply to this comment of the same anomaly. |
| `mentioned_user_ids` | `list of integers` | Optional. Users to notify. They must be active users. |

> **Example: Example request**
>
> ```bash
> 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]
>   }'
> ```
>
> The response carries the comment's message, author, and mentions. To read `entity_type`, `entity_id`, `transaction_id`, `parent_id`, or `deleted_at`, get the comment again with one of the `GET` endpoints.

A rejected comment returns a status code that says 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. |

---

## Update a Comment

Changes the text of a comment. Only its author can update it.

**Endpoint**: `PUT /comments/{id}`

**Permission**: Member role, as the comment's author

| Body Field | Type | Description |
| --- | --- | --- |
| `message` | `string` | The new comment text. |
| `mentioned_user_ids` | `list of integers` | Users mentioned in the comment. Only users newly added are notified. |

> **Example: Example request**
>
> ```bash
> curl -X PUT "https://your-instance.qualytics.io/api/comments/555" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{ "message": "Confirmed with the source team.", "mentioned_user_ids": [] }'
> ```

---

## Delete a Comment

Deletes a comment. A comment that still has replies is kept as a placeholder, so the replies stay in the thread. Deleting the last reply also removes the placeholder.

**Endpoint**: `DELETE /comments/{id}`

**Permission**: Member role, as the comment's author, or the **Admin** role

> **Example: Example request**
>
> ```bash
> curl -X DELETE "https://your-instance.qualytics.io/api/comments/555" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

---

## Get Source Records

Returns the rows behind an anomaly, as stored by the Scan in the enrichment datastore.

**Endpoint**: `GET /anomalies/{id}/source-record`

**Permission**: Member role, with the **Viewer** team permission on the anomaly's datastore. `include_masked=true` also needs the **Editor** team permission.

| Query Parameter | Type | Description |
| --- | --- | --- |
| `limit` | `integer` | The most rows to return. Defaults to 10. |
| `include_masked` | `boolean` | Return the real values of masked fields. Each call is recorded in the Masking Audit Log with the masked fields. Defaults to `false`. |

An anomaly without source records returns `409 Conflict`. See [Source Records](https://userguide.qualytics.io/anomalies/deep-dive/anomaly-details/source-record/).

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345/source-record?limit=5" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> **Response**: `source_record` holds the rows as a JSON string, so parse it to read them.
>
> ```json
> {
>   "created": "2026-05-15T17:22:08Z",
>   "source_record": "[{\"emp_id\": 102, \"name\": \"Bob Johnson\", \"salary\": 30000}]"
> }
> ```

---

## Download Source Records

Downloads the rows behind an anomaly as a CSV file named `source_records_<id>.csv`.

**Endpoint**: `GET /anomalies/{id}/source-record/download`

**Permission**: Member role, with the **Viewer** team permission on the anomaly's datastore. `include_masked=true` also needs the **Editor** team permission.

| Query Parameter | Type | Description |
| --- | --- | --- |
| `limit` | `integer` | The most rows to return. With no limit, the file holds every row the Scan kept. |
| `include_masked` | `boolean` | Return the real values of masked fields, recorded in the Masking Audit Log. Defaults to `false`. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345/source-record/download" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -o source_records_12345.csv
> ```

> **Info: Reading source records from the enrichment datastore**
>
> Real values of masked fields are only returned by endpoints that accept `include_masked`: these two, the container scan equivalents (see [Get source records of a container scan](https://userguide.qualytics.io/operations/scan/api/#get-source-records-of-a-container-scan)), and `GET /containers/{id}/source-records`. All of them check the **Editor** team permission and write to the Masking Audit Log. Reading the `_source_records` output with `GET /datastores/{id}/source-records` always returns `***MASKED***` in the `RECORD` column. See [Read Source Records](https://userguide.qualytics.io/source-datastore/enrichment-datastore/api/#read-source-records).

---

## Search Tickets

Searches the connected ticketing system for tickets whose key or summary matches, to find an existing ticket before linking it.

**Endpoint**: `GET /integrations/ticketing/tickets/search`

**Permission**: Manager role

| Query Parameter | Type | Description |
| --- | --- | --- |
| `query` | `string` | Text to match against the ticket key or summary. |
| `status` | `string` | Only tickets with this status. |
| `priority` | `string` | Only tickets with this priority. |
| `project_key` | `string` | Jira only. Only tickets of this project. Without it, every configured project is searched. |

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/integrations/ticketing/tickets/search?query=customer" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> **Response**:
>
> ```json
> {
>   "items": [
>     {
>       "ticket_id": "10002",
>       "ticket_number": "SCRUM-3",
>       "title": "Task 3",
>       "description": null,
>       "status": "To Do",
>       "priority": null,
>       "created_at": "2025-12-02T12:34:20",
>       "updated_at": "2025-12-02T12:34:21",
>       "assigned_to": null,
>       "url": "https://your-domain.atlassian.net/browse/SCRUM-3"
>     }
>   ],
>   "total": 1,
>   "page": 1,
>   "size": 50,
>   "pages": 1
> }
> ```

---

## Search Tickets One Page at a Time

Runs the same search a page at a time, with a token for the next page. This is what the **Link Ticket** dialog uses for its **Load more tickets** button. It does not count every match first, so it stays quick on large projects.

**Endpoint**: `GET /integrations/ticketing/tickets/search-page`

**Permission**: Manager role

| Query Parameter | Type | Description |
| --- | --- | --- |
| `query` | `string` | Optional. On Jira, matches a ticket key, the key or name of an allowed project, or words in the summary, description, or comments. On ServiceNow, matches words in the short description or description, not the incident number. Leave it empty to list the most recently updated tickets first. |
| `project_key` | `string` | Optional. Jira only. Limits the search to one of the allowed projects. Any other key returns `400`. Without it, every allowed project is searched. |
| `status` | `string` | Optional. Only tickets with this status. |
| `priority` | `string` | Optional. Only tickets with this priority. |
| `limit` | `integer` | The most tickets in the page, from 1 to 100. Defaults to 50. A value above 100 returns `422`. |
| `next_page_token` | `string` | The token from the previous page. Leave it out for the first page. |

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/integrations/ticketing/tickets/search-page?project_key=SCRUM&limit=2" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> **Response**: pass `next_page_token` back unchanged to read the next page. It is `null` on the last page.
>
> ```json
> {
>   "items": [
>     {
>       "ticket_id": "10431",
>       "ticket_number": "SCRUM-43",
>       "title": "Null customer IDs in orders",
>       "description": null,
>       "status": "To Do",
>       "priority": "Medium",
>       "created_at": "2026-10-20T14:02:11",
>       "updated_at": "2026-10-26T09:15:40",
>       "assigned_to": null,
>       "url": "https://your-domain.atlassian.net/browse/SCRUM-43"
>     },
>     {
>       "ticket_id": "10430",
>       "ticket_number": "SCRUM-42",
>       "title": "Late shipments feed",
>       "description": null,
>       "status": "In Progress",
>       "priority": "Medium",
>       "created_at": "2026-10-19T11:47:03",
>       "updated_at": "2026-10-25T16:30:12",
>       "assigned_to": null,
>       "url": "https://your-domain.atlassian.net/browse/SCRUM-42"
>     }
>   ],
>   "next_page_token": "CAEaAggD"
> }
> ```

---

## Get a Ticket

Returns one ticket from the connected ticketing system.

**Endpoint**: `GET /integrations/ticketing/tickets/{ticket_id}`

**Permission**: Manager role

| Parameter | Type | Description |
| --- | --- | --- |
| `ticket_id` | `string` | The ticket's ID or key, such as the `sys_id` in ServiceNow or `PROJ-123` in Jira. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/integrations/ticketing/tickets/SCRUM-3" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

---

## Create a Ticket for an Anomaly

Creates a ticket in the connected ticketing system and links it to the anomaly in one call. The anomaly's details are added to the description, with a link back to Qualytics. On Jira, the issue is created with the type set for that project on the integration, `Task` unless another was picked. A type Jira refuses returns `400`, naming the type and the project.

**Endpoint**: `POST /integrations/ticketing/anomalies/{anomaly_id}/tickets`

**Permission**: Manager role, with the **Author** team permission on the anomaly's datastore

| Body Field | Type | Description |
| --- | --- | --- |
| `title` | `string` | The ticket's summary. |
| `description` | `string` | Optional. The ticket's description. The anomaly's details are added to it. |
| `status` | `string` | Optional. The ticket's initial status. |
| `priority` | `string` | Optional. The ticket's priority. |
| `urgency`, `impact` | `string` | Optional. ServiceNow only. |
| `category`, `subcategory` | `string` | Optional. ServiceNow only. |
| `assigned_to`, `assigned_group` | `string` | Optional. The user and the group responsible for the ticket. |
| `project_key` | `string` | Optional. Jira only. The project to create the ticket in. Without it, the integration's default project is used. |
| `additional_fields` | `object` | Optional. Other fields to send to the ticketing system. |

For the values each field accepts, see [Create a Jira Ticket](https://userguide.qualytics.io/anomalies/how-tos/ticket/jira/create-ticket/#fields) and [Create a ServiceNow Ticket](https://userguide.qualytics.io/anomalies/how-tos/ticket/servicenow/create-ticket/#fields).

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X POST "https://your-instance.qualytics.io/api/integrations/ticketing/anomalies/1/tickets" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "title": "Investigate CUSTOMER_NAME similarity anomaly",
>     "description": "Follow-up on an anomaly detected during the daily scan.",
>     "priority": "Medium"
>   }'
> ```
>
> **Response**:
>
> ```json
> {
>   "ticket": {
>     "ticket_id": "10428",
>     "ticket_number": "SCRUM-40",
>     "title": "Investigate CUSTOMER_NAME similarity anomaly",
>     "status": "Idea",
>     "priority": "Medium",
>     "category": "Task",
>     "created_at": "2026-01-23T15:22:05",
>     "url": "https://your-domain.atlassian.net/browse/SCRUM-40",
>     "metadata": { "project_key": "SCRUM", "issue_type": "Task" }
>   },
>   "anomaly_id": 1,
>   "integration_id": 2,
>   "message": "Ticket SCRUM-40 created and linked to anomaly 1"
> }
> ```

---

## List Ticket Links

Returns the tickets linked to an anomaly. Each call also asks Qualytics to refresh those tickets from the ticketing system in the background, so the next call returns their latest status.

**Endpoint**: `GET /anomalies/{anomaly_id}/ticket-links`

**Permission**: Member role, with the **Reporter** team permission on the anomaly's datastore

> **Example: Example request and response**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/anomalies/12345/ticket-links" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```
>
> **Response**:
>
> ```json
> [
>   {
>     "id": 678,
>     "anomaly_id": 12345,
>     "integration_id": 3,
>     "ticket_id": "abc123sys",
>     "ticket_number": "INC0010001",
>     "ticket_url": "https://example.service-now.com/incident.do?sys_id=abc123sys",
>     "status": "New",
>     "last_synced": "2026-05-15T18:02:11Z",
>     "ticket_metadata": null,
>     "post_updates": true,
>     "created": "2026-05-15T17:40:00Z"
>   }
> ]
> ```

---

## Create a Ticket Link

Links an existing ticket to an anomaly. Only the link is recorded in Qualytics: nothing is sent to the ticketing system.

**Endpoint**: `POST /anomalies/{anomaly_id}/ticket-links`

**Permission**: Manager role, with the **Author** team permission on the anomaly's datastore

| Body Field | Type | Description |
| --- | --- | --- |
| `integration_id` | `integer` | The ticketing integration that holds the ticket. |
| `ticket_id` | `string` | The ticket's ID in the ticketing system, such as the ServiceNow `sys_id`. |
| `ticket_number` | `string` | The ticket's number, such as `INC0010001`. |
| `ticket_url` | `string` | Optional. A direct link to the ticket. |
| `status` | `string` | The ticket's current status. |
| `ticket_metadata` | `object` | Optional. More data to keep with the link. |

Linking a ticket that is already linked to the anomaly returns `409 Conflict`.

> **Example: Example request**
>
> ```bash
> 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"
>   }'
> ```
>
> The response carries the new link, with the fields shown in [List Ticket Links](https://userguide.qualytics.io/anomalies/api/#list-ticket-links).

---

## Update a Ticket Link

Turns Qualytics writes on or off for one linked ticket, which is what the **Two-way** / **Read only** button on the ticket card does. The ticket's status, title, and comments are still read back either way.

**Endpoint**: `PATCH /anomalies/{anomaly_id}/ticket-links/{link_id}`

**Permission**: Manager role, with the **Author** team permission on the anomaly's datastore

| Body Field | Type | Description |
| --- | --- | --- |
| `post_updates` | `boolean` | Whether Qualytics posts comments and status notes to this ticket. |

> **Example: Example request**
>
> ```bash
> curl -X PATCH "https://your-instance.qualytics.io/api/anomalies/12345/ticket-links/678" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{ "post_updates": false }'
> ```

---

## Delete a Ticket Link

Removes the link between a ticket and an anomaly. The ticket stays in the ticketing system. Returns `204 No Content`.

**Endpoint**: `DELETE /anomalies/{anomaly_id}/ticket-links/{link_id}`

**Permission**: Manager role, with the **Author** team permission on the anomaly's datastore

> **Example: Example request**
>
> ```bash
> curl -X DELETE "https://your-instance.qualytics.io/api/anomalies/12345/ticket-links/678" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

*Last updated: November 3, 2026.*
