# The ShinrAI PII API v2

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

API version 2.0.0 is stable. Changes within 2.x are additive: new fields, parameters, values and routes. A breaking change gets a new major version with a new path prefix. We announce it 12 months ahead, and the previous major version stays served during that period. Ignore response fields and values that you do not know. `api_version` in `GET /v2/capabilities` and in every detect and protect result names the version. The offline edition serves the native API v1 until its image includes v2.

If your client was written for the pre-release 2.0.0-draft.4, read the list of changes in the [changelog](https://getshinrai.com/public-docs/changelog.md) ("API 2.0.0 stable").

## Features at a glance

Every feature has a one-line explanation and a minimal request. The sections below give the details.

Set your key once:

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

### Inputs

#### Text
Find personal data in a text. Every entity comes back with its type, position and confidence.

```bash
curl -s https://api.goshinrai.com/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"}'
```

#### Several texts
Send up to 256 texts in one request. One request keeps one replacement map for all of them.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/json" -d '{"texts": ["Anna Weber called.", "Call Anna Weber back at +49 30 1234567."]}'
```

#### Text files
Send a text file as it is and get the protected text back.

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

#### Tables
Protect rows and columns. Every entity names its row and column.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"inputs": [{"kind": "table", "columns": [{"name": "name"}, {"name": "email"}], "rows": [["Anna Weber", "anna@example.org"]]}]}'
```

#### JSON
Protect every string in a JSON value, for example a tool call. Every entity carries a JSON Pointer to its string.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"inputs": [{"kind": "json", "value": {"customer": {"name": "Anna Weber", "email": "anna@example.org"}}}]}'
```

#### Transcripts
Send a transcript with word times. Every entity comes back with the times of its words.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"inputs": [{"kind": "transcript", "forms": {"display": "Call Anna Weber"}, "atoms_form": "display", "time_unit": "ms",
       "atoms": [{"text": "Call", "t0": 0, "t1": 300}, {"text": "Anna", "t0": 350, "t1": 600}, {"text": "Weber", "t0": 600, "t1": 950}]}]}'
```

#### Pages
Send the text of a page with the word boxes from your own OCR or PDF text layer. Every entity comes back with its boxes.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"inputs": [{"kind": "page", "text": "Anna Weber", "box_unit": "px",
       "atoms": [{"start": 0, "end": 4, "page": 1, "box": [10, 20, 40, 12]}, {"start": 5, "end": 10, "page": 1, "box": [54, 20, 50, 12]}]}]}'
```

#### Images
Find personal data in a screenshot or a scan. The OCR reads 14 languages, and every entity comes back with pixel boxes.

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

#### Redacted images
Get the redacted image back, with every entity filled black.

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

#### Audio
Send a recording of up to 5 minutes and get it back with every personal detail bleeped.

```bash
curl -sS "https://api.goshinrai.com/v2/protect?language=de" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" -H "Accept: audio/wav" --data-binary @call.mp3 -o call.redacted.wav
```

#### Audio transcripts
Get the transcript of a recording and the times of every entity, without the audio.

```bash
curl -sS "https://api.goshinrai.com/v2/detect?language=de" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" --data-binary @call.mp3
```

### Detection

#### Language and model
Set the language for the best results, and pin a model version when you need the same results over time.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber wohnt in Darmstadt.", "detection": {"language": "de", "model": "latest"}}'
```

#### Types
Include or exclude types by their ShinrAI names or by the names of Google, AWS, Azure or Presidio.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber, anna@example.org, +49 30 1234567", "detection": {"types": {"include": ["EMAIL_ADDRESS", "PHONE_NUMBER"], "vocabulary": "google"}}}'
```

#### Confidence floors
Set a confidence floor for all types, per type or per language.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber, Darmstadt", "detection": {"thresholds": {"default": 0.5, "per_type": {"CITY": 0.8}}}}'
```

#### Ignored values and your own values
Never report values such as your company name, and find values of your own with a type.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Innovius support: case K-4711 for Anna Weber", "detection": {"exclude_values": {"values": ["Innovius"]},
       "custom": {"user_values": [{"value": "K-4711", "type": "CUSTOMER_ID"}]}}}'
```

#### Your own spans
Protect the spans that your own detector found, alone or together with the ShinrAI detection.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"inputs": [{"kind": "text", "text": "Ticket for Anna Weber", "entities": [{"type": "PERSON", "span": {"start": 11, "end": 21}}]}],
       "detection": {"mode": "provided"}}'
```

