# Checks API

Source: https://userguide.qualytics.io/data-quality-checks/checks/api/

This page documents the endpoints that apply to every check regardless of its rule type: the change log of a check, its definition at a point in time, and the daily assertion history behind its **Insights** section. Create, read, update, and delete payloads are rule-specific and live on each rule type's API page (see [Getting Started with Checks](https://userguide.qualytics.io/data-quality-checks/checks/getting-started/)). The optional `impact_metrics` field of those payloads is described in [Impact Metrics in the API](https://userguide.qualytics.io/data-quality-checks/checks/deep-dive/impact-metrics/#impact-metrics-in-the-api).

All endpoints use the base URL of your Qualytics deployment (for example, `https://your-instance.qualytics.io/api`) and require a Bearer token. Every timestamp is returned in UTC as ISO 8601 with a trailing `Z`; see [Timestamps](https://userguide.qualytics.io/settings/access-tokens/personal-token/api/#timestamps).

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

> **Note: Permissions**
>
> All three endpoints require the **Member** role and the **Reporter** team permission on the check's datastore.

## History

Returns the paginated change log of a check: one entry per saved version, with the fields that changed, their before and after values, the user who made the change, and when. Use it to reconstruct every state a check has been in.

**Endpoint:** `GET /api/quality-checks/{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 the history of a deleted check. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks/778/history?page=1&size=50" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "items": [
>     {
>       "changeset": {
>         "filter": ["order_date >= '2026-01-01'", "order_date >= '2026-06-01'"],
>         "description": ["Orders must reference a customer", "Orders must reference an active customer"]
>       },
>       "transaction": {
>         "id": 9876,
>         "issued_at": "2026-06-02T13:40:11Z",
>         "user": { "id": 42, "name": "Alice Lee" }
>       },
>       "operation": "update"
>     },
>     {
>       "changeset": {
>         "fields": [null, ["customer_id"]],
>         "status": [null, "Active"]
>       },
>       "transaction": {
>         "id": 8120,
>         "issued_at": "2026-01-15T09:02:47Z",
>         "user": { "id": 42, "name": "Alice Lee" }
>       },
>       "operation": "insert"
>     }
>   ],
>   "total": 2,
>   "page": 1,
>   "size": 50,
>   "pages": 1
> }
> ```

Each field inside `items[].changeset` is a `[before, after]` pair. The `transaction` object carries the change `id`, the `issued_at` timestamp, and the `user` who saved it. The `operation` field reports whether the entry is an `insert`, `update`, or `delete`. To load successive windows, increment `page` until it reaches `pages`.

## Version at a Point in Time

Returns the full check definition as it stood at a given instant. Pass the `start_time` of a scan (from `GET /api/operations/{id}` or `GET /api/container-scans/{id}`) to retrieve the exact definition that scan evaluated, even if the check has been edited since.

**Endpoint:** `GET /api/quality-checks/{id}/version?version_at={UTC timestamp}`

**Query parameters:**

| Parameter | Type | Description |
| --- | --- | --- |
| `version_at` | `string` (ISO 8601) | The instant to resolve, interpreted as UTC (for example `2026-06-01T02:00:00Z`). When omitted, the current definition is returned. A value earlier than the check's creation returns the first saved version. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks/778/version?version_at=2026-05-15T02:00:00Z" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "id": 778,
>   "rule_type": "notNull",
>   "description": "Orders must reference a customer",
>   "filter": "order_date >= '2026-01-01'",
>   "coverage": 1.0,
>   "status": "Active",
>   "fields": [{ "id": 3101, "name": "customer_id" }],
>   "container": { "id": 235, "name": "orders" },
>   "datastore": { "id": 101, "name": "Sales" },
>   "created": "2026-01-15T09:02:47Z",
>   "last_asserted": "2026-06-02T02:00:04Z"
> }
> ```

The response has the same shape as `GET /api/quality-checks/{id}`, so any client that already reads a check can read a historical version. Deleted checks can be resolved as well.

The returned `importance_level` is the importance the check had at `version_at`, so a version requested before an importance change shows the earlier level.

> **Note: Execution state is not versioned**
>
> Only the check's definition is restored to the requested instant. Execution state such as `last_asserted`, `first_asserted`, and `active_anomaly_count`, along with `rule_type` (which never changes), always reflects the check as it is now. In the example above, `last_asserted` is later than `version_at` because a scan ran after the requested instant. Use `latest_container_scan_start_time` from [Insights](https://userguide.qualytics.io/data-quality-checks/checks/api/#insights) or `START_TIME` in `_scan_operations` for the timing of a specific evaluation.

## Insights

Returns the daily assertion history behind the **Insights** section of a check: for each day in the window, the result of the latest evaluation, the scan that produced it, and the asserted and anomalous record counts. This is the API equivalent of the pass/fail timeline in the UI and distinguishes checks that could not be evaluated from checks that passed.

**Endpoint:** `GET /api/quality-checks/{id}/insights`

**Query parameters:**

| Parameter | Type | Description |
| --- | --- | --- |
| `report_date` | `date` | Last day of the window, `YYYY-MM-DD`. Send it on every request. When omitted, the window can end on an earlier day instead of today. |
| `timeframe` | `"week" \| "month" \| "quarter" \| "year"` (default `week`) | Length of the window ending on `report_date`. |
| `offset` | `int` | Your timezone offset from UTC in minutes, so that days are bucketed in local time. Omit to bucket in UTC. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks/778/insights?report_date=2026-06-07&timeframe=week" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "generated_at": "2026-06-07T18:21:03Z",
>   "start_date": "2026-06-01",
>   "end_date": "2026-06-07",
>   "timeframe": "week",
>   "daily_assertion_measured": [
>     {
>       "date": "2026-06-01",
>       "latest_scan_id": 12345,
>       "latest_container_scan_id": 456,
>       "latest_container_scan_start_time": "2026-06-01T02:00:00Z",
>       "latest_partition_scan_id": 9001,
>       "latest_assertion_result": "passed",
>       "latest_asserted_records": 120000,
>       "latest_anomalous_records": 0,
>       "total_scans": 1,
>       "total_asserted_records": 120000,
>       "total_anomalous_records": 0
>     },
>     {
>       "date": "2026-06-02",
>       "latest_scan_id": 12360,
>       "latest_container_scan_id": 471,
>       "latest_container_scan_start_time": "2026-06-02T02:00:00Z",
>       "latest_partition_scan_id": 9017,
>       "latest_assertion_result": "failed",
>       "latest_asserted_records": 121500,
>       "latest_anomalous_records": 37,
>       "total_scans": 2,
>       "total_asserted_records": 243000,
>       "total_anomalous_records": 37
>     }
>   ]
> }
> ```

