PII Pseudonymizer

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.

Request (curl)
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
  }'
Response
{
  "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.

MethodURLPurposeCounts toward the limit
POSTapi.php?action=pseudonymizeReplace personal data with fake valuesYes
POSTapi.php?action=restorePut original values back using a replacement listYes
GETapi.php?action=infoCurrent limits, languages and detector idsNo
GETapi.php?action=openapiOpenAPI 3.0 description for code generators and API toolsNo

POST bodies must be sent with Content-Type: application/json and UTF-8 encoding. Cross-origin requests from browsers are allowed.

Recommended workflow

  1. Pseudonymize the text and keep the returned mapping in your application’s memory.
  2. Check the result if a person will see it, or if it leaves your organisation. Detection is automatic and can miss data.
  3. Use the safe text with the external service, for example an AI model, translator or ticket system.
  4. Restore the original values in the reply, with the restore action or with your own find-and-replace using the mapping.
  5. 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

FieldTypeDefaultDescription
textstringThe text to pseudonymize. Required unless you send texts. Up to 50,000 characters.
textsarray of stringsUp to 20 texts processed together (see several texts). Send either text or texts.
accept_termsbooleanRequired. Must be true.
localestringautoLanguage 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.
detectarray of stringsallDetectors to use. Ids are listed below.
disablearray of stringsnoneDetectors to skip. Use either detect or disable.
known_namesarray 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.
keystringemptyConsistency 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_mappingbooleantrueReturn the replacement list. Needed for restoring.

Detector ids

IdReplaces
nameNames: People’s names after greetings, titles and labels, then every other mention, including other grammatical cases
addressStreet addresses: Streets with house numbers and P.O. boxes
postcodePostcodes and cities: Postcode followed by a town, e.g. 1000 Ljubljana or D-10115 Berlin
phonePhone numbers: Local and international numbers; the country code is kept
emailE-mail addresses: Replaced with addresses at example.com
ibanIBANs: Bank accounts in IBAN format; only numbers with a valid checksum
national_idPersonal ID and tax numbers: EMŠO/JMBG, OIB, Slovenian tax number, Steuer-ID, Austrian and German social security numbers, VAT IDs
credit_cardPayment cards: Card numbers with a valid Luhn checksum
date_of_birthDates of birth: Dates after words like born, rojen, rođen, рођен or geboren
accountCustomer, contract and document numbers: Numbers after labels such as customer no., passport, pogodba št., broj računa
secretPINs, passwords and usernames: Values after labels such as PIN, CVV, password, geslo, lozinka
ipIP addresses: IPv4 and IPv6
ssnUS 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.

Request with all options
{
  "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

FieldTypeDescription
okbooleantrue on success.
text / textsstring / arrayThe pseudonymized text(s), in the same form as the request.
locale / localesstring / arrayLanguage used: en, de, sl, hr, sr or bs.
replacementsintegerNumber of entries in the replacement list.
mappingarrayObjects with type, original and pseudonym. Different grammatical forms of a name are separate entries. Contains the real data.
fallbackbooleantrue 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.
warningstring or nullExplanation when fallback is true.
processing_msintegerProcessing 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.

Request
{
  "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
}
Response
{
  "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.

FieldTypeDescription
text / textsstring / arrayText(s) containing fake values. Same limits as for pseudonymize.
mappingarrayObjects with original and pseudonym; type is ignored. Up to 10,000 entries.
accept_termsbooleanRequired. Must be true.
Request
{
  "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
}
Response
{
  "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.

Requests (curl)
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.json

Rate limits and sizes

Requests per minute60 per IP address (IPv6: per /64 network), shared between the web page and the API
Request body1,048,576 bytes
Text length50,000 characters in total
Texts per request20
Known names200, each up to 100 characters
Consistency key200 characters
Replacement list for restore10,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.

Error response
{
  "ok": false,
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. The limit is 10 requests per minute per network.",
    "retry_after": 42
  }
}
StatusCodeMeaning and fix
400invalid_jsonThe body is not a JSON object.
400invalid_fieldA field is missing, has the wrong type or an unknown value; see field.
400terms_not_acceptedAdd "accept_terms": true.
400empty_textAll texts are empty.
400too_many_texts, too_many_names, name_too_long, key_too_long, too_many_mapping_entriesA size limit was exceeded; see limits.
403https_requiredCall the API over HTTPS.
404unknown_actionUse info, openapi, pseudonymize or restore.
405method_not_allowedUse POST for pseudonymize and restore, GET for info and openapi.
413payload_too_large, text_too_longSplit the text into smaller requests.
415unsupported_media_typeSend Content-Type: application/json.
429rate_limitedWait retry_after seconds.
500internal_error, restore_failedTry again later or with a smaller request.
503service_unavailable, api_disabledThe 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 7.4+
<?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'];
JavaScript
// 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 });
Python 3 (requests)
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": false if 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 texts or a key when separate texts must refer to the same fake people. Without either, every request gets new fake values.
  • Check fallback on 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