Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.
The full pattern KINDI is built for: take a clinical note, mask it, send the masked text to an LLM, then decrypt the LLM's response client-side using your saved mappings to restore the original PII.
The LLM only ever sees opaque tokens like <MASKED_PERSON_a1b2c3d4> and <MASKED_MRN_c9d0e1f2>, never the real values. Your masked-to-original mapping never leaves your process.
This example uses /mask; you make the LLM call yourself, so the mapping never leaves your process. If you'd rather KINDI make the OpenAI/Anthropic call for you, use the LLM Proxy instead: it masks and unmasks server-side (the mapping lives only in that request, then is discarded) and you skip the decrypt step entirely.
MRN is an extended entity type; enable it under Settings → Entities (or PUT /me/entities) before this example detects it. On a new account only the ten core types are on, so an MRN in the note would be forwarded to the LLM unmasked and no <MASKED_MRN_…> token would appear. See PII types.
Go is omitted from this example because the OpenAI / Anthropic Go SDKs vary widely. Compose the round-trip from the mask-and-decrypt example plus your preferred LLM client manually.
import base64, json, requests from openai import OpenAI from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes KINDI = "https://api.kindi.me" KEY = "mk_live_..." note = "Patient John Doe, DOB 1980-01-01, complains of headache." # 1. Mask. m = requests.post( f"{KINDI}/api/v1/mask", headers={"Authorization": f"Bearer {KEY}"}, json={"text": note}, ).json() # 2. Send masked text to the LLM. chat = OpenAI().chat.completions.create( model="gpt-5.4-mini", messages=[{"role": "user", "content": f"Summarize: {m['masked_text']}"}], ) masked_summary = chat.choices[0].message.content # 3. Decrypt the mappings (same flow as the mask-and-decrypt example). kek = HKDF( algorithm=hashes.SHA256(), length=32, salt=b"masker-kek-salt-v1", info=b"masker-kek-v1", ).derive(KEY.encode()) dek = AESGCM(kek).decrypt( base64.b64decode(m["nonce_dek"]), base64.b64decode(m["wrapped_dek"]), None, ) plain = AESGCM(dek).decrypt( base64.b64decode(m["nonce_payload"]), base64.b64decode(m["ciphertext"]), None, ) mappings = json.loads(plain.decode()) # 4. Substitute the <MASKED_…> tokens back into the LLM output. # `mappings` is { "<MASKED_PERSON_a1b2c3d4>": "John Doe", ... }; # each key is the exact token that appears in masked_text. final = masked_summary for token, original in mappings.items(): final = final.replace(token, original) print(final)

What to watch for

  • The LLM may paraphrase tokens ("the patient" instead of <MASKED_PERSON_a1b2c3d4>). Re-substitution only helps when the token survives in the response verbatim.
  • Long contexts can drop placeholders silently. For high-stakes pipelines, validate that every placeholder in the input is still present in the output before re-substituting.
  • Refusals: some LLMs flag token-laden text as PHI even though it isn't. Add a system prompt that explains the <MASKED_…> placeholders are tokens, not real data.