**Daily metric fields:**

| Field | Type | Description |
| --- | --- | --- |
| `date` | `date` | The day the metrics belong to. |
| `latest_scan_id` | `int \| null` | The scan operation that last evaluated the check that day. Use it with `GET /api/operations/{id}`. |
| `latest_container_scan_id` | `int \| null` | The container scan of that evaluation. |
| `latest_container_scan_start_time` | `string \| null` | When that container scan started, in UTC. |
| `latest_partition_scan_id` | `int \| null` | The partition scan of that evaluation. |
| `latest_assertion_result` | `"passed" \| "failed" \| "unasserted" \| null` | Result of the latest evaluation. `unasserted` means the scan could not evaluate the check (for example, a filter with invalid syntax or a missing column). `null` means no scan evaluated the check that day. |
| `latest_asserted_records` | `int \| null` | Records the latest evaluation asserted. |
| `latest_anomalous_records` | `int \| null` | Records the latest evaluation flagged as anomalous. |
| `total_scans` | `int \| null` | Number of evaluations that day. |
| `total_asserted_records` | `int \| null` | Records asserted across all evaluations that day. |
| `total_anomalous_records` | `int \| null` | Records flagged across all evaluations that day. |

## Timestamp fields

| Field | Notes |
| --- | --- |
| `last_asserted` | Returned on every check payload. It is the UTC start time of the partition scan that last evaluated the check with a `passed` or `failed` result; unasserted evaluations do not move it. `null` for checks that have never been evaluated. Not versioned: the `/version` endpoint returns its current value. |
| `created` | When the check was created, in UTC. |
| `last_updated` | When the check was last updated, in UTC: the later of its last edit and its last importance change. A check whose importance changed but that nobody edited still gets a date. `null` when the check has neither. |

