# Scan API

Source: https://userguide.qualytics.io/operations/scan/api/

This page provides payload examples for running, scheduling, and checking the status of scan operations. Replace the placeholder values with data specific to your setup.

All endpoints use the base URL of your Qualytics deployment (e.g., `https://your-instance.qualytics.io/api`).

> **Tip: Interactive API reference**
>
> For the full, interactive API reference (request schemas, response examples, and an in-browser request runner), visit the [API docs](https://userguide.qualytics.io/api/docs).

## Running a Scan operation

To run a scan operation, use the API payload example below and replace the placeholder values with your specific values.

### Endpoint (Post)

**Endpoint**: `POST /api/operations/run`

Remediation is not set per scan. It follows the datastore's `enrichment_remediation_strategy` setting.

**Tab: Option I: Running a Scan operation of all containers**

- **container_names:** `[]` means that it will scan all containers.
- **max_records_analyzed_per_partition:** `null` means that it will scan all records of all containers.
- **auto_resolve_passed_anomalies:** `true` (default for Full scans) automatically resolves previously open anomalies when the same checks no longer detect the issue. Forced to `false` server-side when `incremental` is `true`.

```json
{
    "type":"scan",
    "name":null,
    "datastore_id": 42,
    "container_names":[],
    "incremental":false,
    "auto_resolve_passed_anomalies":true,
    "max_records_analyzed_per_partition":null,
    "enrichment_source_record_limit":10
}
```

**Tab: Option II: Running a Scan operation of specific containers**

- **container_names:** `["table_name_1", "table_name_2"]` means that it will scan only the tables table_name_1 and table_name_2.
- **max_records_analyzed_per_partition:** `1000000` means that it will scan a maximum of 1 million records per partition.

```json
{
    "type":"scan",
    "name":null,
    "datastore_id": 42,
    "container_names":[
      "table_name_1",
      "table_name_2"
    ],
    "max_records_analyzed_per_partition":1000000,
    "enrichment_source_record_limit":10
}
```

**Tab: Option III: Running a Scan operation of specific checks**

- **check_ids:** `[101, 102]` means that only those two checks are asserted, and only the containers that own at least one of them are scanned. It combines with `check_categories`, `container_names`, and `container_tags`: a check has to pass every selection to be asserted.
- Omit `check_ids`, or send `null`, to assert every active check. An empty list is rejected with a `422`, and so is an id that is not an active check in the selected categories on a targeted container or field.

```json
{
    "type":"scan",
    "name":null,
    "datastore_id": 42,
    "container_names":[],
    "check_categories":["data_integrity"],
    "check_ids":[101, 102],
    "incremental":false,
    "enrichment_source_record_limit":10
}
```

> **Info: About enrichment_source_record_limit**
>
> Caps how many source records are kept per anomaly: the rows written to the `_source_records` enrichment output and returned by the anomaly source-record endpoints. It defaults to `10` and accepts values from `1` to `1,000,000,000`. Set it on the datastore to change the default for every scan, or on a scan run or schedule to override it for that run. A change applies to scans that start after it; earlier anomalies keep the sample they were written with. The limit does not affect `anomalous_records_count` on the anomaly, which holds the full count whenever the platform can compute it (it is `null` for shape anomalies whose affected rows cannot be counted). Raising it on wide tables increases enrichment storage and scan time.

> **Info: About persist_source_records_audit**
>
> When `true`, the scan writes every record it loads, for each container it loads, into a new enrichment output named `<enrichment_prefix>_<operation_id>_<container_id>_records_adt`, with the source container's columns. It defaults to `false` and requires the datastore to have an enrichment destination. The snapshot is not capped by `enrichment_source_record_limit` or other anomaly limits: it holds exactly what the scan loaded, so `incremental`, filters, and `max_records_analyzed_per_partition` still shape it. Values are stored unmasked, including fields masked on the source container, so restrict access to the enrichment destination before enabling it. The field is also accepted on scheduled scans (`POST /api/operations/schedule`) and on Flow scan actions. See [Persist Source Records for Audit](https://userguide.qualytics.io/operations/scan/deep-dive/scan-settings/#persist-source-records-for-audit).
>
> ```json
> {
>     "type":"scan",
>     "datastore_id": 42,
>     "container_names":[],
>     "incremental":false,
>     "persist_source_records_audit":true
> }
> ```

## Scheduling a Scan operation of all containers

To schedule a scan operation, use the API payload example below and replace the placeholder values with your specific values.

### Endpoint (Post)

**Endpoint**: `POST /api/operations/schedule`

This payload is to run a scheduled scan operation every day at 00:00

**Tab: Scheduling a Scan operation of all containers**

```json
{
    "type":"scan",
    "name":"My scheduled Scan operation",
    "datastore_id":"datastore-id",
    "container_names":[],
    "incremental": false,
    "auto_resolve_passed_anomalies": true,
    "max_records_analyzed_per_partition":null,
    "enrichment_source_record_limit":10,
    "crontab":"0 0 * * *"
}
```

> **Info: About check_ids on a schedule**
>
> A schedule accepts `check_ids` the same way a run does, and keeps the picked checks from one run to the next. A pick that is later archived, set to draft, or deleted is dropped from its runs. When no pick is left, the scheduled run fails and asks for the selection to be updated, and updating the schedule is refused until it picks at least one active check again, or leaves `check_ids` out to assert every active check.

## Retrieving Scan operation information

### Endpoint (Get)

**Endpoint**: `GET /api/operations/{id}`

The `status` object includes `auto_resolved_anomaly_count`, the number of previously open anomalies this scan automatically resolved (always `0` for Incremental scans and for Full scans that ran with `auto_resolve_passed_anomalies` set to `false`).

`check_ids` lists the checks the scan asserted, or `null` when it asserted every active check. For a scan whose tables were chosen by tag, it also includes the checks the tags selected through tagged fields and tagged checks. `requested_check_ids` lists only the checks the request picked, or `null` when it picked none.

**Tab: Example result response**

```json
{
    "id": 12345,
    "created": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
    "type": "scan",
    "start_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
    "end_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
    "result": "success",
    "message": null,
    "triggered_by": "user@example.com",
    "datastore": {
        "id": 101,
        "name": "Datastore-Sample",
        "store_type": "jdbc",
        "type": "db_type",
        "enrichment_only": false,
        "enrichment_prefix": "data_prefix",
        "favorite": false
    },
    "schedule": null,
    "incremental": false,
    "auto_resolve_passed_anomalies": true,
    "remediation": "none",
    "max_records_analyzed_per_partition": -1,
    "greater_than_time": null,
    "greater_than_batch": null,
    "high_count_rollup_threshold": 10,
    "enrichment_source_record_limit": 10,
    "check_categories": ["data_integrity"],
    "check_ids": [101, 102],
    "requested_check_ids": [101, 102],
    "status": {
        "total_containers": 2,
        "containers_analyzed": 2,
        "partitions_scanned": 2,
        "records_processed": 28,
        "anomalies_identified": 2,
        "auto_resolved_anomaly_count": 1
    },
    "containers": [
        {
        "id": 234,
        "name": "Container1",
        "container_type": "table",
        "table_type": "table"
        },
        {
        "id": 235,
        "name": "Container2",
        "container_type": "table",
        "table_type": "table"
        }
    ],
    "container_scans": [
        {
        "id": 456,
        "created": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "container": {
            "id": 235,
            "name": "Container2",
            "container_type": "table",
            "table_type": "table"
        },
        "start_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "end_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "records_processed": 8,
        "anomaly_count": 1,
        "result": "success",
        "message": null
        },
        {
        "id": 457,
        "created": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "container": {
            "id": 234,
            "name": "Container1",
            "container_type": "table",
            "table_type": "table"
        },
        "start_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "end_time": "YYYY-MM-DDTHH:MM:SS.ssssssZ",
        "records_processed": 20,
        "anomaly_count": 1,
        "result": "success",
        "message": null
        }
    ],
    "tags": []
}
```

## Container Scans

A scan operation runs one container scan per container, and each container scan runs one partition scan per partition. Container scans are the unit that links a scan to its anomalies, its check results, and its source records. Every timestamp is UTC, ISO 8601 with a trailing `Z`.

### List container scans

**Endpoint:** `GET /api/container-scans`

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

**Query parameters (all optional):**

| Parameter | Type | Description |
| --- | --- | --- |
| `datastore`, `container`, `operation` | `int[]` | Scope to specific datastores, containers, or scan operations. |
| `name`, `search` | `string` | Match on the container name (`search` also matches the ID). |
| `result` | `string[]` | Filter by result (`success`, `failure`, `aborted`, `queued`, `running`). |
| `latest_only` | `bool` | Return only the latest container scan of each container. |
| `created_date`, `offset` | `date`, `int` | Return container scans created on one day; `offset` is your timezone offset from UTC in minutes. |
| `sort_created`, `sort_active_anomalies` | `"asc" \| "desc"` | Sort options. |
| `page`, `size` | `int` | Pagination. |

### Get a container scan

**Endpoint:** `GET /api/container-scans/{id}`

**Permission:** Member role plus the Reporter team permission on the datastore.

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "id": 456,
>   "created": "2026-06-01T02:00:00.117Z",
>   "operation_id": 12345,
>   "datastore": { "id": 101, "name": "Sales" },
>   "container": { "id": 235, "name": "orders", "container_type": "table" },
>   "start_time": "2026-06-01T02:00:00.117Z",
>   "end_time": "2026-06-01T02:03:41.902Z",
>   "records_processed": 121500,
>   "anomaly_count": 37,
>   "result": "success",
>   "message": null,
>   "partition_scans": [
>     {
>       "id": 9017,
>       "partition": { "id": 300, "name": "orders" },
>       "records_processed": 121500,
>       "anomaly_count": 37,
>       "open_anomaly_count": 37,
>       "archived_anomaly_count": 0,
>       "result": "success",
>       "message": null
>     }
>   ]
> }
> ```

`start_time` is the value to pass as `version_at` to [`GET /api/quality-checks/{id}/version`](https://userguide.qualytics.io/data-quality-checks/checks/api/#version-at-a-point-in-time) to see the check definitions this scan evaluated.

### List anomalies of a container scan

**Endpoint:** `GET /api/container-scans/{id}/anomalies`

**Permission:** Member role.

Returns the anomalies the container scan produced, paginated, in the same shape as `GET /api/anomalies`. The list endpoint offers the same scoping with more filters through `GET /api/anomalies?container_scan={id}`; see the [Anomalies API](https://userguide.qualytics.io/anomalies/api/#list-anomalies).

### Get source records of a container scan

Returns the source records of every anomaly the container scan produced that has source records, deduplicated when several anomalies share a record. Two formats are available.

**Endpoint (JSON):** `GET /api/container-scans/{id}/source-records`

**Endpoint (CSV):** `GET /api/container-scans/{id}/source-records/download`

**Permission:** Member role plus the Viewer team permission on the datastore; `include_masked=true` needs the Editor team permission.

**Query parameters:**

| Parameter | Type | Description |
| --- | --- | --- |
| `limit` | `int` (≥1) | Maximum number of rows returned. Defaults to `10` on the JSON endpoint; omit on the CSV endpoint to download every recorded row. |
| `include_masked` | `bool` (default `false`) | When `true`, returns raw values for masked fields. Requires the **Editor** team permission, and the platform writes an audit-log entry naming the masked fields you accessed. |

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/container-scans/456/source-records/download?include_masked=true" \
>   -H "Authorization: Bearer YOUR_TOKEN" -o source_records_456.csv
> ```

