> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Get Masking Report

GET https://api.captain.dev/v2/jobs/{job_id}/pii

Get the PII masking report for a job that ran with `mask_pii: true`. Returns a signed download link (valid for up to 300 seconds) and an entity count for each report object; the masked values themselves are only in the downloaded objects. Reports are kept for 90 days after the job ran, then expire automatically (410). Requires a key with the `index` permission.

Reference: https://docs.captain.dev/reference/jobs/pii/get

## Request

### Path parameters

- `job_id` (string, required) — ID of the job whose masking report to act on, as returned by the indexing endpoint that started it.

## Response

### 200

Successful Response

- `job_id` (string, required)
- `state` (string, required) — Always `retained` on a 200.
- `entity_count` (integer, required) — Entities across all files.
- `files` (list of PiiReportFileV2, required)
- `expires_in_seconds` (integer, required) — Lifetime of every `url` in the response (up to 300).
- `incomplete` (boolean, optional, default: false) — `true` while the job is still running or reported failed files, so objects may still be missing.
- `truncated_objects` (boolean, optional, default: false) — `true` when only the first 500 of the job's report objects are listed.
- `engine` (enum, optional, nullable) — The engine the request selected: `kev`, `jev` or `presidio`. For jobs that used the earlier names: `captain-presidio` when every file was masked by Presidio, `captain-jev` otherwise. Null for jobs that predate engine reporting.
  - Allowed values: `kev`, `jev`, `presidio`, `captain-jev`, `captain-presidio`
- `fallback` (boolean, optional, nullable) — True when at least one file in the job was masked by a fallback engine. Null for jobs that predate fallback reporting.

## Errors

### 401 Unauthorized Error

Missing or invalid API key

- `error` (string, required) — Always `HTTP_ERROR`.
- `message` (string, required)
- `status_code` (integer, required)
- `path` (string, optional) — Request path.

### 403 Forbidden Error

`missing_permission`: the key lacks the `index` permission

- `code` (string, required) — Machine-readable reason, listed per status code below.
- `message` (string, required)
- `deleted_at` (string, optional, nullable) — Only on 410 `deleted`: when the report was destroyed (RFC3339).
- `path` (string, optional) — Request path.

### 404 Not Found Error

Job not found, or it belongs to another organization

- `error` (string, required) — Always `HTTP_ERROR`.
- `message` (string, required)
- `status_code` (integer, required)
- `path` (string, optional) — Request path.

### 409 Conflict Error

`not_masked`: the job did not run with `mask_pii`. `predates_feature`: the job was indexed before reports were kept. `report_unavailable`: no report objects exist yet

- `code` (string, required) — Machine-readable reason, listed per status code below.
- `message` (string, required)
- `deleted_at` (string, optional, nullable) — Only on 410 `deleted`: when the report was destroyed (RFC3339).
- `path` (string, optional) — Request path.

### 410 Gone Error

`deleted`: the report was deleted (`deleted_at` is included). `expired`: 90 days passed since the job ran and the report was removed automatically

- `code` (string, required) — Machine-readable reason, listed per status code below.
- `message` (string, required)
- `deleted_at` (string, optional, nullable) — Only on 410 `deleted`: when the report was destroyed (RFC3339).
- `path` (string, optional) — Request path.

### 503 Service Unavailable Error

`report_signer_unconfigured`, `registry_unavailable`, `report_store_unavailable`, or `audit_unavailable`. Retry

- `code` (string, required) — Machine-readable reason, listed per status code below.
- `message` (string, required)
- `deleted_at` (string, optional, nullable) — Only on 410 `deleted`: when the report was destroyed (RFC3339).
- `path` (string, optional) — Request path.

## Types

### PiiReportFileV2

- `file_id` (string, required)
- `entity_count` (integer, required) — Entities recorded for this file.
- `truncated` (boolean, required) — `true` when an object hit the 5000-entity cap.
- `objects` (list of PiiReportObjectV2, required)
- `requested_engine` (enum, optional, nullable) — The engine the request selected, `kev` when it was omitted. Null for jobs that used the earlier engine names.
  - Allowed values: `kev`, `jev`, `presidio`
