API documentation
The API does the same as the web page: it replaces names, addresses, phone numbers, ID, tax and bank numbers and other personal data with realistic fake values, and can put the original values back afterwards. It speaks JSON over HTTPS, needs no account or API key, and stores nothing except the time and IP address of each request.
Quick start
Send a POST request with a JSON body. Every request that processes text must contain "accept_terms": true to confirm that you accept the terms of service.
curl -s -X POST 'https://www.pii.kwizmo.eu/api.php?action=pseudonymize' \
-H 'Content-Type: application/json' \
-d '{
"text": "Spoštovani gospod Novak, moje ime je Maja Kovačič. Tel: 041 123 456",
"accept_terms": true
}'{
"ok": true,
"text": "Spoštovani gospod Zajc, moje ime je Irena Božič. Tel: 078 369 169",
"locale": "sl",
"replacements": 4,
"mapping": [
{ "type": "phone", "original": "041 123 456", "pseudonym": "078 369 169" },
{ "type": "name", "original": "Novak", "pseudonym": "Zajc" },
{ "type": "name", "original": "Maja", "pseudonym": "Irena" },
{ "type": "name", "original": "Kovačič", "pseudonym": "Božič" }
],
"fallback": false,
"warning": null,
"processing_ms": 14
}Endpoints
Base URL: https://www.pii.kwizmo.eu/api.php. The action is chosen with the action query parameter. Add &pretty to any URL for indented JSON.
| Method | URL | Purpose | Counts toward the limit |
|---|---|---|---|
| POST | api.php?action=pseudonymize | Replace personal data with fake values | Yes |
| POST | api.php?action=restore | Put original values back using a replacement list | Yes |
| GET | api.php?action=info | Current limits, languages and detector ids | No |
| GET | api.php?action=openapi | OpenAPI 3.0 description for code generators and API tools | No |
POST bodies must be sent with Content-Type: application/json and UTF-8 encoding. Cross-origin requests from browsers are allowed.
Recommended workflow
- Pseudonymize the text and keep the returned
mappingin your application’s memory. - Check the result if a person will see it, or if it leaves your organisation. Detection is automatic and can miss data.
- Use the safe text with the external service, for example an AI model, translator or ticket system.
- Restore the original values in the reply, with the
restoreaction or with your own find-and-replace using the mapping. - Discard the mapping when you no longer need it. It contains the real personal data.
Pseudonymize
POST https://www.pii.kwizmo.eu/api.php?action=pseudonymize
Request fields
| Field | Type | Default | Description |
|---|---|---|---|
text | string | — | The text to pseudonymize. Required unless you send texts. Up to 50,000 characters. |
texts | array of strings | — | Up to 20 texts processed together (see several texts). Send either text or texts. |
accept_terms | boolean | — | Required. Must be true. |
locale | string | auto | Language of the text. Fake names, streets and postcodes are chosen for it. One of: auto, en, de, at, ch, sl, hr, sr, bs, me. Set it for short texts; bs and me are never detected automatically. |
detect | array of strings | all | Detectors to use. Ids are listed below. |
disable | array of strings | none | Detectors to skip. Use either detect or disable. |
known_names | array of strings | [] | Full names to replace everywhere, first name first, including other grammatical cases (Janez, Janeza, Janezu). Names are otherwise found only after cues such as “Dear”, “gospod” or “Lep pozdrav”. Up to 200 names of 100 characters. |
key | string | empty | Consistency key. Empty: each request gets new fake values. The same key always produces the same fake values, also on the web page. Anyone who knows the key gets the same results, so use a long random value. |
include_mapping | boolean | true | Return the replacement list. Needed for restoring. |
Detector ids
| Id | Replaces |
|---|---|
name | Names: People’s names after greetings, titles and labels, then every other mention, including other grammatical cases |
address | Street addresses: Streets with house numbers and P.O. boxes |
postcode | Postcodes and cities: Postcode followed by a town, e.g. 1000 Ljubljana or D-10115 Berlin |
phone | Phone numbers: Local and international numbers; the country code is kept |
email | E-mail addresses: Replaced with addresses at example.com |
iban | IBANs: Bank accounts in IBAN format; only numbers with a valid checksum |
national_id | Personal ID and tax numbers: EMŠO/JMBG, OIB, Slovenian tax number, Steuer-ID, Austrian and German social security numbers, VAT IDs |
credit_card | Payment cards: Card numbers with a valid Luhn checksum |
date_of_birth | Dates of birth: Dates after words like born, rojen, rođen, рођен or geboren |
account | Customer, contract and document numbers: Numbers after labels such as customer no., passport, pogodba št., broj računa |
secret | PINs, passwords and usernames: Values after labels such as PIN, CVV, password, geslo, lozinka |
ip | IP addresses: IPv4 and IPv6 |
ssn | US Social Security numbers: Format 123-45-6789 |
A value excluded from one detector can still be caught by a more general one. For example, a labelled card number can be replaced as a document number when credit_card is disabled.
{
"text": "Poštovani gospodine Kovačeviću, šaljem ugovor br. 45/2023.",
"locale": "hr",
"disable": ["ip", "email"],
"known_names": ["Ivana Marić", "Petar Babić"],
"key": "b6Jq0v7yZ3-our-team-key",
"include_mapping": true,
"accept_terms": true
}Response fields
| Field | Type | Description |
|---|---|---|
ok | boolean | true on success. |
text / texts | string / array | The pseudonymized text(s), in the same form as the request. |
locale / locales | string / array | Language used: en, de, sl, hr, sr or bs. |
replacements | integer | Number of entries in the replacement list. |
mapping | array | Objects with type, original and pseudonym. Different grammatical forms of a name are separate entries. Contains the real data. |
fallback | boolean | true if detailed detection failed and a coarse redaction was returned instead: every capitalized word becomes [redacted], every digit #, every e-mail [email]. The original text is never returned. |
warning | string or null | Explanation when fallback is true. |
processing_ms | integer | Processing time on the server. |
Several texts at once
Send texts instead of text to process related texts, such as the messages of one conversation, in one request. The same person then gets the same fake name in every text, in every grammatical case, even without a key. The total length of all texts must stay within 50,000 characters, and the request counts once toward the rate limit.
{
"texts": [
"Poštovani gospodine Kovačeviću, šaljem ugovor.",
"Razgovarao sam s gospodinom Kovačevićem. Kovačević se slaže."
],
"locale": "hr",
"accept_terms": true
}{
"ok": true,
"texts": [
"Poštovani gospodine Blaževiću, šaljem ugovor.",
"Razgovarao sam s gospodinom Blaževićem. Blažević se slaže."
],
"locales": ["hr", "hr"],
"replacements": 3,
"mapping": [
{ "type": "name", "original": "Kovačević", "pseudonym": "Blažević" },
{ "type": "name", "original": "Kovačeviću", "pseudonym": "Blaževiću" },
{ "type": "name", "original": "Kovačevićem", "pseudonym": "Blaževićem" }
],
"fallback": false,
"warning": null,
"processing_ms": 9
}Restore
POST https://www.pii.kwizmo.eu/api.php?action=restore replaces fake values with the originals, using a replacement list you send along. The server keeps no replacement lists, so you must always provide one; the mapping from a pseudonymize response can be sent unchanged. Only whole words are replaced, longest fake values first, and every position is replaced once.
| Field | Type | Description |
|---|---|---|
text / texts | string / array | Text(s) containing fake values. Same limits as for pseudonymize. |
mapping | array | Objects with original and pseudonym; type is ignored. Up to 10,000 entries. |
accept_terms | boolean | Required. Must be true. |
{
"text": "Hvala, gospodine Blaževiću. Blažević će dobiti odgovor danas.",
"mapping": [
{ "original": "Kovačević", "pseudonym": "Blažević" },
{ "original": "Kovačeviću", "pseudonym": "Blaževiću" }
],
"accept_terms": true
}{
"ok": true,
"text": "Hvala, gospodine Kovačeviću. Kovačević će dobiti odgovor danas.",
"processing_ms": 1
}Restoring is a simple find-and-replace, so you can also do it in your own code and never send the real data back to the service.
Info and OpenAPI
GET ?action=info returns the current limits, the language and detector ids, and data retention details. GET ?action=openapi returns an OpenAPI 3.0 document for tools such as Postman, Insomnia or client generators. Neither request counts toward the rate limit.
curl -s 'https://www.pii.kwizmo.eu/api.php?action=info&pretty'
curl -s 'https://www.pii.kwizmo.eu/api.php?action=openapi' -o openapi.jsonRate limits and sizes
| Requests per minute | 60 per IP address (IPv6: per /64 network), shared between the web page and the API |
|---|---|
| Request body | 1,048,576 bytes |
| Text length | 50,000 characters in total |
| Texts per request | 20 |
| Known names | 200, each up to 100 characters |
| Consistency key | 200 characters |
| Replacement list for restore | 10,000 entries |
The window is a sliding 60 seconds. Successful pseudonymize and restore responses include X-RateLimit-Limit and X-RateLimit-Remaining. When the limit is reached the API answers with status 429, a Retry-After header and retry_after in the body; wait that many seconds before trying again. Need more? Contact info@kwizmo.eu.
Errors
Errors return "ok": false and an error object with a stable code, a readable message and, where it helps, the field that caused it. Messages may change; check the code.
{
"ok": false,
"error": {
"code": "rate_limited",
"message": "Too many requests. The limit is 10 requests per minute per network.",
"retry_after": 42
}
}| Status | Code | Meaning and fix |
|---|---|---|
| 400 | invalid_json | The body is not a JSON object. |
| 400 | invalid_field | A field is missing, has the wrong type or an unknown value; see field. |
| 400 | terms_not_accepted | Add "accept_terms": true. |
| 400 | empty_text | All texts are empty. |
| 400 | too_many_texts, too_many_names, name_too_long, key_too_long, too_many_mapping_entries | A size limit was exceeded; see limits. |
| 403 | https_required | Call the API over HTTPS. |
| 404 | unknown_action | Use info, openapi, pseudonymize or restore. |
| 405 | method_not_allowed | Use POST for pseudonymize and restore, GET for info and openapi. |
| 413 | payload_too_large, text_too_long | Split the text into smaller requests. |
| 415 | unsupported_media_type | Send Content-Type: application/json. |
| 429 | rate_limited | Wait retry_after seconds. |
| 500 | internal_error, restore_failed | Try again later or with a smaller request. |
| 503 | service_unavailable, api_disabled | The service is temporarily unavailable or the API is switched off. |
A 200 response with "fallback": true is not an error, but the text was only coarsely redacted. Treat it as a signal to retry with a shorter text.
Examples in PHP, JavaScript and Python
All examples pseudonymize a text, pass it on, and restore the reply. They retry once the rate limit allows it.
<?php
// PHP 7.4+, no Composer or extensions needed.
function piiApi(string $action, array $payload): array
{
$payload['accept_terms'] = true;
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'timeout' => 30,
'ignore_errors' => true, // read the JSON body of 4xx/5xx responses too
],
]);
for ($attempt = 1; $attempt <= 3; $attempt++) {
$body = file_get_contents('https://www.pii.kwizmo.eu/api.php?action=' . urlencode($action), false, $context);
$data = json_decode((string) $body, true);
if (!is_array($data)) {
throw new RuntimeException('The PII service did not return JSON.');
}
if ($data['ok'] ?? false) {
return $data;
}
if (($data['error']['code'] ?? '') === 'rate_limited' && $attempt < 3) {
sleep((int) ($data['error']['retry_after'] ?? 60)); // wait as instructed, then retry
continue;
}
throw new RuntimeException($data['error']['message'] ?? 'PII service error');
}
throw new RuntimeException('The PII service stayed rate limited.');
}
// 1. Pseudonymize before sending the text anywhere
$result = piiApi('pseudonymize', [
'text' => $ticketText,
'locale' => 'sl',
'known_names' => [$customer['full_name']],
]);
$safeText = $result['text'];
$mapping = $result['mapping']; // contains real data: keep it in memory only
// 2. Use the safe text with an external service
$reply = $externalService->answer($safeText);
// 3. Put the real values back into the reply
$restored = piiApi('restore', ['text' => $reply, 'mapping' => $mapping]);
echo $restored['text'];// Browser or Node.js 18+
async function piiApi(action, payload) {
const response = await fetch('https://www.pii.kwizmo.eu/api.php?action=' + action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, accept_terms: true }),
});
const data = await response.json();
if (!data.ok) {
const err = new Error(data.error.message);
err.code = data.error.code;
err.retryAfter = data.error.retry_after;
throw err;
}
return data;
}
const { text: safeText, mapping } = await piiApi('pseudonymize', {
text: 'Sehr geehrte Frau Dr. Müller, meine Adresse ist Karl-Marx-Straße 12, 10115 Berlin.',
locale: 'de',
});
const reply = await askSomeService(safeText);
const { text: realReply } = await piiApi('restore', { text: reply, mapping });import time
import requests
API = "https://www.pii.kwizmo.eu/api.php"
def pii_api(action, payload, retries=3):
payload = {**payload, "accept_terms": True}
for attempt in range(retries):
r = requests.post(API, params={"action": action}, json=payload, timeout=30)
data = r.json()
if data.get("ok"):
return data
if data["error"]["code"] == "rate_limited" and attempt < retries - 1:
time.sleep(int(r.headers.get("Retry-After", 60)))
continue
raise RuntimeError(f'{data["error"]["code"]}: {data["error"]["message"]}')
result = pii_api("pseudonymize", {
"texts": ["Зовем се Јелена Јовановић, ЈМБГ 2505905710011.", "Јелена ће доћи сутра."],
"locale": "sr",
"key": "a-long-random-key-for-this-project",
})
safe_texts = result["texts"]
restored = pii_api("restore", {"texts": safe_texts, "mapping": result["mapping"]})Good practice
- Treat the mapping as personal data. Keep it in memory, or encrypted if you must store it, and delete it when the conversation is over. Set
"include_mapping": falseif you never need to restore. - Pass the names you know. Customer and employee names from your own database make detection much more reliable, especially for names without a greeting or title nearby.
- Set the language when you know it. Short texts are hard to detect, and Bosnian or Montenegrin must always be set explicitly.
- Use
textsor akeywhen separate texts must refer to the same fake people. Without either, every request gets new fake values. - Check
fallbackon every response and log it. - Review before publishing. Detection is heuristic: it can miss data and can replace ordinary words. The result is pseudonymized, not anonymized, and may still identify a person through its context.
- Only process data you are allowed to process, and respect the rate limit instead of spreading requests across many addresses.
Privacy and terms
For each pseudonymize or restore request, the service stores only the request time and the IP address, for up to 30 days, to enforce rate limits and prevent abuse. Texts, results, replacement lists, names, keys and options are processed in memory and never stored or logged. Use of the API is subject to the terms of service; Kwizmo d.o.o. is not responsible for misuse or for personal data that is missed, wrongly detected or wrongly replaced.
Questions: info@kwizmo.eu
