# Web Scanners

> Manage web scanners that crawl your sites for trackers, cookies, and third-party requests. Create a scanner against a root domain, edit its crawl scope, trigger a scan on demand, or delete it (which also removes its suppression rules).

Source: https://docs.oursprivacy.com/docs/platform-api/web-scanners

Manage web scanners that crawl your sites for trackers, cookies, and third-party requests. Create a scanner against a root domain, edit its crawl scope, trigger a scan on demand, or delete it (which also removes its suppression rules).

## GET /rest/v1/web-scanners

List every web scanner for this account. Not paginated — accounts have a small number of scanners in practice, so the response always fits in a single page. Requires API-key scope or current OAuth user permission: webScanner:list

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners`

Authentication: apiKey or oauth2.

### Responses

- `200`: List of web scanners
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entities` | object[] | Yes |  |
| `entities[].id` | string | Yes |  |
| `entities[].accountId` | string | Yes |  |
| `entities[].name` | string \| null | No |  |
| `entities[].status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `entities[].rootDomain` | string | Yes |  |
| `entities[].excludedPatterns` | string[] \| null | No |  |
| `entities[].includedUrls` | string[] \| null | No |  |
| `entities[].urlLimit` | number \| null | No |  |
| `entities[].scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `entities[].nextScheduledScanAt` | string \| null | No |  |
| `entities[].scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `entities[].lastScannedAt` | string \| null | No |  |
| `entities[].lastScanStartedAt` | string \| null | No |  |
| `entities[].lastRunCookieCount` | number \| null | No |  |
| `entities[].lastRunRequestCount` | number \| null | No |  |
| `entities[].lastRunSuccessUrlCount` | number \| null | No |  |
| `entities[].lastRunHighRiskRequestCount` | number \| null | No |  |
| `entities[].createdAt` | string \| null | No |  |
| `entities[].updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## POST /rest/v1/web-scanners

Create a new web scanner for a root domain. Call GET /rest/v1/web-scanners first to check for an existing scanner: each successful request creates a new scanner, even when the root domain is already used. If the response is ambiguous, check the list before retrying to avoid creating a duplicate. A first scan is enqueued automatically after creation on a best-effort basis; the returned entity may not yet reflect asynchronous scan-state changes. `rootDomain` is required; missing, empty, or malformed values are rejected as HTTP 400. Everything else falls back to defaults (`status: Enabled`, `urlLimit: 1000`, `scanSchedule: weekly`, no excluded patterns, no extra seed URLs). Invalid configuration or account limits return HTTP 409. Requires scope: `webScanner:create`. Requires API-key scope or current OAuth user permission: webScanner:create

Request URL: `POST https://app.oursprivacy.com/rest/v1/web-scanners`

Authentication: apiKey or oauth2.

### Request body (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rootDomain` | string | Yes | Root domain to crawl (e.g. `example.com`). Required on create. Missing or empty values fail request validation as HTTP 400. Present-but-malformed values are rejected as HTTP 400 with the validation reason in `details`. |
| `name` | string \| null | No | Optional display name for this web scanner. |
| `status` | string | No | Whether this web scanner is enabled. Defaults to `Enabled`. One of: `"Disabled"`, `"Enabled"`. |
| `excludedPatterns` | string[] \| null | No | URL glob patterns to skip during crawl. Max 100 entries. |
| `includedUrls` | string[] \| null | No | Additional seed URLs to include as crawl entry points. Each must be an http(s) URL. Max 100 entries. |
| `urlLimit` | number \| null | No | Maximum URLs to crawl per scan (1–20,000). Defaults to 1,000 when omitted. |
| `scanSchedule` | string | No | How often the scanner crawls this monitor on its own: `daily`, `weekly`, `monthly`, or `manual` to disable scheduled crawls and only run on demand. Defaults to `weekly`. Cadences advance on UTC calendar days from the last completed scan. One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |

### Responses

- `200`: Created web scanner
- `400`: Bad request — the request body is malformed or `rootDomain` is missing, empty, or not a valid domain.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — one of the business-rule limits was exceeded (for example too many excluded patterns or `urlLimit` out of range). Reason is in the response `error` field.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `accountId` | string | Yes |  |
| `name` | string \| null | No |  |
| `status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `rootDomain` | string | Yes |  |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `nextScheduledScanAt` | string \| null | No |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `lastScannedAt` | string \| null | No |  |
| `lastScanStartedAt` | string \| null | No |  |
| `lastRunCookieCount` | number \| null | No |  |
| `lastRunRequestCount` | number \| null | No |  |
| `lastRunSuccessUrlCount` | number \| null | No |  |
| `lastRunHighRiskRequestCount` | number \| null | No |  |
| `createdAt` | string \| null | No |  |
| `updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}

Find a single web scanner by ID. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Responses

- `200`: Web scanner details
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `accountId` | string | Yes |  |
| `name` | string \| null | No |  |
| `status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `rootDomain` | string | Yes |  |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `nextScheduledScanAt` | string \| null | No |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `lastScannedAt` | string \| null | No |  |
| `lastScanStartedAt` | string \| null | No |  |
| `lastRunCookieCount` | number \| null | No |  |
| `lastRunRequestCount` | number \| null | No |  |
| `lastRunSuccessUrlCount` | number \| null | No |  |
| `lastRunHighRiskRequestCount` | number \| null | No |  |
| `createdAt` | string \| null | No |  |
| `updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## PATCH /rest/v1/web-scanners/{id}

