The PrivacyPal SDK lets you encode sensitive data into Privacy Twins before it reaches an AI model,
then decode the response to restore original values. Available for both Node.js and Python:
identical API surface, same endpoint, same behavior. The same clients work against the enterprise
data plane at api.privacypal.ai
and the PrivacyPal Family data plane at
api.family.privacypal.ai.
Async/await API. Works with Express, Next.js, Fastify, and any Node.js runtime.
npm install @privacypal/sdk
Synchronous API built on requests. Works with FastAPI, Django, Flask, and any Python 3.10+ runtime.
pip install privacypal-sdk
Zero to protected in five minutes: install, initialize, then run the full encode → AI → decode loop. Real PII never leaves your boundary; the model only ever sees Privacy Twins.
1. Install
npm install @privacypal/sdk
pip install privacypal-sdk
2. Get a key: sign up at the
PrivacyPal Portal
and export it as PRIVACYPAL_API_KEY.
3. Run the loop: this is a complete program. Encode, hand the safe text to any model or vendor, decode the answer.
import { PrivacyPalClient } from '@privacypal/sdk'; const client = new PrivacyPalClient({ apiUrl: 'https://api.privacypal.ai', apiKey: process.env.PRIVACYPAL_API_KEY, }); // 1. Encode: real PII becomes Privacy Twins const encoded = await client.encode({ data: 'Refund request from Sarah Connor, sarah@acme.com, card ending 4242', sourceContainer: 'quickstart', sourceElement: 'demo', }); console.log(encoded.encodedData); // safe for any LLM, vendor API, or log line // 2. Use the protected text anywhere, e.g. the built-in AI gateway const chat = await client.chatWithAI({ prompt: 'Draft a polite reply approving the refund: ' + encoded.encodedData, }); // 3. Decode: restore the original values, with an audit trail const decoded = await client.decode({ continuationId: encoded.continuationId, data: chat.decodedResponse ?? chat.llmResponse, sensitiveHashes: encoded.transformations.map(t => t.originalHash), authorization: { token: process.env.PRIVACYPAL_API_KEY, purpose: 'Quickstart demo' }, }); console.log(decoded.decodedData);
import os from privacypal_sdk import PrivacyPalClient client = PrivacyPalClient( api_url='https://api.privacypal.ai', api_key=os.environ['PRIVACYPAL_API_KEY'], ) # 1. Encode: real PII becomes Privacy Twins encoded = client.encode( 'Refund request from Sarah Connor, sarah@acme.com, card ending 4242', source_container='quickstart', source_element='demo', ) print(encoded['encodedData']) # safe for any LLM, vendor API, or log line # 2. Use the protected text anywhere, e.g. the built-in AI gateway # Note: use prompt (not message) for chat_with_ai chat = client.chat_with_ai( prompt='Draft a polite reply approving the refund: ' + encoded['encodedData'], ) # 3. Decode: restore the original values, with an audit trail decoded = client.decode( continuation_id=encoded['continuationId'], data=chat.get('decodedResponse') or chat['llmResponse'], sensitive_hashes=[t['originalHash'] for t in encoded['transformations']], authorization={'token': os.environ['PRIVACYPAL_API_KEY'], 'purpose': 'Quickstart demo'}, ) print(decoded['decodedData'])
That is the whole mental model: encode → transmit → decode. Everything else in this guide (batch, files, streaming, multi-turn sessions, the Family data plane) is a variation on that loop.
Highlights since 1.0.0, across the SDKs and the platform API (now v1.4.4):
developerKey (Node) / developer_key (Python) instead of a user JWT. Sent as x-pp-developer-key; rotate at runtime with updateDeveloperKey / update_developer_key.clientId / client_id config, sent as x-pp-client-id to attribute traffic per integration.register accepts an optional signup token for email-verified registration flows.encodeFile / encode_file accepts a platform identifier; Python adds a per-call timeout override for interactive callers (default upload timeout stays 120s).health_check now reports a healthy API correctly (the /health endpoint returns plain text, not JSON), and errors are typed exceptions on a common PrivacyPalError base carrying status_code.entitlement_lease / update_entitlement_lease, sent as x-pp-entitlement-lease and checked on the encode, batch, file, and decode routes./api/memory endpoint group (episodes, recall, stats, settings, audit, export, correction, and crypto-shred erase).api.family.privacypal.ai and a family detection profile that additionally protects school names and city-level geography. See the Family API section below.Install the SDK for your language. Both packages expose identical functionality.
npm install @privacypal/sdk
Requires Node.js 18 or later. TypeScript types are included.
pip install privacypal-sdk
Requires Python 3.10 or later. Installs requests as the only dependency.
Create a client instance with your API URL and API key. You can obtain an API key from the PrivacyPal Portal.
Server-to-server integrations can pass a developerKey (Node) /
developer_key (Python) instead of a user JWT; it is sent as the
x-pp-developer-key header and takes precedence when both are set.
An optional clientId / client_id
attributes traffic per integration.
import { PrivacyPalClient } from '@privacypal/sdk'; const client = new PrivacyPalClient({ apiUrl: 'https://api.privacypal.ai', apiKey: process.env.PRIVACYPAL_API_KEY, }); // Verify connectivity (optional) const health = await client.healthCheck(); console.log(health.success); // true
from privacypal_sdk import PrivacyPalClient import os client = PrivacyPalClient( api_url='https://api.privacypal.ai', api_key=os.environ['PRIVACYPAL_API_KEY'], ) # Verify connectivity (optional) health = client.health_check() print(health['success']) # True
encode scans a string for PII and replaces sensitive values
with Privacy Twins. Returns encodedData that is safe to pass
to AI models, and a continuationId needed to decode later.
const encoded = await client.encode({ data: 'Patient: Jane Doe, DOB: 1985-03-15, SSN: 123-45-6789', sourceContainer: 'my-app', // identifies your application sourceElement: 'patient-record', // identifies the data field scoreThreshold: 0.35, // PII confidence threshold (default) language: 'en', // language hint (default) }); console.log(encoded.encodedData); // "Patient: [TWIN-A1B2], DOB: [TWIN-C3D4]..." console.log(encoded.continuationId); // keep this to decode later console.log(encoded.transformations); // list of PII replacements made
encoded = client.encode( 'Patient: Jane Doe, DOB: 1985-03-15, SSN: 123-45-6789', source_container='my-app', # identifies your application source_element='patient-record', # identifies the data field score_threshold=0.35, # PII confidence threshold (default) language='en', # language hint (default) ) print(encoded['encodedData']) # "Patient: [TWIN-A1B2], DOB: [TWIN-C3D4]..." print(encoded['continuationId']) # keep this to decode later print(encoded['transformations']) # list of PII replacements made
To encode multiple items in one request, use encodeBatch / encode_batch. To encode a file (PDF, DOCX, CSV, image), use encodeFile / encode_file: it accepts an optional platform identifier, defaults to a 120s upload timeout, and (Python) takes a per-call timeout override for interactive callers.
Detection is policy-driven on the server. Accounts on the PrivacyPal Family data plane automatically get the family detection profile (school names and city-level geography protected, on top of standard PII); enterprise accounts get the standard profile. The profile is resolved from the authenticated account and cannot be switched from the client, so there is no SDK parameter for it, by design.
Pass the continuationId from the encode response alongside
text containing Privacy Twins to restore original sensitive values.
// Encode first const encoded = await client.encode({ data: 'Call me at 555-867-5309, I am Sarah Connor', sourceContainer: 'chatbot', sourceElement: 'user-message' }); // Send encoded data to AI, get a response with Privacy Twins const aiReply = await callOpenAI(encoded.encodedData); // Decode the AI response, restore original values const decoded = await client.decode({ continuationId: encoded.continuationId, data: aiReply.content, sensitiveHashes: encoded.transformations.map(t => t.originalHash), authorization: { token: 'your-jwt', purpose: 'Display to user' }, }); console.log(decoded.decodedData); // Real names and numbers restored
# Encode first encoded = client.encode( 'Call me at 555-867-5309, I am Sarah Connor', source_container='chatbot', source_element='user-message' ) # Send encoded data to AI, get a response with Privacy Twins ai_reply = call_openai(encoded['encodedData']) # Decode the AI response, restore original values decoded = client.decode( continuation_id=encoded['continuationId'], data=ai_reply['content'], sensitive_hashes=[t['originalHash'] for t in encoded['transformations']], authorization={'token': 'your-jwt', 'purpose': 'Display to user'}, ) print(decoded['decodedData']) # Real names and numbers restored
Use chatWithAI to send prompts through PrivacyPal's AI gateway, which automatically encodes PII before forwarding
to the LLM and decodes the response. For token-by-token output, Node.js adds
chatWithAIStream (Server-Sent Events with per-chunk decoding);
Python streaming is on the roadmap, so Python callers use the non-streaming
chat_with_ai today. Multi-turn conversations carry twin mappings
across turns via sessionContinuationIds and the
ConversationSession helper.
const result = await client.chatWithAI({ prompt: 'Summarise the patient notes for Jane Doe.', model: 'gemini-2.0-flash-exp', provider: 'vertex', }); console.log(result.decodedResponse); // Decoded: real names in the answer
result = client.chat_with_ai( prompt='Summarise the patient notes for Jane Doe.', model='gemini-2.0-flash-exp', provider='vertex', ) print(result['decodedResponse']) # Decoded: real names in the answer
PrivacyPal Family runs on its own, fully isolated deployment of the PrivacyPal API. Two surfaces matter to developers:
https://api.family.privacypal.ai. The same API and SDKs documented here (encode, decode, AI chat, memory), deployed with entirely separate credentials and signing keys. An enterprise token is not valid on the Family plane and a Family member token is not valid on api.privacypal.ai, in either direction, by design.https://family.privacypal.ai/api/family. Households, members, age stages, guardrails, devices, alerts, approvals, wellbeing, digests, and billing. Parents authenticate with a session (cookie or bearer token); kids sign in with a Family Code plus optional PIN and never hold an email or password; registered devices use a device token. Partner access is limited to read-only, aggregate admin endpoints.
Family member requests automatically use the family detection profile: on top of standard PII,
school names (a dedicated SCHOOL entity) and city-level geography
are twinned, because first name + school + city is a re-identification vector for a child.
The platform enforces its product promises server-side: activity telemetry is signals-only
(no prompt or response text can enter it), and teens in the Pilot stage (14 to 17) manage their own
memory facts.
// Same SDK, Family data plane. The member token comes from the // Family platform (device member-token exchange), not from login(). const familyClient = new PrivacyPalClient({ apiUrl: 'https://api.family.privacypal.ai', apiKey: memberToken, }); const encoded = await familyClient.encode({ data: 'Alex goes to Washington High in a sunny city', sourceContainer: 'family-app', sourceElement: 'kid-message', }); // Family profile applies automatically: name, school, and city are all twinned.
# Same SDK, Family data plane. The member token comes from the # Family platform (device member-token exchange), not from login(). family_client = PrivacyPalClient( api_url='https://api.family.privacypal.ai', api_key=member_token, ) encoded = family_client.encode( 'Alex goes to Washington High in a sunny city', source_container='family-app', source_element='kid-message', ) # Family profile applies automatically: name, school, and city are all twinned.
The full Family platform surface is specified in family/openapi.yaml.
Node.js: Throws Error; check err.message for status prefixes. Python: Raises typed exceptions (AuthenticationError, TrialExpiredError, NetworkError, RequestError), all subclasses of PrivacyPalError, which carries status_code.
| Exception / Pattern | HTTP Status | When it occurs |
|---|---|---|
| AuthenticationError / "401:" | 401 | Missing, expired, or invalid API key |
| TrialExpiredError / "403:" or "[Trial expired]" | 403 | Trial ended or subscription required |
| NetworkError / "Network Error:" | N/A | Cannot reach the API (connection refused, DNS failure) |
| RequestError / "Request Error:" | N/A | Request configuration error |
try { const encoded = await client.encode({ data: '...' }); } catch (err) { if (err.message?.startsWith('401:')) { console.error('Invalid or expired API key'); } else if (err.message?.startsWith('403:') || err.message?.includes('Trial expired')) { console.error('Subscription required'); } else if (err.message?.includes('Network Error')) { console.error('Cannot reach API'); } else { throw err; } }
from privacypal_sdk import ( PrivacyPalClient, AuthenticationError, TrialExpiredError, NetworkError, ) try: encoded = client.encode('...') except AuthenticationError: print('Invalid or expired API key') except TrialExpiredError: print('Subscription required') except NetworkError: print('Cannot reach API') except Exception as e: raise
Real workflows from production deployments: encode at the boundary, hand twins to an external agent or API, decode the response back to real values.
Encode the sensitive input, hand the twins off to an external agent, then decode the response back to original values.
const encoded = await client.encode({ data: 'User: John Smith, Email: john@acme.com, ID: user-123', sourceContainer: 'my-agent', sourceElement: 'user_input', }); // Share encoded data with an external agent const response = await sendToExternalAgent(encoded.encodedData); // Decode the response back to real values const decoded = await client.decode({ continuationId: encoded.continuationId, data: response.text, }); console.log(decoded.decodedData);
encoded = client.encode( 'User: John Smith, Email: john@acme.com, ID: user-123', source_container='my-agent', source_element='user_input', ) # Share encoded data with an external agent response = send_to_external_agent(encoded['encodedData']) # Decode the response back to real values decoded = client.decode( continuation_id=encoded['continuationId'], data=response['text'], ) print(decoded['decodedData'])
Encode PHI before sending to a vendor API, decode the analysis on the way back. HIPAA-compliant by design.
const encoded = await client.encode({ data: 'Patient: Jane Doe, DOB: 1985-03-15, Dx: Condition X', sourceContainer: 'ehr-system', sourceElement: 'patient-record', }); const analysis = await fetch('https://vendor-api.com/analyze', { method: 'POST', body: JSON.stringify({ data: encoded.encodedData }), }); const result = await client.decode({ continuationId: encoded.continuationId, data: (await analysis.json()).report, });
encoded = client.encode( 'Patient: Jane Doe, DOB: 1985-03-15, Dx: Condition X', source_container='ehr-system', source_element='patient-record', ) response = requests.post( 'https://vendor-api.com/analyze', json={'data': encoded['encodedData']}, ) result = client.decode( continuation_id=encoded['continuationId'], data=response.json()['report'], )
Encode trade info to protect trader identity, match through Privacy Twins, and (in a future release) decode only after x402 payment is settled.
// Future: import { x402Middleware } from '@x402/payment'; const encoded = await client.encode({ data: 'Trader: trader-789, Symbol: AAPL, Qty: 1000', sourceContainer: 'dark-pool', sourceElement: 'trade-request', }); const match = await findCounterparty(encoded.encodedData); if (match.found) { // Future: x402 payment required before decode const realData = await client.decode({ continuationId: encoded.continuationId, data: match.counterpartyData, }); return realData.decodedData; }
# Future: from x402 import middleware encoded = client.encode( 'Trader: trader-789, Symbol: AAPL, Qty: 1000', source_container='dark-pool', source_element='trade-request', ) match = find_counterparty(encoded['encodedData']) if match['found']: # Future: x402 payment required before decode real_data = client.decode( continuation_id=encoded['continuationId'], data=match['counterpartyData'], ) return real_data['decodedData']
Explore all endpoints, request/response schemas, and try the API interactively. Open API Reference →