#### Long texts
Choose how the model reads a long text: automatic, sentence by sentence, or as one piece.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber called. She lives in Darmstadt.", "detection": {"spans": {"segment": "sentence"}}}'
```

### Protection

#### Pseudonymisation
Pseudonymise and keep the mapping, so you can restore an answer later.

```bash
curl -s https://api.goshinrai.com/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"]}}'
```

#### Labels and masks
Replace every value with a numbered label such as [PERSON_1], or mask it with a character.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber, anna@example.org", "policy": {"preset": "label", "rules": [{"types": ["EMAIL"], "action": "mask", "mask": {"char": "*"}}]}}'
```

#### Partial and generalised values
Keep the e-mail domain and the last four digits of a card, generalize names and places.

```bash
curl -s https://api.goshinrai.com/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"}]}}'
```

#### Rules per type
Choose an action per type: replace with a fixed text, remove or keep.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber from Darmstadt, +49 30 1234567, anna@example.org", "policy": {"preset": "pseudonymize",
       "rules": [{"types": ["PHONE"], "action": "replace", "replace": {"value": "[phone]"}}, {"types": ["EMAIL"], "action": "remove"},
                 {"types": ["CITY"], "action": "keep"}]}}'
```

### Outputs

#### Annotations
Get years, amounts, legal references and bias terms as annotations. Protect never changes them.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "In 2019 Anna Weber paid 1,200 EUR.", "output": {"include": ["entities", "annotations"]}}'
```