Partially update a web scanner. Only the fields you send are changed; omitted fields keep their current value. List-valued fields (`excludedPatterns`, `includedUrls`) are replaced wholesale when sent. If `rootDomain` is provided and malformed, the request is rejected as HTTP 400. Use `POST /rest/v1/web-scanners/{id}/trigger` to start a new scan after edits. Requires API-key scope or current OAuth user permission: webScanner:update

Request URL: `PATCH https://app.oursprivacy.com/rest/v1/web-scanners/{id}`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Request body (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rootDomain` | string \| null | No | Replace the scanner root domain. When provided, malformed values are rejected as HTTP 400 with the validation reason in `details`. |
| `name` | string \| null | No |  |
| `status` | string | No | One of: `"Disabled"`, `"Enabled"`. |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | No | How often the scanner crawls this monitor on its own: `daily`, `weekly`, `monthly`, or `manual` to disable scheduled crawls and only run on demand. Defaults to `weekly`. Cadences advance on UTC calendar days from the last completed scan. Omit to leave the current cadence unchanged. One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |

### Responses

- `200`: Updated web scanner
- `400`: Bad request — the request body is malformed or `rootDomain` is not a valid domain.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — a business-rule validation failed (for example oversized lists, `urlLimit` out of range, or a malformed seed URL). Reason is in the response `error` field.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `accountId` | string | Yes |  |
| `name` | string \| null | No |  |
| `status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `rootDomain` | string | Yes |  |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `nextScheduledScanAt` | string \| null | No |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `lastScannedAt` | string \| null | No |  |
| `lastScanStartedAt` | string \| null | No |  |
| `lastRunCookieCount` | number \| null | No |  |
| `lastRunRequestCount` | number \| null | No |  |
| `lastRunSuccessUrlCount` | number \| null | No |  |
| `lastRunHighRiskRequestCount` | number \| null | No |  |
| `createdAt` | string \| null | No |  |
| `updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## DELETE /rest/v1/web-scanners/{id}

Delete a web scanner. Associated suppression rules are deleted in the same operation. Requires API-key scope or current OAuth user permission: webScanner:delete

Request URL: `DELETE https://app.oursprivacy.com/rest/v1/web-scanners/{id}`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Responses

- `200`: Deleted successfully
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The id of the web scanner that was deleted. |
| `deleted` | boolean | Yes | True when the scanner and its rules were deleted. |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## POST /rest/v1/web-scanners/{id}/trigger

Manually kick off a new scan for this web scanner. The request body is empty (or `{}`). A successful response means the request was accepted; because the scan starts asynchronously, the returned entity may still reflect pre-trigger values for fields like `scanStatus` and `lastScanStartedAt`. The trigger is rate-limited: a 409 is returned if another scan is already in flight, or if this production monitor has `urlLimit >= 5000` and its previous scan completed within the last 10 minutes; the reason is in the response `error` field. Requires API-key scope or current OAuth user permission: webScanner:trigger

Request URL: `POST https://app.oursprivacy.com/rest/v1/web-scanners/{id}/trigger`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Responses

- `200`: Web scanner details returned after the trigger request was accepted.
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — another scan is already running, or this production monitor has `urlLimit >= 5000` and its previous scan completed within the last 10 minutes. Reason is in the response `error` field.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `accountId` | string | Yes |  |
| `name` | string \| null | No |  |
| `status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `rootDomain` | string | Yes |  |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `nextScheduledScanAt` | string \| null | No |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `lastScannedAt` | string \| null | No |  |
| `lastScanStartedAt` | string \| null | No |  |
| `lastRunCookieCount` | number \| null | No |  |
| `lastRunRequestCount` | number \| null | No |  |
| `lastRunSuccessUrlCount` | number \| null | No |  |
| `lastRunHighRiskRequestCount` | number \| null | No |  |
| `createdAt` | string \| null | No |  |
| `updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## POST /rest/v1/web-scanners/{id}/authenticated-scan

Start a normal full scan with short-lived credentials supplied by your scheduler. Every in-scope page in this scan uses the credentials. Set `scanSchedule` to `manual` when your scheduler should be the only source of scans. Credential values are not returned or included in scan results. Requires API-key scope or current OAuth user permission: webScanner:trigger

Request URL: `POST https://app.oursprivacy.com/rest/v1/web-scanners/{id}/authenticated-scan`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Request body (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expiresInHours` | integer | Yes | Whole-hour validity window for these credentials. Send a fresh credential for every externally scheduled scan. |
| `credentials` | object[] | Yes | Short-lived credentials for this scan. Their values are not returned in API responses or captured in scan results. |
| `credentials[].location` | string | Yes | Where to place the credential for first-party crawl requests. Header credentials are not sent to third-party requests. One of: `"header"`, `"cookie"`. |
| `credentials[].name` | string | Yes | HTTP header or cookie name. Use `Authorization` for a bearer-style header. |
| `credentials[].value` | string | Yes | Complete credential value, including any required prefix such as `Bearer `. |

### Responses

- `200`: Web scanner details returned after the authenticated scan request was accepted.
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — another scan is already running, the scanner is within its cooldown, or the scan could not be accepted.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `accountId` | string | Yes |  |
| `name` | string \| null | No |  |
| `status` | string | Yes | One of: `"Disabled"`, `"Enabled"`. |
| `rootDomain` | string | Yes |  |
| `excludedPatterns` | string[] \| null | No |  |
| `includedUrls` | string[] \| null | No |  |
| `urlLimit` | number \| null | No |  |
| `scanSchedule` | string | Yes | One of: `"daily"`, `"manual"`, `"monthly"`, `"weekly"`. |
| `nextScheduledScanAt` | string \| null | No |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `lastScannedAt` | string \| null | No |  |
| `lastScanStartedAt` | string \| null | No |  |
| `lastRunCookieCount` | number \| null | No |  |
| `lastRunRequestCount` | number \| null | No |  |
| `lastRunSuccessUrlCount` | number \| null | No |  |
| `lastRunHighRiskRequestCount` | number \| null | No |  |
| `createdAt` | string \| null | No |  |
| `updatedAt` | string \| null | No |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## POST /rest/v1/web-scanners/{id}/targeted-scan

Queue an isolated verification capture for one exact in-scope page. This does not update monitor history, inventory counts, or the monitor-wide last-scanned timestamp. Poll the verification-run endpoint with the returned id for terminal evidence. Limited to one request per minute per account across all scanners and callers; retry a cooldown conflict after 60 seconds. Requires API-key scope or current OAuth user permission: webScanner:trigger

Request URL: `POST https://app.oursprivacy.com/rest/v1/web-scanners/{id}/targeted-scan`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Request body (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `targetUrl` | string (uri) | Yes | Exact in-scope HTTP(S) page URL to verify. Fragments are removed; query parameters are retained. |

### Responses

- `200`: Queued one-page verification run.
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — the target is outside the monitor scope, excluded by configuration, another monitor scan is already in progress, or the account verification cooldown is active.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes |  |
| `scannerId` | string (uuid) | Yes |  |
| `requestedUrl` | string (uri) | Yes |  |
| `resolvedUrl` | string \| null | No |  |
| `status` | string | Yes | One of: `"queued"`, `"in_progress"`, `"completed"`, `"failed"`. |
| `cause` | string \| null | No |  |
| `createdAt` | string | Yes |  |
| `completedAt` | string \| null | No |  |
| `run` | object \| null | No |  |
| `run.metadata` | object \| null | No |  |
| `run.metadata.success_urls` | string[] | Yes |  |
| `run.metadata.failed_urls` | object[] | Yes |  |
| `run.metadata.failed_urls[].url` | string | Yes |  |
| `run.metadata.failed_urls[].status_code` | number \| null | No |  |
| `run.metadata.failed_urls[].details` | string \| null | No |  |
| `run.metadata.excluded_urls` | string[] \| null | No |  |
| `run.metadata.excluded_patterns` | string[] \| null | No |  |
| `run.summary_generated_at` | string \| null | No |  |
| `run.cookieCount` | integer | Yes |  |
| `run.localStorageCount` | integer | Yes |  |
| `run.requestCount` | integer | Yes |  |
| `run.vendorCount` | integer | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/verification-run

Read a UUID-addressed one-page verification run. The run is isolated from normal monitor history and is returned only when it belongs to the requested scanner. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/verification-run`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `runId` | query | string (uuid) | Yes | Verification run UUID returned by the targeted scan command. Verification run UUID returned by the targeted scan command. |

### Responses

- `200`: One-page verification run and captured evidence summary.
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes |  |
| `scannerId` | string (uuid) | Yes |  |
| `requestedUrl` | string (uri) | Yes |  |
| `resolvedUrl` | string \| null | No |  |
| `status` | string | Yes | One of: `"queued"`, `"in_progress"`, `"completed"`, `"failed"`. |
| `cause` | string \| null | No |  |
| `createdAt` | string | Yes |  |
| `completedAt` | string \| null | No |  |
| `run` | object \| null | No |  |
| `run.metadata` | object \| null | No |  |
| `run.metadata.success_urls` | string[] | Yes |  |
| `run.metadata.failed_urls` | object[] | Yes |  |
| `run.metadata.failed_urls[].url` | string | Yes |  |
| `run.metadata.failed_urls[].status_code` | number \| null | No |  |
| `run.metadata.failed_urls[].details` | string \| null | No |  |
| `run.metadata.excluded_urls` | string[] \| null | No |  |
| `run.metadata.excluded_patterns` | string[] \| null | No |  |
| `run.summary_generated_at` | string \| null | No |  |
| `run.cookieCount` | integer | Yes |  |
| `run.localStorageCount` | integer | Yes |  |
| `run.requestCount` | integer | Yes |  |
| `run.vendorCount` | integer | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/verification-runs

List the one-page verification runs recorded for this scanner, newest first, each with its own capture counts. Verification runs are isolated from monitor history and never affect inventory counts or the monitor-wide last-scanned timestamp. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/verification-runs`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `limit` | query | integer | No | How many runs to read, newest first. Defaults to 20, capped at 50. How many runs to read, newest first. Defaults to 20, capped at 50. |

### Responses

- `200`: One-page verification runs for this scanner, newest first.
- `400`: Bad Request
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | Yes |  |
| `items[].id` | string (uuid) | Yes |  |
| `items[].scannerId` | string (uuid) | Yes |  |
| `items[].requestedUrl` | string (uri) | Yes |  |
| `items[].resolvedUrl` | string \| null | No |  |
| `items[].status` | string | Yes | One of: `"queued"`, `"in_progress"`, `"completed"`, `"failed"`. |
| `items[].cause` | string \| null | No |  |
| `items[].createdAt` | string | Yes |  |
| `items[].completedAt` | string \| null | No |  |
| `items[].run` | object \| null | No |  |
| `items[].run.metadata` | object \| null | No |  |
| `items[].run.metadata.success_urls` | string[] | Yes |  |
| `items[].run.metadata.failed_urls` | object[] | Yes |  |
| `items[].run.metadata.failed_urls[].url` | string | Yes |  |
| `items[].run.metadata.failed_urls[].status_code` | number \| null | No |  |
| `items[].run.metadata.failed_urls[].details` | string \| null | No |  |
| `items[].run.metadata.excluded_urls` | string[] \| null | No |  |
| `items[].run.metadata.excluded_patterns` | string[] \| null | No |  |
| `items[].run.summary_generated_at` | string \| null | No |  |
| `items[].run.cookieCount` | integer | Yes |  |
| `items[].run.localStorageCount` | integer | Yes |  |
| `items[].run.requestCount` | integer | Yes |  |
| `items[].run.vendorCount` | integer | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/runs

List the scan runs recorded for this scanner, newest first, one per UTC day. Pass an entry's `runDate` as `date` to read that run from the summary, findings, cookies, or decision queue. A missing id and an id this credential cannot access return the same 404. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/runs`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `limit` | query | integer \| null | No | Maximum number of items to return. Defaults to 25; values below 1 are clamped to 1 and values above 100 are clamped to 100. Maximum number of items to return. Defaults to 25; values below 1 are clamped to 1 and values above 100 are clamped to 100. |
| `cursor` | query | string | No | Opaque pagination cursor from pagination.nextCursor in the previous response. Do not decode or modify it. Malformed cursors return 400 Bad Request. Opaque pagination cursor from pagination.nextCursor in the previous response. Do not decode or modify it. Malformed cursors return 400 Bad Request. |

### Responses

- `200`: Scan runs recorded for this scanner, newest first
- `400`: Bad request — malformed `cursor`.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entities` | object[] | Yes |  |
| `entities[].runDate` | string | Yes | When the run was recorded. Pass it as `date` to read that run. |
| `pagination` | object | Yes |  |
| `pagination.nextCursor` | string \| null | No |  |
| `pagination.hasMore` | boolean | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/findings

List the third-party trackers (requests) found on a scan run, with their risk, category, the pages they were seen on, redacted deterministic PII/PHI data-flow metadata, and whether each host is already covered by a CMP consent service. Data-flow findings include only recipient metadata, categories, source locations, safe query/body field names when available, and counts; matched query, body, and pathname values are never returned. Defaults to the latest run; pass `date` (a day listed by `GET /rest/v1/web-scanners/{id}/runs`) to read an earlier run. Documented exception to the cursor-pagination standard: paginates with `limit` and `offset` because each run is an immutable snapshot. A host that is neither covered (`coveredByCmp: false`) nor matched by a suppression rule still needs a triage decision — resolve it by adding the host to a CMP consent service or by creating a suppression rule with `POST /rest/v1/web-scanner-rules`. Use `GET /rest/v1/web-scanners/{id}/summary` for the rolled-up counts. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/findings`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `date` | query | string | No | Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. |
| `limit` | query | integer | No | Maximum number of findings to return. Defaults to 25; must be between 1 and 100. |
| `offset` | query | integer \| null | No | Skip this many findings before returning. Use with `limit` for load-more paging. |

### Responses

- `200`: Third-party trackers and redacted outbound data-flow findings
- `400`: Bad request — malformed `date`, a day with no run, or `limit`/`offset` out of range.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | Yes | Third-party trackers seen on the run. `coveredByCmp` is true when the host is already mapped to a CMP consent service; `risk` is high/medium/low/unknown. Hosts that are neither covered nor matched by a suppression rule are the ones that need a triage decision — clear them by adding the host to a CMP consent service or creating a suppression rule (POST /rest/v1/web-scanner-rules). |
| `items[].hostname` | string | Yes |  |
| `items[].urls` | string[] | Yes |  |
| `items[].types` | string[] | Yes |  |
| `items[].cookies` | object[] \| null | No |  |
| `items[].cookies[].name` | string | Yes |  |
| `items[].cookies[].value` | string \| null | No |  |
| `items[].cookies[].domain` | string \| null | No |  |
| `items[].cookies[].path` | string \| null | No |  |
| `items[].seenOn` | string[] | Yes |  |
| `items[].privacyKeywords` | string[] \| null | No |  |
| `items[].risk` | string \| null | No |  |
| `items[].category` | string \| null | No |  |
| `items[].displayName` | string \| null | No |  |
| `items[].dataFlow` | object \| null | No |  |
| `items[].dataFlow.recipient` | object | Yes |  |
| `items[].dataFlow.recipient.hostname` | string | Yes |  |
| `items[].dataFlow.recipient.displayName` | string \| null | No |  |
| `items[].dataFlow.recipient.category` | string \| null | No |  |
| `items[].dataFlow.recipient.risk` | string \| null | No |  |
| `items[].dataFlow.categories` | string[] | Yes |  |
| `items[].dataFlow.signals` | object[] | Yes |  |
| `items[].dataFlow.signals[].category` | string | Yes | One of: `"credit_card"`, `"date_of_birth"`, `"diagnosis"`, `"email"`, `"health_condition"`, `"health_plan_id"`, `"ip_address"`, `"medical_record_number"`, `"medication"`, `"phone"`, `"ssn"`. |
| `items[].dataFlow.signals[].source` | string | Yes | One of: `"query_parameter"`, `"request_body"`, `"url_path"`. |
| `items[].dataFlow.signals[].field` | string \| null | No |  |
| `items[].dataFlow.signals[].encoding` | string \| null | No |  |
| `items[].dataFlow.requestCount` | integer | Yes |  |
| `items[].coveredByCmp` | boolean | Yes |  |
| `items[].coveredByVendorLabel` | string \| null | No |  |
| `total` | integer | Yes | Total number of findings in the run. |
| `hasMore` | boolean | Yes | True when more findings are available beyond the current window. |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/cookies

List the cookies and local-storage entries observed on a scan run. Defaults to the latest run; pass `date` (a day listed by `GET /rest/v1/web-scanners/{id}/runs`) to read an earlier run. Cookies paginate with `limit` and `offset` (a documented exception to the cursor-pagination standard, since each run is an immutable snapshot); local-storage entries are returned in full. A missing id and an id this credential cannot access return the same 404. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/cookies`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (uuid) | Yes | Web scanner UUID from the list or create response. Web scanner UUID from the list or create response. |
| `date` | query | string | No | Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. |
| `limit` | query | integer | No | Maximum number of cookies to return, from 1 to 100. Defaults to 25. Maximum number of cookies to return, from 1 to 100. Defaults to 25. |
| `offset` | query | integer \| null | No | Skip this many cookies before returning. Use with `limit` for load-more paging. Skip this many cookies before returning. Use with `limit` for load-more paging. |

### Responses

- `200`: Cookies and local-storage entries found on the selected scan run
- `400`: Bad request — malformed `date`, a day with no run, or `limit`/`offset` out of range.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `cookies` | object[] | Yes | Cookies observed on the run, paginated by `limit`/`offset`. |
| `cookies[].name` | string | Yes |  |
| `cookies[].value` | string \| null | No |  |
| `cookies[].domain` | string \| null | No |  |
| `cookies[].path` | string \| null | No |  |
| `localStorage` | object[] | Yes | Local-storage entries observed on the run. Returned in full (not paginated). |
| `localStorage[].name` | string | Yes |  |
| `localStorage[].value` | string \| null | No |  |
| `totalCookies` | integer | Yes |  |
| `totalLocalStorage` | integer | Yes |  |
| `hasMore` | boolean | Yes | True when more cookies are available beyond the current window. |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/summary

Compliance summary for a scan run — the rolled-up "what does this site look like, and what still needs a decision" view, assembled server-side so you do not have to page every finding. Includes total host/vendor/cookie counts, captured privacy policies and host coverage, a breakdown by risk and by category, coverage (how many hosts are already covered by a CMP consent service or a suppression rule vs. how many still need a decision), the new/removed host delta versus the previous run, up to 10 highest-risk hosts that still need a decision, the homepage consent check (which third parties were contacted before any choice, after Reject all, and with Global Privacy Control on), and the Content-Security-Policy gap for each first-party hostname. After a consent or CSP fix, trigger a full scan with `POST /rest/v1/web-scanners/{id}/trigger` and read this summary again to confirm it. A null privacyPolicyUrl means that no policy was captured for the hostname. Defaults to the latest run; pass `date` (a day listed by `GET /rest/v1/web-scanners/{id}/runs`) to read an earlier run. Clear a host that needs a decision by adding it to a CMP consent service or creating a suppression rule with `POST /rest/v1/web-scanner-rules`. When the scanner has no completed runs, every count is 0 and `runDate` is null. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/summary`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `date` | query | string | No | Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. |
| `runRevision` | query | string | No |  |
| `coverageRevision` | query | string | No |  |

### Responses

- `200`: Rolled-up compliance summary for the selected scan run
- `400`: Bad request — malformed `date`, or a day with no run.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `scannerId` | string | Yes |  |
| `rootDomain` | string | Yes |  |
| `scanStatus` | string | Yes | One of: `"idle"`, `"scanning"`. |
| `runDate` | string \| null | No |  |
| `runRevision` | string \| null | No |  |
| `coverageRevision` | string \| null | No |  |
| `successUrlCount` | number \| null | No | Pages the run crawled successfully. 0 means the run captured no pages, so zero hosts is not a clean result. Null when there is no run or no crawl record for it. |
| `hostCount` | integer | Yes |  |
| `vendorCount` | integer | Yes |  |
| `cookieCount` | integer | Yes |  |
| `localStorageCount` | integer | Yes |  |
| `privacyPolicyCount` | integer | Yes |  |
| `privacyPolicies` | object[] | Yes | Distinct privacy policies captured for first-party hostnames in the selected scan run, including the bounded visible policy text. |
| `privacyPolicies[].url` | string (uri) | Yes |  |
| `privacyPolicies[].hostnames` | string[] | Yes |  |
| `privacyPolicies[].text` | string | Yes |  |
| `privacyPolicyHosts` | object[] | Yes | Every successfully crawled first-party hostname and its captured privacy policy URL. A null privacyPolicyUrl means no policy was captured for that hostname. |
| `privacyPolicyHosts[].hostname` | string | Yes |  |
| `privacyPolicyHosts[].privacyPolicyUrl` | string \| null | No |  |
| `countsByRisk` | object | Yes |  |
| `countsByRisk.high` | integer | Yes |  |
| `countsByRisk.medium` | integer | Yes |  |
| `countsByRisk.low` | integer | Yes |  |
| `countsByRisk.unknown` | integer | Yes |  |
| `byCategory` | object[] | Yes |  |
| `byCategory[].category` | string | Yes |  |
| `byCategory[].hostCount` | integer | Yes |  |
| `coverage` | object | Yes |  |
| `coverage.totalHostCount` | integer | Yes |  |
| `coverage.coveredHostCount` | integer | Yes |  |
| `coverage.needsDecisionHostCount` | integer | Yes |  |
| `coverage.coveragePercent` | integer | Yes |  |
| `delta` | object \| null | No |  |
| `delta.newHostCount` | integer | Yes |  |
| `delta.removedHostCount` | integer | Yes |  |
| `topUncoveredHosts` | object[] | Yes | Up to 10 hosts that still need a decision (neither CMP-covered nor suppressed), highest risk first. Clear each by adding the host to a CMP consent service or creating a suppression rule (POST /rest/v1/web-scanner-rules) with the reason that explains why it is allowed (baa, internal, approved, compliant, firstParty, ignore). |
| `topUncoveredHosts[].hostname` | string | Yes |  |
| `topUncoveredHosts[].urls` | string[] | Yes |  |
| `topUncoveredHosts[].types` | string[] | Yes |  |
| `topUncoveredHosts[].cookies` | object[] \| null | No |  |
| `topUncoveredHosts[].cookies[].name` | string | Yes |  |
| `topUncoveredHosts[].cookies[].value` | string \| null | No |  |
| `topUncoveredHosts[].cookies[].domain` | string \| null | No |  |
| `topUncoveredHosts[].cookies[].path` | string \| null | No |  |
| `topUncoveredHosts[].seenOn` | string[] | Yes |  |
| `topUncoveredHosts[].privacyKeywords` | string[] \| null | No |  |
| `topUncoveredHosts[].risk` | string \| null | No |  |
| `topUncoveredHosts[].category` | string \| null | No |  |
| `topUncoveredHosts[].displayName` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow` | object \| null | No |  |
| `topUncoveredHosts[].dataFlow.recipient` | object | Yes |  |
| `topUncoveredHosts[].dataFlow.recipient.hostname` | string | Yes |  |
| `topUncoveredHosts[].dataFlow.recipient.displayName` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow.recipient.category` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow.recipient.risk` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow.categories` | string[] | Yes |  |
| `topUncoveredHosts[].dataFlow.signals` | object[] | Yes |  |
| `topUncoveredHosts[].dataFlow.signals[].category` | string | Yes | One of: `"credit_card"`, `"date_of_birth"`, `"diagnosis"`, `"email"`, `"health_condition"`, `"health_plan_id"`, `"ip_address"`, `"medical_record_number"`, `"medication"`, `"phone"`, `"ssn"`. |
| `topUncoveredHosts[].dataFlow.signals[].source` | string | Yes | One of: `"query_parameter"`, `"request_body"`, `"url_path"`. |
| `topUncoveredHosts[].dataFlow.signals[].field` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow.signals[].encoding` | string \| null | No |  |
| `topUncoveredHosts[].dataFlow.requestCount` | integer | Yes |  |
| `topUncoveredHosts[].coveredByCmp` | boolean | Yes |  |
| `topUncoveredHosts[].coveredByVendorLabel` | string \| null | No |  |
| `accessibility` | object \| null | No | Automated accessibility (WCAG 2.1/2.2 A + AA) rollup for the run: `score` is a 0-100 site score (mean of per-page scores; higher is better), with distinct rule-violation `countsByImpact` and the most frequently violated rules in `topViolations` (each with the number of pages it appears on). Covers only the machine-detectable subset of WCAG (~30-40%) — a high score is not a certification of full conformance; manual audit is still required. Null when the run audited no pages. |
| `accessibility.engine` | string | Yes |  |
| `accessibility.score` | integer | Yes |  |
| `accessibility.pagesEvaluated` | integer | Yes |  |
| `accessibility.totalViolations` | integer | Yes |  |
| `accessibility.totalNodes` | integer | Yes |  |
| `accessibility.countsByImpact` | object | Yes |  |
| `accessibility.countsByImpact.critical` | integer | Yes |  |
| `accessibility.countsByImpact.serious` | integer | Yes |  |
| `accessibility.countsByImpact.moderate` | integer | Yes |  |
| `accessibility.countsByImpact.minor` | integer | Yes |  |
| `accessibility.topViolations` | object[] | Yes |  |
| `accessibility.topViolations[].id` | string | Yes |  |
| `accessibility.topViolations[].impact` | string \| null | No |  |
| `accessibility.topViolations[].help` | string | Yes |  |
| `accessibility.topViolations[].helpUrl` | string | Yes |  |
| `accessibility.topViolations[].wcagTags` | string[] | Yes |  |
| `accessibility.topViolations[].pageCount` | integer | Yes |  |
| `accessibility.topViolations[].nodeCount` | integer | Yes |  |
| `accessibility.topViolations[].pages` | string[] | Yes |  |
| `accessibility.topViolations[].sampleNodes` | object[] | Yes |  |
| `accessibility.topViolations[].sampleNodes[].target` | string[] | Yes |  |
| `accessibility.topViolations[].sampleNodes[].html` | string | Yes |  |
| `accessibility.topViolations[].sampleNodes[].failureSummary` | string \| null | No |  |
| `consentProbe` | object \| null | No | Consent check for this run: the homepage loaded before any choice, after Reject all, after Accept all, and with Global Privacy Control on. Null when the run had no consent check. |
| `consentProbe.requestedUrl` | string (uri) | Yes | The page loaded in each consent state. |
| `consentProbe.status` | string | Yes | ours_consent_not_installed means the page has no Ours Privacy Consent Management, so the Reject all and Accept all states could not be driven; the baseline and gpc states are still captured. A failed check, or one where Ours Consent is not installed, is never a clean result. One of: `"queued"`, `"in_progress"`, `"completed"`, `"ours_consent_not_installed"`, `"failed"`. |
| `consentProbe.cause` | string \| null | No | Why the check failed, or why Reject all and Accept all could not be tested. |
| `consentProbe.completedAt` | string \| null | No |  |
| `consentProbe.states` | object[] | Yes | What each state captured. Empty until the check finishes. |
| `consentProbe.states[].state` | string | Yes | baseline is before any consent choice; reject_all and accept_all follow the matching banner choice; gpc loads with Global Privacy Control on. One of: `"baseline"`, `"reject_all"`, `"accept_all"`, `"gpc"`. |
| `consentProbe.states[].outcome` | string | Yes | One of: `"completed"`, `"ours_consent_not_installed"`, `"failed"`. |
| `consentProbe.states[].cause` | string \| null | No |  |
| `consentProbe.states[].requestHostnames` | string[] \| null | No | Third-party hosts contacted in this state. |
| `consentProbe.states[].cookieNames` | string[] \| null | No |  |
| `consentProbe.states[].storageKeys` | string[] \| null | No |  |
| `consentProbe.findings` | object[] | Yes | Hosts contacted in a state where the visitor had not agreed. Empty when none were observed. The Ours Privacy consent script host is never listed, since the banner needs it; other contacted hosts may still be strictly necessary. |
| `consentProbe.findings[].kind` | string | Yes | tracker_before_consent: contacted before any choice; reject_not_honored: contacted after Reject all; gpc_not_honored: contacted with Global Privacy Control on. One of: `"tracker_before_consent"`, `"reject_not_honored"`, `"gpc_not_honored"`. |
| `consentProbe.findings[].hostnames` | string[] | Yes |  |
| `csp` | object[] | Yes | Content-Security-Policy result for each crawled first-party hostname, most serious gap first. |
| `csp[].hostname` | string | Yes | A crawled first-party hostname. |
| `csp[].hasCsp` | boolean | Yes | Whether pages on this hostname sent a Content-Security-Policy. |
| `csp[].gap` | string \| null | No | The most serious policy gap, graded the same way as the dashboard and PDF report. Null when the policy allows every third-party domain the scan observed. One of: `"no_policy"`, `"missing_script_src"`, `"missing_connect_src"`, `"unallowed_domains"`, `null`. |
| `csp[].domainsNotInCsp` | string[] | Yes | Third-party domains contacted from this hostname that the policy does not allow. |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## GET /rest/v1/web-scanners/{id}/decision-queue

List every normalized host in the selected scan run that is neither covered by a current CMP consent service nor matched by an active suppression rule. A host the run's consent check contacted before any choice, after Reject all, or with Global Privacy Control on is included even when the crawl did not request it, with `consentViolations` naming those states. Results are sorted by risk and hostname. Continue with pagination.nextCursor while preserving runRevision and coverageRevision; changed evidence, including a consent check that finishes mid-read, returns HTTP 400 and requires restarting from the first page. Historical runs are evaluated against current coverage configuration. Requires API-key scope or current OAuth user permission: webScanner:find

Request URL: `GET https://app.oursprivacy.com/rest/v1/web-scanners/{id}/decision-queue`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |
| `limit` | query | integer \| null | No | Maximum number of items to return. Defaults to 25; values below 1 are clamped to 1 and values above 100 are clamped to 100. Maximum number of items to return. Defaults to 25; values below 1 are clamped to 1 and values above 100 are clamped to 100. |
| `cursor` | query | string | No | Opaque pagination cursor from pagination.nextCursor in the previous response. Do not decode or modify it. Malformed cursors return 400 Bad Request. Opaque pagination cursor from pagination.nextCursor in the previous response. Do not decode or modify it. Malformed cursors return 400 Bad Request. |
| `date` | query | string | No | Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. Which scan run to read: a day (`2026-05-15`) or an ISO-8601 timestamp, of which only the UTC day is used. A day with no run returns 400; list run days with `GET /rest/v1/web-scanners/{id}/runs`. Defaults to the most recent run when omitted. |
| `runRevision` | query | string | No |  |
| `coverageRevision` | query | string | No |  |

### Responses

- `200`: Revision-bound third-party hosts that still need a coverage decision
- `400`: Bad request — malformed date or cursor, a day with no run, expired cursor, or a stale run/coverage revision.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `scannerId` | string | Yes |  |
| `runDate` | string \| null | No |  |
| `runRevision` | string \| null | No |  |
| `coverageRevision` | string \| null | No |  |
| `total` | integer | Yes |  |
| `entities` | object[] | Yes |  |
| `entities[].hostname` | string | Yes |  |
| `entities[].displayName` | string \| null | No |  |
| `entities[].risk` | string \| null | No |  |
| `entities[].category` | string \| null | No |  |
| `entities[].seenOn` | string[] | Yes |  |
| `entities[].dataFlow` | object \| null | No |  |
| `entities[].dataFlow.recipient` | object | Yes |  |
| `entities[].dataFlow.recipient.hostname` | string | Yes |  |
| `entities[].dataFlow.recipient.displayName` | string \| null | No |  |
| `entities[].dataFlow.recipient.category` | string \| null | No |  |
| `entities[].dataFlow.recipient.risk` | string \| null | No |  |
| `entities[].dataFlow.categories` | string[] | Yes |  |
| `entities[].dataFlow.signals` | object[] | Yes |  |
| `entities[].dataFlow.signals[].category` | string | Yes | One of: `"credit_card"`, `"date_of_birth"`, `"diagnosis"`, `"email"`, `"health_condition"`, `"health_plan_id"`, `"ip_address"`, `"medical_record_number"`, `"medication"`, `"phone"`, `"ssn"`. |
| `entities[].dataFlow.signals[].source` | string | Yes | One of: `"query_parameter"`, `"request_body"`, `"url_path"`. |
| `entities[].dataFlow.signals[].field` | string \| null | No |  |
| `entities[].dataFlow.signals[].encoding` | string \| null | No |  |
| `entities[].dataFlow.requestCount` | integer | Yes |  |
| `entities[].consentViolations` | string[] | Yes | Consent states in which this host was contacted although the visitor had not agreed. Empty when none. A host can be listed from the consent check alone, without crawl evidence; cover it in the consent banner or add a rule to clear it, the same as any other row. |
| `pagination` | object | Yes |  |
| `pagination.nextCursor` | string \| null | No |  |
| `pagination.hasMore` | boolean | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).

## POST /rest/v1/web-scanners/{id}/resolve-coverage-gap

Resolve one hostname from a revision-bound Web Scanner decision queue by creating an exact-host suppression rule. The request is bound to the reviewed run, and retrying the same idempotency key returns the recorded outcome without repeating the write. Requires API-key scope or current OAuth user permission: webScanner:update

Request URL: `POST https://app.oursprivacy.com/rest/v1/web-scanners/{id}/resolve-coverage-gap`

Authentication: apiKey or oauth2.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes |  |

### Request body (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `runDate` | string (date-time) | Yes |  |
| `runRevision` | string | Yes |  |
| `coverageRevision` | string | Yes |  |
| `hostname` | string | Yes |  |
| `idempotencyKey` | string (uuid) | Yes |  |
| `suppression` | object | Yes |  |
| `suppression.reason` | string | Yes | One of: `"ignore"`, `"baa"`, `"internal"`, `"approved"`, `"compliant"`, `"firstParty"`, `"other"`. |
| `suppression.notes` | string \| null | No |  |

### Responses

- `200`: Recorded outcome for the exact-host suppression decision
- `400`: Bad request — invalid body, or `runDate` is a day with no run.
- `401`: Unauthorized — missing, invalid, or expired API key or OAuth access token
- `403`: Forbidden — credential does not have the required current Ours permission
- `404`: Not Found
- `409`: Conflict — the reviewed evidence changed, the idempotency key was reused with different input, or another decision conflicts with this exact hostname.
- `500`: Internal Server Error

#### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | Yes | One of: `"already_resolved"`, `"applied"`, `"replayed"`. |
| `hostname` | string | Yes |  |
| `effectState` | string | Yes | One of: `"preexisting_coverage"`, `"suppression_active"`. |
| `coverageSource` | string | Yes | One of: `"cmp"`, `"suppression"`. |
| `suppressionRuleId` | string \| null | No |  |
| `coveredByVendorLabel` | string \| null | No |  |
| `committedCoverageRevision` | string | Yes |  |

Full schema: [OpenAPI specification](https://docs.oursprivacy.com/openapi-platform.json).
