API Documentation
Reference for the upii.eu API, which replaces the personal data it detects in text and documents with placeholders. Detection is not perfect: always review the result.
1. Authentication
Send your own software's requests with a Bearer token. Create API keys from your dashboard. Without a token, only the limited demo (500 characters, 10 requests a minute) answers.
curl -X POST https://upii.eu/protect \ -H "Authorization: Bearer pii_your_api_key" \ -H "Content-Type: application/json" \ -d '{"text": "Jean Dupont, jean@test.fr"}'
2. Processing Modes
Control output format with the mode field. Available on /protect and /batch. The value name anonymize is kept as it is, so existing integrations keep working: it means “replace with placeholders”.
anonymize
Default. Protects the text: replaces personal data with typed placeholders (pseudonymisation).
Jean Dupont → [PERSON_1]
detect
Returns entities only. No text modification.
anonymized: null
redact
Replaces all detected personal data with [REDACTED].
Jean Dupont → [REDACTED]
3. Pseudonymization
Choose how placeholders are generated with the pseudonym field. Available on /protect, /batch, and /protect-document (and on their deprecated aliases).
These three styles belong to the API above. The documents of a project in the app (the dashboard) are always written with one fixed token, {{PERSON:2a37e4c1b9}}: the type and a 10-character tag that is the same for the same value everywhere in that project. Restore needs exactly that token.
counter
Default. Per-request positional labels.
Jean Dupont → PERSON_1
Marie Curie → PERSON_2
Same labels regenerated each request.
stable
A fixed tag per value, different for every account.
Jean Dupont → PERSON_caa52d3b16
Marie Curie → PERSON_627eb1ef64
Same input value → same placeholder across all your calls.
realistic
A believable substitute name from a built-in name list.
Jean Dupont → Pierre Lemaire
Marie Curie → Catherine Lambert
Always the same substitute for the same value (as with stable), and the text reads naturally, for example when a person has to read the result.
When to use:
stable: recognising the same person across documents, keeping a record of what was replaced, linking records to each other.realistic: text that has to stay readable, such as a draft you show to someone. Combine with thelocalefield below.
Locale (realistic mode only):
The locale field selects which name list is used. Allowed values: fr_BE (default), nl_BE, de_BE. Ignored for other pseudonym modes.
Coverage in realistic: PERSON and ORG are replaced with names from the list. Every other type (e-mail, phone, IBAN, IP, date, amount and so on) gets the same tag as in stable mode: upii does not invent look-alike IBANs or phone numbers.
Caveats (apply to stable and realistic):
- Repetition shows: a placeholder that appears several times tells the reader that the same value appears several times. This is true of any scheme where one value always gets the same placeholder.
- One account only: the same value gets the same placeholder only inside your account. Two accounts do not get the same placeholder for the same value.
- Spelling variants: upper or lower case, extra spaces, phone formatting (
+33 6 12=+33612) and spaces in an IBAN are ignored when a value is compared, so these variants get the same placeholder. - Realistic mode only: a substitute name can happen to be the name of a real person. The substitute does not reveal the original name.
Repeatable results: in realistic mode your account always gets the same substitutes for the same text, as long as the name list and your account do not change. GET /version returns the running build, its build time, the version of the name list and the identifiers of the detection models, so you can note which version of upii produced a result.
Example — Stable tag
curl -X POST https://upii.eu/protect \ -H "Authorization: Bearer pii_your_key" \ -H "Content-Type: application/json" \ -d '{"text": "Jean Dupont and Marie Curie", "pseudonym": "stable"}' // Response {"anonymized": "[PERSON_caa52d3b16] and [PERSON_627eb1ef64]", ...}
Example — Realistic substitution
curl -X POST https://upii.eu/protect \ -H "Authorization: Bearer pii_your_key" \ -H "Content-Type: application/json" \ -d '{"text": "Jean Dupont and Marie Curie", "pseudonym": "realistic", "locale": "fr_BE"}' // Response (substitute Belgian French names; the same for your account every time) {"anonymized": "[Pierre Lemaire] and [Catherine Lambert]", ...}
4. POST /protect
Protect a single text: pseudonymise the personal data it detects (replace it with placeholders), only detect it, or redact it.
Deprecated alias: POST /anonymize is the same endpoint under its former name, kept for existing integrations. It takes the same request, returns the same response and has the same limits; its answers carry the headers Deprecation: true and Link: </protect>; rel="successor-version". New integrations should use /protect.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | Yes | 1 to 50,000 characters |
| mode | string | No | anonymize (default), detect, or redact |
| pseudonym | string | No | counter (default), stable, or realistic. See Pseudonymization. |
| locale | string | No | fr_BE (default), nl_BE, or de_BE. Used by pseudonym=realistic; ignored otherwise. |
Example — Protect
curl -X POST https://upii.eu/protect \
-H "Authorization: Bearer pii_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "Marie Lefèvre, marie@company.fr, +33 6 12 34 56 78"}' Response
The field that holds the protected text keeps its name, anonymized, so existing integrations keep reading it.
{ "anonymized": "[PERSON_1], [EMAIL_1], [PHONE_1]", "entities": [ {"type": "PERSON", "index": 1, "placeholder": "PERSON_1", "value": "Marie Lefèvre", "start": 0, "end": 14}, {"type": "EMAIL", "index": 1, "placeholder": "EMAIL_1", "value": "marie@company.fr", "start": 16, "end": 32}, {"type": "PHONE", "index": 1, "placeholder": "PHONE_1", "value": "+33 6 12 34 56 78", "start": 34, "end": 52} ], "relationships": [], "latency": {"l1_ms": 0.08, "l2_ms": 28.5, "l3_ms": 0.01, "total_ms": 30.2} }
Example — Detect Only
curl -X POST https://upii.eu/protect \ -H "Authorization: Bearer pii_your_key" \ -H "Content-Type: application/json" \ -d '{"text": "Contact jean@test.fr", "mode": "detect"}' // Response: entities returned, anonymized is null {"anonymized": null, "entities": [{"type": "EMAIL", ...}], ...}
5. POST /batch
Process up to 100 texts in a single request. Same response format as /protect but returns an array.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| texts | string[] | Yes | 1 to 100 texts, each 1-50,000 chars |
| mode | string | No | anonymize, detect, or redact |
| pseudonym | string | No | counter (default), stable, or realistic. Applied to every text. See Pseudonymization. |
| locale | string | No | fr_BE (default), nl_BE, or de_BE. Applied to every text in realistic mode. |
curl -X POST https://upii.eu/batch \ -H "Authorization: Bearer pii_your_key" \ -H "Content-Type: application/json" \ -d '{"texts": ["Jean Dupont", "marie@test.fr"], "mode": "anonymize"}' // Returns: an array of /protect responses
6. POST /protect-document
Upload a document for per-page protection. Not part of the self-serve plans; it is arranged with us (see Plans & limits).
Deprecated alias: POST /anonymize-document is the same endpoint under its former name, kept for existing integrations, with the same request, response and limits and the same Deprecation and Link headers as above (successor: /protect-document).
Supported formats
PDF, DOCX, XLSX, CSV, TXT — max 50 MB, max 500 pages.
curl -X POST https://upii.eu/protect-document \
-H "Authorization: Bearer pii_your_key" \
-F "file=@contract.pdf" Response
{ "filename": "contract.pdf", "pages": [ {"page": 1, "anonymized": "...", "entities": [...], "relationships": [...]}, {"page": 2, ...} ], "totals": {"pages": 2, "entities": 14, "relationships": 3}, "latency": {...} }
7. Demo Mode
Try the API without authentication. Send requests to /api/protect (note the /api prefix) with no auth header.
| Limit | Value |
|---|---|
| Rate limit | 10 req/min per IP |
| Max text | 500 characters |
| Detected values | Masked as *** |
| Links between items | Not returned |
| Endpoints | /api/protect only (the deprecated /api/anonymize still works) |
# No auth needed curl -X POST https://upii.eu/api/protect \ -H "Content-Type: application/json" \ -d '{"text": "Jean Dupont, jean@test.fr"}'
8. Entity Types
The API detects these types of personal data.
Languages. The upii website and dashboard are available in English, French and Dutch. The detection of names, organisations and addresses was built and checked on English, French and Dutch text, so those are the languages upii supports. Text in another language (German, Spanish, Italian, Portuguese and so on) is not supported: names in it can be missed.
Fixed formats. E-mail addresses, phone numbers, IBANs, national numbers, dates and amounts are found by rules rather than by the language model. They are checked mostly on Belgian documents; formats of other countries can be missed or given another type. Always review the result before you use it.
Substitute names. The locale values fr_BE, nl_BE and de_BE only choose the name list used by the realistic pseudonym style; German detection is not supported.
Full names, honorifics
Marie Lefèvre, Dr. Schmidt
Email addresses
jean@company.fr
Phone numbers
+33 6 12 34 56 78
Street addresses
12 rue de Paris, 75001
Organization names
Goldman Sachs, SNCF
Dates in various formats
15/03/2025, March 2025
A date of birth, found by its cue (born on, né le, geboren am, DOB); other dates stay DATE
né le 12/03/1975
Monetary amounts
1.500,00 EUR, $250
IBANs and BIC/SWIFT
FR76 3000 6000...
Credit cards, account numbers
4111 1111 1111 1111
IPv4 and IPv6 addresses
192.168.1.1
Web URLs
https://example.com/path
National IDs, SSN, passport
2 85 01 75 116 005 42
Time durations
3 years, 6 months
Percentages
12.5%, 3 percent
9. Plans & limits
upii has three plans: Starter, Pro, Studio. Every plan has every feature; they differ only in how many pages you can protect each month. Allowances and prices are on the pricing page. Documents you add to a project in the app follow your plan's page allowance.
API limits by plan
The plan codes are starter, pro and studio.
The API limits are the same on all three, and on the trial.
While your trial or plan is active they allow texts of 50,000 characters and 60 requests per minute on the text endpoints; once the trial has ended or a plan is no longer active it is 500 characters and 10 requests per minute.
The other limits in the table do not depend on the plan.
| Plan code | Rate | Max text | Document endpoint | API keys |
|---|---|---|---|---|
| starter | 60 req/min while your trial or plan is active, else 10 | 50,000 chars while active, else 500 | No | 1 |
| pro | 60 req/min while your trial or plan is active, else 10 | 50,000 chars while active, else 500 | No | 1 |
| studio | 60 req/min while your trial or plan is active, else 10 | 50,000 chars while active, else 500 | No | 1 |
Higher limits (more API keys, the document endpoint, higher rates) are arranged with us: write to support@chut.app.
10. Errors
All errors return JSON with a detail field.
| Code | Meaning |
|---|---|
| 400 | Bad request (text too long, unsupported file, etc.) |
| 401 | Missing or invalid API key |
| 403 | Feature not available on your tier |
| 413 | File too large or too many pages |
| 422 | Validation error (invalid JSON, missing fields) |
| 429 | Rate limit exceeded — wait 60 seconds |
| 503 | Service starting up — retry in 15 seconds |
| 504 | Inference timeout |
11. API Key Management
Manage keys from the dashboard or programmatically via cookie-authenticated endpoints.
| Method | Path | Description |
|---|---|---|
| GET | /keys | List your API keys |
| POST | /keys | Create a new key (body: {"name": "..."}) |
| PATCH | /keys/{id} | Rename a key |
| DELETE | /keys/{id} | Revoke a key (permanent) |
Questions? Contact support@chut.app