#### Linkage risk
Estimate how likely a text singles out a person. It is a heuristic, not a count.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "The 34-year-old head surgeon from Biberach joined in 2019.", "output": {"include": ["entities", "linkage_risk"]}}'
```

#### Offsets, texts and statistics
Get positions in UTF-16 or UTF-8, the entity texts, statistics and a shorter entity list.

```bash
curl -s https://api.goshinrai.com/v2/detect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber, anna@example.org", "output": {"offset_unit": "utf16", "include_text": true, "include": ["entities", "stats"], "max_entities": {"per_input": 10}}}'
```

### Restore and sessions

#### Restore
Restore a text that contains the surrogates. Send the mapping.delta entries as original and replacement pairs.

```bash
curl -s https://api.goshinrai.com/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."}]}'
```

#### Restore tables
Compile a mapping into a restore table and restore in your own code, for example in a streamed model answer.

```bash
curl -s https://api.goshinrai.com/v2/restore-tables -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"mapping": {"known": [{"original": "Anna Weber", "replacement": "Julia Brandt"}]}}'
```

#### One replacement
Get one replacement for a value and type that you choose.

```bash
curl -s https://api.goshinrai.com/v2/replacements -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"value": "Anna Weber", "type": "PERSON", "language": "de"}'
```

#### Sessions
Keep one map across many requests with a session (24 hours from its creation by default), then export it.

```bash
SESSION=$(curl -s -X POST https://api.goshinrai.com/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.goshinrai.com/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.goshinrai.com/v2/sessions/$SESSION/mapping -H "Authorization: Bearer $SHINRAI_API_KEY"
```

#### Known pairs
Give earlier pairs to a new request, so the same values keep the same surrogates.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Anna Weber called again.", "mapping": {"known": [{"original": "Anna Weber", "replacement": "Julia Brandt"}]}}'
```

### Jobs

#### Text batches
Protect up to 20,000 texts from a JSONL file in the background, at half price.

```bash
UPLOAD=$(curl -s https://api.goshinrai.com/v2/uploads -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/x-ndjson" --data-binary @rows.jsonl | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
curl -s https://api.goshinrai.com/v2/jobs -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind": "text_batch", "inputs": [{"kind": "file", "source": {"upload": "'$UPLOAD'"}}]}'
```

#### Documents
Get a PDF or Word file back as a redacted PDF, together with its protected text.

```bash
UPLOAD=$(curl -s https://api.goshinrai.com/v2/uploads -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/pdf" --data-binary @contract.pdf | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
curl -s https://api.goshinrai.com/v2/jobs -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind": "document", "inputs": [{"kind": "file", "source": {"upload": "'$UPLOAD'"}}]}'
```

#### Long recordings
Bleep a recording of up to 60 minutes in the background.

```bash
UPLOAD=$(curl -s https://api.goshinrai.com/v2/uploads -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" --data-binary @meeting.mp3 | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
curl -s https://api.goshinrai.com/v2/jobs -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind": "audio", "inputs": [{"kind": "audio", "source": {"upload": "'$UPLOAD'"}, "language": "de"}]}'
```

### Tiers, retries and account

#### Tiers
Choose realtime for small inputs with low latency, or batch for half price.

```bash
curl -s "https://api.goshinrai.com/v2/detect?tier=realtime" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: text/plain" --data-binary 'Call Anna Weber at +49 30 1234567.'
```

#### Safe retries
Retry with the same Idempotency-Key. The service charges the request once.

```bash
curl -s https://api.goshinrai.com/v2/protect -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Idempotency-Key: order-4711" \
  -H "Content-Type: application/json" -d '{"text": "Anna Weber, order 4711"}'
```

#### Capabilities
What this deployment serves: models, languages, input kinds, tiers your plan allows and limits.

```bash
curl -s https://api.goshinrai.com/v2/capabilities -H "Authorization: Bearer $SHINRAI_API_KEY"
```

#### Types list
List every type with its description and the names of Google, AWS, Azure and Presidio.

```bash
curl -s https://api.goshinrai.com/v2/types -H "Authorization: Bearer $SHINRAI_API_KEY"
```

#### Usage
Your balance and the last 30 days.

```bash
curl -s https://api.goshinrai.com/v2/usage -H "Authorization: Bearer $SHINRAI_API_KEY"
```

#### OpenAPI
Get the full OpenAPI 3.1 document of the v2 API.

```bash
curl -s https://api.goshinrai.com/v2/openapi.json -o shinrai-pii-api-v2.json
```

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

## 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. Audio costs the records of its transcript, at least 10 records per started minute. 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`.

## 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`.
- `detection.mode`: `detect` (default), `provided` (only your spans in `inputs[].entities`, no model call) or `merge` (both).
- `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.offset_unit`: `codepoint` (default), `utf16`, `utf8` or `grapheme`; `extra_offset_units` adds more.
- `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. `POST /v2/sessions` creates one (optionally with `known` pairs), and `ttl_s` sets when it expires (default one hour); `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.

A session lives at most 24 hours from its creation; the session shows the end in `max_expires_at`. With the extended-sessions setting of your account, sessions created after the change live up to 7 days. Contact support to switch the setting on. `limits.sessions_max_lifetime_s` in `GET /v2/capabilities` shows the maximum for your key. `expires_at` never passes `max_expires_at`: on create, a longer `ttl_s` is shortened; a PATCH with a `ttl_s` beyond it answers 422 `validation_failed` (pointer `/ttl_s`) with `max_expires_at` and `max_ttl_s`. The first request to an expired session answers 410 `session_expired` and deletes the session; later requests answer 404. An expired session that nobody reads is deleted one hour after its expiry.

## 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.

## Audio

Send a recording as the body of `/v2/protect` or `/v2/detect` on the standard tier: up to 5 minutes and 12 MiB. Use a compressed format (MP3, Opus, AAC) or 16 kHz mono WAV to stay below 12 MiB. Accepted types: `audio/wav`, `audio/mpeg`, `audio/ogg` (`audio/opus`), `audio/flac`, `audio/mp4` (`audio/m4a`), `audio/aac`, `audio/webm`. Longer or larger recordings run as a job (below). `GET /v2/capabilities` shows `inputs.audio` and the limits `audio_standard_max_seconds`, `audio_standard_max_bytes`, `audio_jobs_max_seconds` and `audio_jobs_max_bytes`.

Bleep every personal detail and get the recording back as WAV:

```bash
curl -sS "https://api.goshinrai.com/v2/protect?language=de" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" -H "Accept: audio/wav" --data-binary @call.mp3 -o call.redacted.wav
```

Get the protected transcript and the times of every entity instead of audio:

```bash
curl -sS "https://api.goshinrai.com/v2/protect?language=de" -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" -H "Accept: application/json" --data-binary @call.mp3
```

`results[0].output.display` is the protected transcript. Every entity has `coords.intervals` (`t0` and `t1` in milliseconds), and `results[0].redaction_plan.intervals` lists every interval the WAV mutes. `/v2/detect` answers the same JSON without protection and puts the recognised transcript in `results[0].text`.

- Options are query parameters: `language` (recommended; without it the speech recognition detects the language), `types`, `exclude`, `preset`, `on_degraded`, `audio_op` (`bleep`, the default 1 kHz tone, or `silence`) and `pad_ms` (0 to 1000, default 150; it widens every muted interval).
- The WAV is 16-bit PCM at the sample rate of the source (at most 48 kHz). The answer headers `Shinrai-Audio-Seconds`, `Shinrai-Audio-Language` and `Shinrai-Entities` summarise the call.
- Cost: the records of the transcript, at least 10 records per started minute (a 61-second call with a short transcript costs 20 records). A job costs the same at the batch weight ×0.5 (a 60-second recording at least 5 records). A call that fails is not charged. `Idempotency-Key` works as for text.
- One audio request per account runs at a time (429 otherwise). When the service is busy it answers 503 `queue_full` or `queue_timeout` with `Retry-After`; retry later or use a job. Audio runs on the standard tier only; `tier=realtime` answers 422. An unsupported audio type answers 415. A longer or larger recording answers 413 `too_large` with `use: /v2/jobs`.
- Limit: the protection reads the words that the speech recognition heard. A word that it mishears and that the model then does not recognise stays audible, and so does speech that it does not transcribe (for example under music or cross-talk). Listen to sensitive recordings before you share them.

Recordings up to 60 minutes run as a job at the batch weight. Upload the file with its content type, start a job of kind `audio`, repeat the status call until `status` is `succeeded`, then download the WAV:

```bash
UPLOAD=$(curl -sS https://api.goshinrai.com/v2/uploads -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: audio/mpeg" --data-binary @meeting.mp3 | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
JOB=$(curl -sS https://api.goshinrai.com/v2/jobs -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind": "audio", "inputs": [{"kind": "audio", "source": {"upload": "'$UPLOAD'"}, "language": "de"}]}' \
  | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
curl -sS https://api.goshinrai.com/v2/jobs/$JOB -H "Authorization: Bearer $SHINRAI_API_KEY"
curl -sS https://api.goshinrai.com/v2/jobs/$JOB/artifacts/protected -H "Authorization: Bearer $SHINRAI_API_KEY" -o meeting.redacted.wav
```

The artifacts are `protected` (the redacted WAV), `transcript` (JSON: language, duration and the protected transcript) and `entities` (JSON: the entities with `coords.intervals`). `policy.media.audio` in the job sets `op` and `pad_ms`, for example `"policy": {"media": {"audio": {"op": "silence", "pad_ms": 200}}}`. An upload is at most `audio_jobs_max_bytes` (50,000,000 bytes on the hosted API).

## Jobs: large batches and documents

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.goshinrai.com/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": "...", "retain_until_expiry": false}`.

An upload is deleted as soon as the last job that reads it finishes. Add `?keep=true` (`POST /v2/uploads?keep=true`) when several jobs will read one upload; it then lives 24 hours, extended by every job that reads it, and `DELETE /v2/jobs/{id}` still removes it. An upload that no job reads expires after 24 hours.

### 2. Start the job

```bash
curl -s https://api.goshinrai.com/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 returns the redacted PDF and the protected text; see Documents below.

### 3. Poll and download

```bash
curl -s https://api.goshinrai.com/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.goshinrai.com/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 redacted PDF (`application/pdf`): image-only pages at 144 dpi, a black box over every protected entity, no text layer |
| `text` | not served | the protected text of the document (`text/plain`) |
| `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 |

Without `output.artifacts`, a text batch returns `protected` and `entities`, and a document job returns `protected`, `text` and `entities`. Ask for `mapping` only when you need to restore; it contains the original values. A text batch
without `protected` or `mapping` only detects.

### Documents

A document job reads a PDF or DOCX file of at most 10,000,000 bytes and returns the redacted PDF, the protected text and the entities. Upload the file with its content type (`application/pdf` or `application/vnd.openxmlformats-officedocument.wordprocessingml.document`):

```bash
UPLOAD=$(curl -s https://api.goshinrai.com/v2/uploads -H "Authorization: Bearer $SHINRAI_API_KEY" \
  -H "Content-Type: application/pdf" --data-binary @contract.pdf | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
```

Start a job of kind `document` with the upload. `language` is optional:

```bash
JOB=$(curl -s https://api.goshinrai.com/v2/jobs -H "Authorization: Bearer $SHINRAI_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: contract-4815" \
  -d '{"kind": "document", "inputs": [{"kind": "file", "source": {"upload": "'$UPLOAD'"}, "language": "de"}]}' \
  | python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])')
```

Repeat the status call until `status` is `succeeded`. Then download the redacted PDF and the protected text, and delete the job:

```bash
curl -s https://api.goshinrai.com/v2/jobs/$JOB -H "Authorization: Bearer $SHINRAI_API_KEY"
curl -s https://api.goshinrai.com/v2/jobs/$JOB/artifacts/protected -H "Authorization: Bearer $SHINRAI_API_KEY" -o contract.redacted.pdf
curl -s https://api.goshinrai.com/v2/jobs/$JOB/artifacts/text -H "Authorization: Bearer $SHINRAI_API_KEY" -o contract.protected.txt
curl -s -X DELETE https://api.goshinrai.com/v2/jobs/$JOB -H "Authorization: Bearer $SHINRAI_API_KEY"
```

The PDF has no text layer: copy the protected text from the `text` artifact. An unsupported file answers 415, a file above the limit 413. A job that fails has `status` `failed` and an `error` with a `code`; do not forward the original file in that case.

### Cancel and delete

- `POST /v2/jobs/{id}/cancel` stops the job. Finished chunks stay charged; the results are deleted, and so is an upload without `keep`.
- `DELETE /v2/jobs/{id}` deletes the job, its results and its uploads at once, also an upload with `keep`. An upload that another job still reads is deleted when that job ends.
- Results are deleted automatically after 24 hours. A kept upload is deleted after 24 hours; every job that reads it extends this time.

### 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`.
- An upload is at most 50,000,000 bytes. A text batch has at most 20,000 lines; a document is
  at most 10,000,000 bytes. `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`). See Limits below.
