---
title: "Preview what the PII layer would do to texts (nothing is stored)"
method: POST
path: "/pii/preview"
tags: ["pii"]
---

# Preview what the PII layer would do to texts (nothing is stored)

`POST /pii/preview`

Run the fact-level PII layer on up to 50 texts with a `pii` lever (inline, from a stored profile, or the deployment default) and return each text anonymised, the masked entities with their offsets in the original, the coverage per language, and the text units the call cost. In `block` mode a text that carries PII is reported as `blocked` (an ingestion would drop that fact) and still shown masked. **Nothing is stored and no text is logged**; the only durable trace is the usage event (`operation=pii_preview`).

A workspace that is not entitled to the layer gets the floor only (regex, structured types) and a `notice` saying so.

**Limits:** 20000 characters in total (413 `error`), 60 requests per minute per integrator (429 `detail`, `scope: pii_preview`), 8 concurrent previews per API process (429 `error` `PII_PREVIEW_CONCURRENCY_LIMIT` with `Retry-After`), and the workspace's monthly token quota (429 `detail` `TOKEN_QUOTA_EXCEEDED`): a scan's text units are converted to an approximate token cost, so the layer and the LLM paths share one budget. Only a run on a metered detector is gated — one that runs in-process (`regex`) makes no external call, costs no tokens and always answers.

Validation errors, including `PROFILE_NOT_FOUND` and both `pii` and `profile_id`, answer 422 `detail`.

## Request body

- PiiPreviewRequest — JSON body of ``POST /pii/preview``: run the PII layer on texts, persist nothing.
  - `texts` string[], required — Texts to scan (a fact each, typically). At most 50.
  - `pii` PiiLevers — The ``pii`` lever of an extraction profile. ``extra='forbid'`` like the profile body: a key this release does not apply is a 422, never silently ignored (decision D2).
    - `mode` 'off' | 'anonymize' | 'block' — Default 'anonymize' when the object is sent without it (a null lever follows the deployment default instead). 'anonymize' keeps every fact and masks the PII in it (structured entities flat, contextual ones with a stable per-workspace pseudonym); 'block' drops any fact that carries PII; 'off' applies only the deployment floor.
    - `contextual_entities` string[], nullable — Which context-detected entity types to act on: NAME, ADDRESS, AGE, USERNAME. null = all of them; [] = none (structured entities only). Structured entities follow `structured_entities` (all by default); only the floor types (secrets and payment data) are in scope unconditionally when mode is not 'off'.
    - `structured_entities` string[], nullable — Which format-detected entity types to act on (emails, phones, cards, IBANs, national ids, credentials...). null = all of them. Lets an integrator whose facts carry 9-digit revenue figures or version numbers switch PHONE or IP_ADDRESS off. The floor types (secrets and payment data) are masked regardless of this list.
    - `reversible` boolean — Reserved: keep the pseudonym-to-value mapping in a restricted vault so an authorised role can reverse it. Not available yet; only false is accepted.
  - `profile_id` string, uuid, nullable — A stored profile of this workspace whose `pii` lever to apply (422 PROFILE_NOT_FOUND).
  - `language` string, nullable — ISO 639-1 code of the texts, one of the extraction languages (422 otherwise). null = detect (es/en heuristic; else unknown).
  - `include_spans` boolean — Return the offsets of every masked entity in the original text.

## Response `200`

Per-text results and the run summary; nothing was stored

- PiiPreviewResponse — What the PII layer would do to each text. Nothing is stored; the only durable trace is the usage event (``operation=pii_preview``).
  - `results` PiiPreviewTextResult[], required
    - `anonymized` string, required
    - `entities` PiiEntitySpan[]
      - `type` string, required
      - `token` string, required — The placeholder that replaced it, e.g. {NAME:7f3a9c1d2e4b}
      - `start` integer, required
      - `end` integer, required
      - `source` 'model' | 'regex', required
    - `coverage` 'full' | 'partial' | 'none', required
    - `blocked` boolean — In `block` mode: this text carries PII, so an ingestion would drop the fact.
  - `summary` PiiRunSummary, required — What the PII layer did across one extraction run (job or preview).
    - `mode` 'off' | 'anonymize' | 'block', required
    - `provider` string, required
    - `provider_version` string, nullable — Detector configuration that ran (guardrail id:version, pattern-set tag).
    - `entitlement` 'premium' | 'floor', required
    - `facts_scanned` integer
    - `facts_anonymized` integer
    - `facts_blocked` integer
    - `by_type` object
    - `coverage` 'full' | 'partial' | 'none'
    - `language` string, nullable
    - `units` integer — Billable text units (1 unit = 1000 chars)
    - `free_units` integer
    - `chars` integer — Characters sent to the detector
    - `processing_ms` integer
  - `persisted` false
  - `notice` string, nullable — Set when the request asked for more than the workspace may run: the playground then ran the floor only (regex, structured types) and says so.
  - `limits` PiiPreviewLimits
    - `max_texts` integer
    - `max_chars` integer
    - `requests_per_minute` integer
    - `max_concurrent_per_process` integer

## Other responses

- `413` — Total text over the cap (`error.code` = `PII_PREVIEW_CONTENT_TOO_LARGE`)
- `422` — Validation error (`detail`), including `PROFILE_NOT_FOUND` or both `pii` and `profile_id`
- `429` — Route rate limit (`detail`, `scope: pii_preview`), `TOKEN_QUOTA_EXCEEDED` (`detail`, monthly quota spent) or `PII_PREVIEW_CONCURRENCY_LIMIT` (`error`, with `Retry-After`)
- `499` — `CLIENT_DISCONNECTED` (`error`): the caller had already gone when the scan was about to start, so nothing was scanned or billed. Nobody receives this body; it is here so the code is documented
- `503` — `PII_DETECTOR_UNAVAILABLE` (`error`); `PII_ENTITLEMENT_UNAVAILABLE` (`error`, retryable) when the workspace's features could not be read

## Changes

- **2026-09-15** `a61a1ffdc84f` — 1 info
  - added the non-success response with the status `499`
- **2026-09-14** `a3feb62260cf` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/scipot/apis/scipot-core-api/changes/pii/preview/post.md)

---

[API](https://skmtc.dev/scipot/apis/scipot-core-api.md) · [All operations](https://skmtc.dev/scipot/apis/scipot-core-api/llms.txt) · [OpenAPI document](https://skmtc.dev/scipot/apis/scipot-core-api/revisions/f23067b72e75?raw)
