<MASKED_PERSON_a1b2c3d4>
and <MASKED_MRN_c9d0e1f2>, never the real values. Your
masked-to-original mapping never leaves your process./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.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import 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)
<MASKED_PERSON_a1b2c3d4>). Re-substitution only helps when the token
survives in the response verbatim.<MASKED_…>
placeholders are tokens, not real data.