- Your account holds at most 1,000,000,000 bytes of job data and 200 live uploads (`jobs_account_budget_bytes`,
  `jobs_account_max_uploads`). Above that, an upload or a job answers 429 `rate_limited` with `limit_name`; delete finished jobs
  or wait until results expire.
- Jobs, uploads and results belong to your account. Other accounts get 404.

## Limits

The hosted API applies these limits. `GET /v2/capabilities` returns the values of your deployment in `limits`.

| Limit | Standard | Realtime | Batch | Jobs |
|---|---|---|---|---|
| Inputs per request | 64 | 4 | 200 | text batch: 20,000 lines |
| Characters per input | 200,000 | 4,000 | 200,000 | 200,000 per line |
| Characters per request | 200,000 | 16,000 | 200,000 | up to the upload limit |
| Request body | 12 MiB | 12 MiB | 12 MiB | upload: 50,000,000 bytes |
| Image | 6 MiB and `ocr.max_pixels` | one image, 4.2 megapixels, 3 MiB | 6 MiB and `ocr.max_pixels` | not served |
| Audio | 300 seconds and 12 MiB | not served | not served | 3,600 seconds and 50,000,000 bytes |
| Document | not served | not served | not served | PDF or DOCX, 10,000,000 bytes |
| Time per request | 600 seconds | 600 seconds | 600 seconds | a job keeps its results for 24 hours |