## Ownership

Ownership is exposed on the standard quality check endpoints, with no dedicated route, through an `owner_id` field on create and update and an `owner` object on reads. For how ownership behaves, see [Check Ownership](https://userguide.qualytics.io/data-quality-checks/checks/deep-dive/ownership/).

### The `owner_id` field

Every quality check carries a single owner. The API exposes it as `owner_id` on writes and as a nested `owner` object on reads.

| Field | Type | Description |
| --- | --- | --- |
| `owner_id` | `int` | The user ID of the check owner. On `POST`, defaults to the caller (or to the `Qualytics` user on [AI Managed (inferred) checks](https://userguide.qualytics.io/data-quality-checks/checks/deep-dive/ownership/#defaults-at-creation-time)). On `PUT`/`PATCH`, omit (or send `null`) to leave the owner unchanged. The target user must have at least the **Drafter** team permission on the check's datastore. |

> **Example: Example owner field on a read response (abbreviated)**
>
> ```json
> {
>   "id": 101,
>   "rule_type": "unique",
>   "inferred": false,
>   "owner": {
>     "id": 42,
>     "name": "Alice Lee",
>     "email": "alice@example.com"
>   }
> }
> ```

### Create

#### Create a check with an explicit owner

**Endpoint:** `POST /api/quality-checks`

**Permission:** Member role + **Author** team permission on the container's datastore, or **Drafter** to create the check as a Draft. The target `owner_id` must have **Drafter** on the same datastore.

Include `owner_id` in the create payload to set an explicit owner. Omit the field to let the platform default to the caller.

> **Example: Example request**
>
> ```bash
> curl -X POST "https://your-instance.qualytics.io/api/quality-checks" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "description": "Ensure C_NAME is unique in the CUSTOMER table",
>     "rule": "unique",
>     "fields": ["C_NAME"],
>     "container_id": 145,
>     "owner_id": 42
>   }'
> ```

### Update

#### Transfer ownership on a single check

**Endpoint:** `PUT /api/quality-checks/{id}`

**Permission:** Member role + **Author** team permission on the check's datastore, or **Drafter** when the check is a Draft and the request does not change its status. The target `owner_id` must have **Drafter** on the same datastore.

Send `owner_id` set to the new owner's user ID. The previous owner is recorded in the check's history.

A `PUT` replaces the check's assertion, so send the current `description` and `properties` from `GET /api/quality-checks/{id}`, an array of each returned field's `name` as `fields`, and the new `owner_id`. For an owner-only change, the [bulk update](https://userguide.qualytics.io/data-quality-checks/checks/api/#transfer-ownership-across-many-checks) is simpler because it needs only `id` and `owner_id`.

> **Example: Example request**
>
> ```bash
> curl -X PUT "https://your-instance.qualytics.io/api/quality-checks/101" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "description": "Ensure C_NAME is unique in the CUSTOMER table",
>     "fields": ["C_NAME"],
>     "properties": null,
>     "owner_id": 42
>   }'
> ```
>
> This Unique check has no properties, so `properties` is `null`, as the `GET` returns it. For a check that has properties, send the `properties` object exactly as the `GET` returns it. Leaving `properties` out clears them.

> **Warning: AI Managed conversion still applies**
>
> For AI Managed (inferred) checks owned by Qualytics, changing the value of any *assertion property* (`properties`, `coverage`, `filter`, or `fields`) in the same payload also transfers ownership to the editor and flips `inferred` from `true` to `false`. To transfer ownership of an AI Managed check and keep `inferred` as `true`, use the [bulk update](https://userguide.qualytics.io/data-quality-checks/checks/api/#transfer-ownership-across-many-checks) with only `id` and `owner_id`.

#### Transfer ownership across many checks

**Endpoint:** `PATCH /api/quality-checks`

**Permission:** Member role + **Author** team permission on the datastore of every check in the request (**Drafter** is enough for checks that are Drafts and stay Drafts). The target `owner_id` must have **Drafter** on each of those datastores.

Send a list of `{ id, owner_id }` entries; the same `owner_id` can be reused across entries to apply one owner to many checks. The same endpoint also takes the other bulk fields, such as `tags` and `additional_metadata`, with `tags_mode` and `additional_metadata_mode` set to `replace` (the default) or `add`; `owner_id` always replaces the current owner.

> **Note: Bulk is all-or-nothing**
>
> If any entry references a check the caller cannot update (404 not found, missing the required team permission on the datastore, or the target `owner_id` lacks **Drafter**), the entire request is rejected and no checks are updated. Retry with the offending entry removed.

> **Example: Apply the same owner to many checks**
>
> ```bash
> curl -X PATCH "https://your-instance.qualytics.io/api/quality-checks" \
>   -H "Authorization: Bearer YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '[
>     { "id": 101, "owner_id": 42 },
>     { "id": 102, "owner_id": 42 },
>     { "id": 103, "owner_id": 42 }
>   ]'
> ```

### Filter

#### Filter checks by a specific owner

**Endpoint:** `GET /api/quality-checks`

**Permission:** Member role. Results are scoped to datastores the caller can see.

Use the `owner_id` query parameter to narrow the list to checks owned by a specific user. Combine with the other list filters (status, tags, rule type, and so on) to scope further.

> **Example: List checks owned by user 42**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks?owner_id=42" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

#### List checks owned by the current user

**Endpoint:** `GET /api/quality-checks`

**Permission:** Member role.

Pass `owner_id` with a user's ID to list the checks that user owns. The **Owned** subtab sends your own user ID, which `GET /api/users/me` returns as `id`, together with the datastore and the status of the tab you are on.

> **Example: Equivalent of the Owned subtab**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks?owner_id=42&datastore=7&status=Active" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

### Error responses

| Status | Description |
| --- | --- |
| `400 Bad Request` | A business-logic conflict prevents the operation (for example, attempting to change properties while the check status is invalid or discarded). |
| `401 Unauthorized` | Missing or invalid API token. |
| `403 Forbidden` | The caller lacks the required team permission on the check's datastore (**Author**, or **Drafter** for a Draft check), or the target `owner_id` lacks **Drafter** on the same datastore. |
| `404 Not Found` | The check ID or `owner_id` user does not exist. |
| `422 Unprocessable Entity` | The payload fails schema validation (for example, a required field such as `description` is missing on `PUT`, `owner_id` is not an integer, or `properties` contains an unrecognized key for the rule type). |

## AI Managed

AI Managed checks use the same endpoints as every other check. For how they behave, see [AI Managed Checks](https://userguide.qualytics.io/data-quality-checks/checks/deep-dive/ai-managed-checks/).

### Telling AI Managed and Authored Checks Apart

Every check returned by the API includes an `inferred` boolean. When `true`, Qualytics AI manages the check; when `false`, the check is Authored and Profile runs leave it alone.

> **Note: Coverage encoding**
>
> The API uses fractional values for `coverage` (`0.0`–`1.0`); the UI displays the same value as a percentage (`0%`–`100%`).

> **Example: Example: filter AI Managed checks**
>
> **Request**:
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/quality-checks?inferred=true" \
>   -H "Authorization: Bearer YOUR_API_TOKEN"
> ```
>
> **Response** (abbreviated):
>
> ```json
> {
>   "items": [
>     {
>       "id": 101,
>       "rule_type": "between",
>       "inferred": true,
>       "coverage": 1.0,
>       "filter": null,
>       "fields": [{ "id": 5001, "name": "price" }],
>       "properties": { "min": 0, "inclusive_min": true, "max": 10000, "inclusive_max": true }
>     }
>   ]
> }
> ```

### Editing an AI Managed Check

To edit an AI Managed check, send a `PUT /api/quality-checks/{id}` request with the check's whole definition. `description` is always required. `fields` is required unless the request keeps the check in Draft, and `properties` is required for rule types that have required properties (such as Between or Expected Values), sent in full because it replaces the stored one. Resend their current values along with the change. Whether the check stays AI Managed depends on which values the request changes, not on which fields it includes.

**Permission**: Drafter or above while the check stays in Draft. Author or above on an Active check, or for any status change other than restoring an archived check to Draft.

#### Edits that convert the check to Authored

Changing the value of any of the following fields flips `inferred` from `true` to `false` on save. Resending a field with its current value does not convert the check.

| Field | What it controls |
| --- | --- |
| `properties` | Rule-specific settings (thresholds, accepted values, regex pattern, and so on). |
| `coverage` | The percentage of records that must satisfy the rule. |
| `filter` | The SQL `WHERE` clause that scopes the rows the check evaluates. |
| `fields` | The list of fields the check applies to. |

> **Example: Convert by lowering coverage**
>
> ```bash
> curl -X PUT "https://your-instance.qualytics.io/api/quality-checks/101" \
>   -H "Authorization: Bearer YOUR_API_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "description": "price stays between 0 and 10000",
>     "fields": ["price"],
>     "properties": { "min": 0, "inclusive_min": true, "max": 10000, "inclusive_max": true },
>     "coverage": 0.995
>   }'
> ```
>
> **Response** (abbreviated): `inferred` is now `false` and the response reflects the new coverage. Subsequent Profile runs will not modify or delete this check.

#### Edits that keep the check AI Managed

Changing only the following fields saves the change without converting the check, as long as the other values are resent unchanged:

| Field | What it controls |
| --- | --- |
| `description` | Free-text description shown in the UI. |
| `tags` | Tags assigned to the check. |
| `additional_metadata` | Key-value metadata pairs. |
| `anomaly_message_field` | The source-record field used as the anomaly message. |
| `status` | Active or Draft state. |

> **Example: Add a tag without converting the check**
>
> ```bash
> curl -X PUT "https://your-instance.qualytics.io/api/quality-checks/101" \
>   -H "Authorization: Bearer YOUR_API_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "description": "price stays between 0 and 10000",
>     "fields": ["price"],
>     "properties": { "min": 0, "inclusive_min": true, "max": 10000, "inclusive_max": true },
>     "tags": ["critical", "ops-reviewed"]
>   }'
> ```
>
> **Response** (abbreviated): the check still has `inferred: true`. The next Profile run can continue to refresh it.

> **Warning: Conversion is silent**
>
> The conversion happens with no extra confirmation step. If you do not want to convert the check, resend `properties` and `fields` with their current values, leave `coverage` and `filter` out (or resend them unchanged), and change only organizational fields.

### Deleting an AI Managed Check

Send `DELETE /api/quality-checks/{id}` to remove the check. Deleting an AI Managed check does not stop Qualytics from re-creating an equivalent check during the next Profile run if the data still suggests the rule.

**Permission**: Author or above.

### Error Responses

| Status Code | Description |
| --- | --- |
| `400 Bad Request` | The payload is malformed (for example, missing a required field or sending an unexpected shape). |
| `401 Unauthorized` | Missing or invalid API token. |
| `403 Forbidden` | The user does not have the required team permission on the datastore. |
| `404 Not Found` | A check with the specified ID does not exist. |
| `422 Unprocessable Entity` | The payload is well-formed but a value fails schema validation (for example, a property that is not supported by the check's rule type). |

*Last updated: November 10, 2026.*
