---
title: "Scan For Phd"
method: POST
path: "/v1/data-editing/scan-phd"
tags: ["data-editing"]
---

# Scan For Phd

`POST /v1/data-editing/scan-phd`

## Request

- Base URL: `https://api.pioneer.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.pioneer.ai/v1/data-editing/scan-phd`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer
- TypeScript: <https://skmtc.dev/pioneer/apis/brain-api/docs/v1/data-editing/scan-phd/post.ts> (a function with typed inputs; its response is checked against the schema)

Scan dataset columns for Prompt Hack/Injection attempts (PHD).

Uses specialized model to detect prompt injection attempts.

Args:
    request.dataset_name: Dataset name.
    request.version: Optional version number. Returns latest if omitted.

## Request body

- DataEditingScanRequest: Request to scan dataset for PII or PHD
  - `dataset_name` string, required: Dataset name
  - `version` string, nullable: Version number (latest if omitted)
  - `columns` string[], nullable: Columns to scan. If not specified, scans all string columns.
  - `threshold` number: Detection threshold

## Response `200`

Successful Response

- DataEditingScanResponse: Response from PII/PHD scanning. A scan has two detection mechanisms of very different quality -- the GLiNER model, and a three-pattern regex (or seven-keyword) sweep for cells the model could not read -- and until ENG-6775 the response was identical either way. `findings_count: 0` therefore could not distinguish "the model read every cell and this dataset is clean" from "the model was down, and three regexes found nothing", which is the ENG-6537 false clean one layer up: the sweep stops an *unread* dataset reading as clean, but a sweep that also finds nothing returns to a bare zero. Attributes: success: Always true; a scan that cannot run reports `scan_status` rather than a non-200, so the heuristic findings are not discarded. dataset_name: The scanned dataset. dataset_version: The scanned version. scan_type: Which scan ran. scan_status: How much of the dataset the model actually read. See `ScanStatus`. unscanned_cells: How many cells the model could not read. Text cells among them were covered by the heuristic sweep instead; non-text cells were covered by nothing, since the sweep skips them while the model would have read them as strings. total_cells: How many cells were in scope, so `unscanned_cells` has a denominator. Without it the magnitude is unrecoverable: 3 of 4 is a failed scan and 3 of 10,000 is a footnote, and both report `unscanned_cells: 3`. findings_count: Total findings, model-derived and heuristic combined. affected_rows: Distinct rows carrying at least one finding. findings: The findings themselves. These do not individually say which mechanism produced them -- that is ENG-6878. phases: Per-phase wall-clock cost, when measured.
  - `success` boolean, required
  - `dataset_name` string, required
  - `dataset_version` string, required
  - `scan_type` 'pii' | 'phd', required
  - `scan_status` 'scanned' | 'degraded' | 'unavailable', required
  - `unscanned_cells` integer, required
  - `total_cells` integer, required
  - `findings_count` integer, required
  - `affected_rows` integer, required
  - `findings` PIIFinding[], required
    - `row_index` integer, required
    - `column` string, required
    - `entity_type` string, required
    - `text` string, required
    - `start` integer, required
    - `end` integer, required
    - `score` number, required
  - `phases` ScanPhaseTimings: Wall-clock cost of each phase of a scan, in milliseconds. A scan reports one latency, and that number cannot answer the question operators actually ask of it: was the time spent reaching the model, or in the model. The two have opposite remedies -- a slow resolve/load is storage or query work, a slow scan is inference concurrency -- so a single figure sends every investigation back to guessing. ENG-6537 records a 3m25s scan with no attribution at all, and the at-scale stress budget (90s) is calibrated against a total that mixes both. The split also has a second consumer: the client-abort stress probe has to abandon a scan *after* the process-wide admission gate is engaged, which is exactly ``pre_scan_ms`` later. Before this it used a static 5s guess that nothing verified (ENG-6723). Attributes: resolve_ms: Resolving the dataset name to a row. load_ms: Reading the dataset's records out of storage. prepare_ms: Column selection/validation and materialising the frame. scan_ms: The scan itself, including any regex fallback sweep.
    - `resolve_ms` number, required
    - `load_ms` number, required
    - `prepare_ms` number, required
    - `scan_ms` number, required
    - `pre_scan_ms` number, required: Return everything that happens before the scan can start. Serialised rather than left to the caller: it is the figure both consumers want, and summing three fields by hand is how a consumer silently omits one when a phase is added. Returns: Milliseconds spent resolving, loading and preparing.

## Other responses

- `422`: Validation Error

## Changes

- **2026-09-24** `0e5e86e2bae5`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/pioneer/apis/brain-api/changes/v1/data-editing/scan-phd/post.md)

---

[API](https://skmtc.dev/pioneer/apis/brain-api.md) - [All operations](https://skmtc.dev/pioneer/apis/brain-api/llms.txt) - [OpenAPI document](https://skmtc.dev/pioneer/apis/brain-api/revisions/0e5e86e2bae5?raw)