- `used_engines` (list of enum, optional, nullable) — Engines whose masking output was kept for this file, in first-use order. Engines that were tried and failed are not listed.
  - Allowed values: `kev`, `jev`, `jev-openrouter`, `jev-ai-gateway`, `presidio`
- `fallback_used` (boolean, optional, nullable) — True when any part of the file was masked by a fallback engine.
- `fallback_reason` (enum, optional, nullable) — Why the first fallback happened: `rate_limited`, `timeout`, `capacity`, `provider_error`, `invalid_response` or `unavailable`. Null without a fallback.
  - Allowed values: `rate_limited`, `timeout`, `capacity`, `provider_error`, `invalid_response`, `unavailable`
- `coverage` (enum, optional, nullable) — `full_requested` when every requested masking capability ran, including custom fields and instructions. `text_full_images_built_in` when custom fields or instructions were requested and image pixels were masked with the built-in categories. `partial` when requested instructions could not be applied. Describes what ran, not a guarantee that nothing was missed.
  - Allowed values: `full_requested`, `text_full_images_built_in`, `partial`
- `model` (string, optional, nullable) — The model that answered, such as `jaredpalmer/kev-4b`. When the file was masked by more than one model, the value lists them comma-separated. Report objects record their own model.
- `image_redaction` (enum, optional, nullable) — `built_in` when image pixels were masked with the built-in categories. Null when the file had no images.
  - Allowed values: `built_in`

### PiiReportObjectV2

- `key_suffix` (string, required) — `doc.json` for the document, or `chunk-{n}.json` for one text chunk.
- `url` (string, required) — Signed download URL, valid for up to 300 seconds.
- `expires_at` (string, required) — When `url` stops working (RFC3339).

## Examples

**Response**

```json
{
  "job_id": "job_01HZP5G8N3",
  "state": "retained",
  "entity_count": 24,
  "files": [
    {
      "file_id": "f05d93c3da314ab4fa24077afc08bd04",
      "entity_count": 24,
      "truncated": false,
      "objects": [
        {
          "key_suffix": "doc.json",
          "url": "https://captain-pii-reports.s3.us-east-1.amazonaws.com/org_01HZP4/job_01HZP5G8N3/f05d93c3da314ab4fa24077afc08bd04/doc.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-Signature=3b9f0c2a1e",
          "expires_at": "2025-01-20T08:25:00Z"
        }
      ]
    }
  ],
  "expires_in_seconds": 300,
  "incomplete": false,
  "truncated_objects": false
}
```

**SDK Code**

```python Python REST
import requests
import json

BASE_URL = "https://api.captain.dev"
API_KEY = "your_api_key"
JOB_ID = "job_01HZP5G8N3"

headers = {
    "Authorization": f"Bearer {API_KEY}",
}

response = requests.get(
    f"{BASE_URL}/v2/jobs/{JOB_ID}/pii",
    headers=headers,
    timeout=30.0
)

if response.status_code == 200:
    data = response.json()
    print(f"{data['entity_count']} masked entities across {len(data['files'])} file(s)")
    for f in data["files"]:
        for obj in f["objects"]:
            report = requests.get(obj["url"], timeout=30.0).json()
            print(f"  {f['file_id']}/{obj['key_suffix']}: {report['entity_count']} entities")
elif response.status_code == 410:
    print(f"Report gone: {response.json()['code']}")
else:
    print(f"Error: {response.status_code} {response.json().get('code')}")
```

```typescript TypeScript REST
const BASE_URL = "https://api.captain.dev";
const API_KEY = "your_api_key";
const JOB_ID = "job_01HZP5G8N3";

const response = await fetch(`${BASE_URL}/v2/jobs/${JOB_ID}/pii`, {
    method: "GET",
    headers: { "Authorization": `Bearer ${API_KEY}` },
});

if (response.ok) {
    const data = await response.json();
    console.log(`${data.entity_count} masked entities across ${data.files.length} file(s)`);
    for (const f of data.files) {
        for (const obj of f.objects) {
            const report = await (await fetch(obj.url)).json();
            console.log(`  ${f.file_id}/${obj.key_suffix}: ${report.entity_count} entities`);
        }
    }
} else if (response.status === 410) {
    console.log(`Report gone: ${(await response.json()).code}`);
} else {
    console.error(`Error: ${response.status}`);
}
```