The rows are the same ones served per anomaly by `GET /api/anomalies/{id}/source-record`, so the number kept per anomaly is capped by `enrichment_source_record_limit` (see [Running a Scan operation](https://userguide.qualytics.io/operations/scan/api/#running-a-scan-operation)).

## List Check Results for a Scan

Two endpoints return every check that a scan reported, with each check's result and the number of anomalies it produced in that scan. Use them to answer "which checks passed, which failed, and which could not be evaluated in scan X". Both are paginated (`page`, `size`).

**Endpoint (whole scan):** `GET /api/operations/{id}/scan-checks`

**Endpoint (one container scan):** `GET /api/container-scans/{id}/checks`

**Query parameters:**

| Parameter | Applies to | Type | Description |
| --- | --- | --- | --- |
| `search` | both | `string` | Substring match on the check description or ID. |
| `result` | container scan | `"passed" \| "failed" \| "unasserted"` | Return only checks with that `scan_result`. |
| `sort_anomalies` | container scan | `"desc" \| "asc"` (default `desc`) | Sort by anomaly count. |

**Permission:** Member role, plus the Reporter team permission on the datastore for the operation endpoint and the Viewer team permission for the container-scan endpoint. The operation endpoint is only available for scan operations.

> **Example: Example request**
>
> ```bash
> curl -X GET "https://your-instance.qualytics.io/api/container-scans/456/checks?result=failed" \
>   -H "Authorization: Bearer YOUR_TOKEN"
> ```

> **Example: Example response (abbreviated)**
>
> ```json
> {
>   "items": [
>     {
>       "id": 778,
>       "description": "Orders must reference a customer",
>       "rule_type": "notNull",
>       "status": "Active",
>       "scan_anomaly_count": 37,
>       "scan_result": "failed",
>       "container_scan_anomaly_count": 37,
>       "container": { "id": 235, "name": "orders" },
>       "container_scan_id": 456,
>       "fields": [{ "id": 3101, "name": "customer_id" }]
>     }
>   ],
>   "total": 1,
>   "page": 1,
>   "size": 50,
>   "pages": 1
> }
> ```

`scan_result` is the check's result in this scan:

| Value | Meaning |
| --- | --- |
| `failed` | The check produced at least one anomaly. |
| `unasserted` | The scan could not evaluate the check in any partition (for example, a filter with invalid syntax or a field that no longer exists). It produced no anomalies, but it did not pass. |
| `passed` | Neither of the above. A check counts as evaluated when any partition of the container scan passed or failed it. |

Read `scan_result` rather than treating a `scan_anomaly_count` of `0` as passed: an unasserted check also has a count of `0`.

`scan_anomaly_count` is the number of distinct anomalies this check produced in the scan. `container_scan_anomaly_count` is the distinct anomaly count for the whole container scan across all checks. Do not sum `scan_anomaly_count` across checks to get it, because one anomaly can fail several checks.

For per-container totals, the `container_scan` object returned by `GET /api/operations/{id}/containers` carries `passed_checks_count`, `failed_checks_count` and `unasserted_checks_count`, which follow the same rules. Unasserted checks are counted in neither of the other two.

To see what a check looked like when the scan ran, pass the scan's `start_time` to [`GET /api/quality-checks/{id}/version`](https://userguide.qualytics.io/data-quality-checks/checks/api/#version-at-a-point-in-time).

*Last updated: November 10, 2026.*