- The capabilities keys are `<tier>_inputs`, `<tier>_chars` and `<tier>_total`; `image_pixels` and `image_bytes`; `audio_standard_max_seconds`, `audio_standard_max_bytes`, `audio_jobs_max_seconds` and `audio_jobs_max_bytes`; `jobs_upload_max_bytes`, `jobs_text_batch_max_lines`, `jobs_text_batch_max_bytes`, `jobs_document_max_bytes`, `jobs_retention_s`, `jobs_account_budget_bytes` and `jobs_account_max_uploads`; `sessions_max_ttl_s` (86,400), `sessions_max_lifetime_s` (86,400, or 604,800 with extended sessions) and `sessions_max_entries` (20,000).
- A request above a limit answers 413 `too_large` with `limit_name`, `limit` and, for audio, `use: /v2/jobs`. Nothing is charged.
- Your plan sets the tiers you can use and the number of requests per minute (429 `rate_limited` with `Retry-After`).

## 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). Limit errors add `limit_name`, `limit` and `supplied`; 402 adds `records_required` and `records_available`. Common codes: 401 `invalid_key`, 402 `insufficient_records`, 403 `tier_not_allowed` or `no_active_plan`, 404 `not_found`, 409 `idempotency_conflict` or `rev_conflict`, 410 `session_expired`, 413 `too_large`, 415 `validation_failed` (an unsupported file or audio type), 422 `validation_failed`, 429 `rate_limited` (also when your account's job storage is full), 501 `capability_unavailable` (an option or input kind this deployment does not serve), 503 `degraded_refused`, `backend_unavailable`, `queue_full` or `queue_timeout`, 504 `processing_timeout`. Retry only when `retryable` is true, and wait for `Retry-After` when it is present.

## Client example (Python)

```python
import httpx

api = httpx.Client(base_url="https://api.goshinrai.com", 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)
```
