# The ShinrAI PII API v2

Find personal data in text, tables, JSON, transcripts, pages and images, protect it (pseudonymise, mask, label, remove, or fill image regions) and restore it later. The hosted API runs at `https://api.shinrai.innovius.io`, the sandbox at `https://api-sbx.shinrai.innovius.io` (test keys). The full OpenAPI 3.1 document is served at `/v2/openapi.json` and rendered in the [API explorer](https://shinrai.innovius.io/docs/api) (Swagger, tag "Native PII API v2"); the [developer guide](https://shinrai.innovius.io/docs/guides/v2) gives an overview. The v1 routes stay available. The Azure, Google and AWS compatibility APIs are for migration and run on their own hosts (`https://azure.api.shinrai.innovius.io`, `https://google.api.shinrai.innovius.io`, `https://aws.api.shinrai.innovius.io`) and under `/v1/azure`, `/v1/google` and `/v1/aws` on the API host; the vendor contract limits what they can return.

## Authentication and cost

Send your key as `Authorization: Bearer <key>`. Requests are charged in records of 1,000 characters per input: a text, a page, a transcript's primary form, a table or a JSON value (all its strings together) each cost at least one record. An image costs one record, plus the records of the text read from it above the first 1,000 characters. Tier weights: standard ×1, batch ×0.5, real-time ×1.6. `restore`, `restore-tables`, `replacements`, `capabilities`, `types`, `usage` and `sessions` are free. Charged calls answer with `X-Records-Charged` and `X-Records-Remaining`; every answer has `X-Request-Id`. A failed call and a result whose model layer did not run (`engine.degraded: true`) are not charged.

Retries: send an `Idempotency-Key` header (at most 128 characters). A repeat with the same key and body is answered again and charged once (`X-Records-Charged: 0`); the same key with another body answers 409 `idempotency_conflict`.

## Cookbook

Set your key once:

```bash
export SHINRAI_API_KEY=shr_live_...
```

What this deployment serves (models, languages, input kinds, tiers your plan allows, limits):

```bash
curl -s https://api.shinrai.innovius.io/v2/capabilities -H "Authorization: Bearer $SHINRAI_API_KEY"
```

Find personal data in a text:

```bash
curl -s https://api.shinrai.innovius.io/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" -d '{"text": "Anna Weber, anna.weber@example.org, IBAN DE89 3704 0044 0532 0130 00"}'
```

Send a text file as it is and get the protected text back:

```bash
curl -s https://api.shinrai.innovius.io/v2/protect?preset=label -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: text/plain" -H "Accept: text/plain" --data-binary @letter.txt
```

Find personal data in a screenshot or a scan (pixel boxes per entity):

```bash
curl -s "https://api.shinrai.innovius.io/v2/detect?language=de" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: image/png" --data-binary @screenshot.png
```

Get the redacted image back (filled black):

```bash
curl -s https://api.shinrai.innovius.io/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: image/png" -H "Accept: image/png" --data-binary @screenshot.png -o redacted.png
```

Pseudonymise and keep the mapping, so you can restore an answer later:

```bash
curl -s https://api.shinrai.innovius.io/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber lives in Darmstadt.", "policy": {"preset": "pseudonymize"}, "output": {"include": ["entities", "mapping"]}}'
```

Restore a text that contains the surrogates (send the `mapping.delta` entries as `original`/`replacement` pairs):

```bash
curl -s https://api.shinrai.innovius.io/v2/restore -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mapping": {"known": [{"original": "Anna Weber", "replacement": "Julia Brandt"}]}, "inputs": [{"id": "1", "text": "Julia Brandt replied."}]}'
```

Keep one map across many requests with a session (at most 24 hours), then export it:

```bash
SESSION=$(curl -s -X POST https://api.shinrai.innovius.io/v2/sessions -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" -d '{"ttl_s": 3600}' | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
curl -s https://api.shinrai.innovius.io/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" -d '{"text": "Anna Weber called.", "mapping": {"session": "'$SESSION'"}}'
curl -s https://api.shinrai.innovius.io/v2/sessions/$SESSION/mapping -H "Authorization: Bearer $SHINRAI_API_KEY"
```

Keep the e-mail domain and the last four digits of a card, generalize names and places:

```bash
curl -s https://api.shinrai.innovius.io/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber aus Biberach, anna@example.org, Karte 4111 1111 1111 1111", "language": "de",
       "policy": {"default": {"action": "generalize"}, "rules": [{"types": ["EMAIL", "CREDIT_CARD"], "action": "partial"}]}}'
```

Your balance and the last 30 days:

```bash
curl -s https://api.shinrai.innovius.io/v2/usage -H "Authorization: Bearer $SHINRAI_API_KEY"
```

Raw bodies (`--data-binary`) accept `text/plain` and `image/png`, `image/jpeg`, `image/bmp`, `image/tiff`, `image/webp` up to 6 MiB; options go into the query string: `language`, `model`, `types` and `exclude` (comma-separated type names), `tier`, `on_degraded`, `granularity` (`line` or `word` boxes), and `preset` on protect.

## JSON requests

The shortest body is `{"text": "..."}`; `{"texts": ["...", "..."]}` sends several texts (ids `"1"`, `"2"`, ...). The full form is a list of `inputs`, each with a `kind`: `text`, `table` (`columns`, `rows`), `json` (`value`), `transcript` (`forms`, word `atoms` with times), `page` (text plus word boxes from your own OCR or PDF text layer) or `image` (`media_type`, `data_b64`). The `id` of an input is optional; results come back with `input_id`.

Useful options:

- `detection.language`: a BCP 47 tag, or `auto` (default).
- `detection.model`: `latest` (default) or a version from `capabilities.models`.
- `detection.types.include` / `exclude`: canonical type names from `GET /v2/types`, or a vendor's names with `detection.types.vocabulary` (`google`, `aws`, `azure:<version>`, `presidio`); entities then carry `vendor_type`.
- `detection.thresholds.default`: the confidence floor (the model's served floor by default); `per_type` and `per_language` set a floor per type or language, below the default too.
- `detection.exclude_values`: values that are never reported (for example your own company name); `detection.custom.user_values`: your own values, with an optional `type`.
- `detection.spans.segment`: how long texts are read: `auto` (default), `sentence` or `none`.
- `policy.preset`: `pseudonymize` (realistic surrogates, reversible), `mask`, `label` (`[PERSON_1]`, reversible per value), `strict`; `policy.rules` sets an action per type: `surrogate`, `label`, `mask` (`char`, `keep_first`, `keep_last`, or `count` with `reverse`), `partial`, `generalize`, `replace`, `remove`, `keep`.
- `partial` keeps what does not identify: the e-mail domain, the phone country prefix, the last four digits of a card or account, the year of a date. `generalize` writes a phrase for the kind of name, place or organisation ("a small town", "eine regionale Firma") in the input language. Both are one-way.
- `output.include`: `entities` (default), `annotations`, `linkage_risk`, `mapping`, `restore_table`, `redaction_plan`, `stats`; `output.include_text: true` echoes entity texts; `output.max_entities.per_input` shortens the entity list (protection still covers all).
- `mapping.session`: the id of a session from `POST /v2/sessions`; the session's map is used and extended.
- `mapping.consistency`: `"account"` keeps a value's surrogate across all requests of your account (opt-in and weaker, see below). Without it, every request draws new surrogates.
- `processing.tier`: `standard` (default), `realtime` (small inputs, low latency, plans from Team) or `batch` (half price, lowest priority).

The response has `results[]` (per input: `entities`, the protected `output`, `media` for images, and on request `annotations` and `linkage_risk`), `engine` (the model used, the detection layers that ran, `degraded`) and `usage`. The mapping (originals and their replacements) is returned only when you ask for it with `output.include: ["mapping"]`.

Within one request a value keeps one surrogate. The next request draws new surrogates, so repeated requests cannot be used to map surrogates back to originals. For the same surrogates across requests, use a session (`mapping.session`) or send the earlier pairs in `mapping.known`. `mapping.consistency: "account"` keeps a value's surrogate across all requests of your account. This is opt-in and weaker: anyone with the key can then build a table of originals and surrogates by repetition. Other customers always get different surrogates.

`entities` holds personal data only. Years, amounts, legal references and bias terms come back in `annotations` when you ask for them, and protect never changes them. `linkage_risk` estimates how likely an input singles out a person from rare names and places, direct identifiers and cues such as age or job title: `level` (`low`, `medium`, `high`), `k_estimate` and the `signals`. It is a heuristic, not a count.

## Sessions

A session holds one map of originals and replacements on the server for at most 24 hours (`ttl_s`, default one hour). `POST /v2/sessions` creates one (optionally with `known` pairs); `mapping.session` on detect or protect uses and extends it; `restore` and `restore-tables` accept `"session"` instead of a mapping. `GET /v2/sessions/{id}` shows its size and expiry, `PATCH` adds `known` pairs or `reserved` replacements or changes `ttl_s` (send `expected_rev` to detect concurrent changes), `DELETE` removes it, `GET /v2/sessions/{id}/mapping` exports the map (it contains originals) and `GET /v2/sessions/{id}/restore-table` returns its restore table. The map is stored encrypted and only your key can read it.

## Images

The packaged OCR reads every language the model serves (see `capabilities.ocr.languages`): send `language` so it reads that script plus English, which Arabic, Hebrew, Japanese and Korean images need; without it, OCR reads German and English; every entity comes back with boxes in pixels of the image you sent: `coords.boxes[]` as `{page: 1, box: [x, y, width, height]}`, one box per text line (`granularity=word` or `output.box_granularity: word` gives one per word). `results[].redaction_plan.boxes[]` lists every box with its entity. `/v2/protect` returns the filled image; `policy.media.image` sets `color`, `pad_px` (default 2) and `all_text: true` to cover every word. Images are accepted up to `capabilities.ocr.max_pixels` and 6 MiB on the standard and batch tiers; the realtime tier takes one image per request up to 4.2 megapixels (a 2560 × 1600 screenshot) and 3 MiB.

## Jobs: large batches and documents (beta)

Use a job when the work is too large for one request: many texts, or a PDF or Word file.
A job runs in the background at the batch weight (0.5 of standard) and keeps its results
for 24 hours.

### 1. Upload the input

Put one JSON object per line. Each line has a `custom_id` and either `text` or `input`
(a `text`, `table` or `json` input). `language` is optional.

```jsonl
{"custom_id": "row-1", "text": "Anna Schmidt, anna@example.com"}
{"custom_id": "row-2", "text": "Call +49 30 1234567", "language": "de"}
{"custom_id": "row-3", "input": {"kind": "table", "columns": [{"name": "email"}], "rows": [["max@example.org"]]}}
```

```bash
curl -s https://api.shinrai.innovius.io/v2/uploads \
  -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @rows.jsonl
```

The answer names the upload: `{"id": "up_...", "bytes": ..., "sha256": "...", "expires_at": "..."}`.

### 2. Start the job

```bash
curl -s https://api.shinrai.innovius.io/v2/jobs \
  -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rows-2026-09-28" \
  -d '{"kind": "text_batch",
       "inputs": [{"kind": "file", "source": {"upload": "up_..."}}],
       "output": {"artifacts": ["protected", "entities"]}}'
```

The answer is `202` with the job and a `Location` header. The service checks every line
before it accepts the job; an error names the line (`/lines/2/custom_id`). A retry with the
same `Idempotency-Key` and body returns the same job and is not charged again.

For a few hundred texts you can skip the upload and send them inline:
`"inputs": [{"id": "a", "kind": "text", "text": "..."}, ...]`.

For a document, upload the PDF or DOCX file with its content type and start
`{"kind": "document", "inputs": [{"kind": "file", "source": {"upload": "up_..."}}]}`.
The job protects the text of the document. The redacted PDF and word boxes
(`redaction_plan`) come in a later release.

### 3. Poll and download

```bash
curl -s https://api.shinrai.innovius.io/v2/jobs/$JOB -H "Authorization: Bearer $SHINRAI_API_KEY"
```

`status` goes `queued` → `running` → `succeeded` (or `failed`, `cancelled`); `progress`
shows done and total lines. When the job succeeded, `artifacts` lists the downloads:

```bash
curl -s https://api.shinrai.innovius.io/v2/jobs/$JOB/artifacts/protected \
  -H "Authorization: Bearer $SHINRAI_API_KEY" -o protected.jsonl
```

| Artifact | Text batch | Document |
|---|---|---|
| `protected` | JSONL, one line per input: `custom_id`, `status`, `output`, `entities` (or `error`) | the protected text |
| `entities` | JSONL: `custom_id`, `status`, `entities` | JSON: entities with offsets into the extracted text |
| `mapping` | JSONL: the replacements the job made (contains originals) | JSON: the same |

Ask for `mapping` only when you need to restore; it contains the original values. A job
without `protected` or `mapping` only detects.

### Cancel and delete

- `POST /v2/jobs/{id}/cancel` stops the job. Finished chunks stay charged; the results are deleted.
- `DELETE /v2/jobs/{id}` deletes the job, its uploads and its results at once.
- Everything is deleted automatically after 24 hours.

### Billing and limits

- Every line counts as one input: at least one record per started 1,000 characters, at the
  batch weight 0.5. A document is charged on the characters of its extracted text.
- The balance must cover the whole batch when you submit it (`402 insufficient_records`).
- Degraded chunks and lines the service refuses as invalid are not charged. With `processing.on_degraded: "allow"`
  degraded lines carry `"degraded": true`.
- `GET /v2/capabilities` shows the limits of your deployment (`jobs_upload_max_bytes`,
  `jobs_text_batch_max_lines`, `jobs_document_max_bytes`, `jobs_retention_s`).
- Jobs, uploads and results belong to your account. Other accounts get 404.

## Types

`GET /v2/types` lists the types this deployment returns, with a description, a group and `personal` (false for years, amounts, legal references and bias terms: those come back as `annotations`), plus the vendor vocabularies. Card numbers, US social security numbers and German tax ids found by pattern must pass their check digits. Types that come with a newer model show `since`.

## Errors

Every error is `{"error": {"code", "message", "request_id", "retryable", ...}}`; validation errors add `details[].pointer` (a JSON Pointer, never your data). Common codes: 401 `invalid_key`, 402 `insufficient_records`, 403 `tier_not_allowed`, 409 `idempotency_conflict` or `rev_conflict`, 410 `session_expired`, 413 `too_large`, 422 `validation_failed`, 429 `rate_limited`, 501 `capability_unavailable` (an option or input kind this deployment does not serve yet), 503 `degraded_refused` or `backend_unavailable`, 504 `processing_timeout`. Retry only when `retryable` is true.

## Client example (Python)

```python
import httpx

api = httpx.Client(base_url="https://api.shinrai.innovius.io", headers={"Authorization": f"Bearer {KEY}"}, timeout=120)
with open("screenshot.png", "rb") as image:
    answer = api.post("/v2/detect", content=image.read(), headers={"Content-Type": "image/png"}).json()
for entity in answer["results"][0]["entities"]:
    for box in entity.get("coords", {}).get("boxes", []):
        x, y, w, h = box["box"]  # pixels of the image you sent
        print(entity["type"], x, y, w, h)
```
