Capabilities

Private inference

Private inference end-to-end encrypts every request between your client and your worker. Your prompts, completions, uploaded audio and images, and tool arguments are sealed on your machine and only decrypted inside your worker. Pendra relays the traffic but never holds the key to read it.

Private inference is available on the Pro and Enterprise plans. See pricing for details.

What Pendra can and can't see

With private inference on, Pendra cannot see:

  • Your prompts and the model's completions
  • Uploaded audio and images
  • Tool arguments

Pendra can still see the operational metadata it needs to route and bill the request: the model id, request and response sizes and timing, token counts, and an opaque routing token. It never sees the content.

Set up a key on your worker

On the worker that serves the request, run pendra keygen --save. In one step it generates a workload keypair, writes the private key to a 0600 file next to the worker's config, records that file's path in the config (workload_private_key_file) — no environment variables to manage — and, if a worker is already running, offers to restart it so the new key takes effect right away. In a script, pass --restart or --no-restart to skip the prompt.

# On the worker: generate + install the key in one step
pendra keygen --save

#   Wrote workload key to ~/.pendra/workload_key (mode 0600)
#   Set workload_private_key_file in ~/.pendra/config.yaml
#   › Restart the worker now to use the new key? [y/N]: y
#   ✓ Worker restarted

# Answer "n" to restart later yourself:
#   systemctl restart pendra      # Linux service
#   (or relaunch the Pendra app on macOS / Windows)

# Scripting it? Skip the prompt with --restart / --no-restart:
pendra keygen --save --restart

Once the worker restarts it advertises the public key's fingerprint. The private key never leaves the worker — Pendra only ever sees the fingerprint. Running pendra keygen on its own (without --save) just prints a keypair, so you can also mint one on any machine — no worker needed — and distribute it yourself.

Key the rest of your fleet

You have two ways to key more than one worker, and you can mix them:

  • Reuse one key everywhere (simplest). A single workload key can serve every worker in your organisation. Generate it once, then drop the same key file at the default location (~/.pendra/workload_key) on each additional worker — it's picked up automatically, with no pendra config set needed:
# Reuse one key on another worker: copy the key file to the
# default location — the worker discovers it there automatically, no config:
scp ~/.pendra/workload_key  other-worker:~/.pendra/workload_key

# then restart that worker so it loads the key:
#   systemctl restart pendra      # Linux service
#   (or relaunch the Pendra app on macOS / Windows)
  • Register distinct keys. An organisation may register up to 3 distinct workload keys — for example, a different key per data centre or per team. Run pendra keygen --save on each such worker to give it its own key. Your clients automatically seal every request to all registered keys, so any worker holding any one of them can serve the request; no client change is needed as you add keys.

The limit is 3 keys per organisation. If a worker advertises a fourth, different key, Pendra refuses its connection until it drops back to one of the registered keys — retire a key on the Encryption page first if you need to swap one in.

Bring your own key

You don't have to let the worker generate the key. A workload key is a standard X25519 private key, and pendra keygen --save writes it as a PEM PKCS#8 file — the same shape openssl produces — so you can create one with your own tooling and point the worker at it instead. Both are read the same way:

# Generate an X25519 key with your own tooling instead:
openssl genpkey -algorithm X25519 -out ~/.pendra/workload_key.pem
chmod 600 ~/.pendra/workload_key.pem

# Point the worker at the PEM file and restart:
pendra config set workload_private_key_file ~/.pendra/workload_key.pem

The worker reads the PEM, derives the public key, and advertises its fingerprint just as it would for a pendra keygen key. Use openssl genpkey -algorithm X25519 specifically — an Ed25519 key is a different algorithm and won't be accepted.

Enable it in your client

Turn on private inference when you construct the client. The SDK fetches your organisation's public key, seals each request to it, and decrypts the sealed reply — all transparently. You can also enable it without a code change by setting PENDRA_PRIVATE_INFERENCE=1 in the environment.

from pendra import Pendra

# Turn on private inference for the whole client.
client = Pendra(private_inference=True)  # reads PENDRA_API_KEY

response = client.chat.completions.create(
    model="qwen3.6:27b",
    messages=[{"role": "user", "content": "Summarise this contract."}],
)
print(response.choices[0].message.content)

Verify the key is yours

The one thing worth checking is that the key your client seals to is really the one on your worker. Compare the fingerprint pendra keygen --save printed on the worker against the fingerprint shown on the Encryption page in the dashboard. Do this out of band — over a channel separate from the one you're securing — so a compromised path can't feed you a fake fingerprint.

To make the check automatic, pin the fingerprint you verified. If the key ever changes, the request fails instead of quietly sending your data somewhere new. If you registered several distinct keys (up to 3 per organisation), pin all of them at once as a comma-separated allowlist — your client then seals only to those fingerprints and rejects anything else:

# Pin the fingerprint you verified out of band. If the key the platform
# routes to ever differs, the request fails instead of sending your data.
# Registered several keys? Pin them all: "a7f3c0d2…e9c21,b8e4d1f3…0a2c5".
client = Pendra(private_inference={"pinned_fingerprint": "a7f3c0d2…e9c21"})

Losing a key is just a rotation

Because Pendra stores none of your prompt or completion content, there's nothing to recover if a key is lost — and nothing to leak if a key is exposed. You simply run pendra keygen --save again on the worker. It writes the new key and, so you don't lose service mid-rotation, automatically preserves the old key as workload_private_key_file_previous (a workload_key.previous file beside your config). The worker keeps advertising both fingerprints, so clients still pinned to the old one keep working while you roll out the new fingerprint. Once every client has re-pinned, remove workload_private_key_file_previous from the config and delete the workload_key.previous file. That's the whole recovery path: rotate.

In the dashboard

The Encryption page is your organisation's hub for private inference. It shows:

  • Your registered workload key fingerprints, to verify out of band.
  • Each worker's status — Encrypted (has a workload key) or not — with a per-worker key fingerprint on its detail page.
  • A per-request Key column on the Usage page, showing which key sealed each request (or for a plaintext request).
  • A Require E2E toggle that rejects any plaintext request across your whole organisation. Turn it on once every worker you rely on has a key — a worker without one receives no traffic while enforcement is on.
The Playground can't seal requests in the browser, so its traffic is not end-to-end encrypted — use the Python or Node SDK for private inference.

How is this end-to-end encrypted if my client has no key?

A fair question: your client never stores a private key, and it even fetches what it needs to encrypt from Pendra — so how can Pendra be unable to read the traffic? The answer is that "the key" is really three different keys, and public-key cryptography keeps them apart:

  • Your workload private key — created by pendra keygen --save, it lives only on your workers and is never sent to Pendra. It is the only key that can decrypt a request.
  • Your workload public key — derived from the private one and safe to share; this is what your client fetches from Pendra. Public-key encryption lets anyone seal a message using only the recipient's public key, so your client needs no stored secret to send an encrypted request.
  • A throwaway reply key — for each request your client mints a fresh keypair, sends the public half so the worker can seal the response back to it, and keeps the private half in memory just long enough to decrypt the reply before discarding it. It is never provisioned, stored, or sent to Pendra.

So "my client holds no key" and "my client encrypts and decrypts" aren't in tension: sending needs only your public key, and receiving uses a key your client generated itself. Pendra can't read anything because the one key that opens a request — your workload private key — exists only on your own worker. It's a locked postbox: Pendra gives your client the address of your postbox and carries the sealed letter, but only your worker holds the key to open it.

What stops Pendra handing over the wrong key? Because your client fetches the public keys from Pendra, a dishonest platform could try to substitute its own and read everything. The strong defence is to pin the fingerprint you verified against your worker (see Verify the key is yours, above): once pinned, your client seals only to keys whose fingerprint matches your pin and refuses anything else, so a key swap fails loudly instead of silently succeeding. Without a pin, the client trusts the keys Pendra returns — including new ones added later — so you can add or rotate worker keys without re-pinning. That's convenient, but it means a compromised key fetch could slip in an extra recipient: if this is part of your threat model, pin the fingerprint.

Technical specification

For security review, here is exactly what happens on the wire. Pendra relays ciphertext it cannot read — it never holds your private key and never sees your prompts, completions, uploaded audio or images, or tool arguments.

Cryptographic suite

  • HPKE (Hybrid Public Key Encryption, RFC 9180) in base mode — suite DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + ChaCha20-Poly1305.
  • Workload keys are 32-byte X25519 keys, stored on the worker as a PEM PKCS#8 private key (a base64 raw-32-byte key is also accepted). A key's fingerprint — the id you verify out of band — is the lowercase-hex SHA-256 of the raw 32-byte public key.

Request

Your SDK generates a fresh random 32-byte data key per request and encrypts the entire request body — messages, sampler parameters, tools, and, for audio, the raw file bytes — with ChaCha20-Poly1305. That data key is sealed with HPKE (info = "pendra-pi-v1") to every workload public key your organisation has registered (up to 3), so a worker holding any one of the matching private keys can unwrap it — only a worker of yours can. The envelope's routing metadata (model, stream flag, request type, and your client's response key) is bound into the AEAD's additional authenticated data, so Pendra can't silently re-route or re-classify a request — the worker detects the mismatch and rejects it.

Response

Each request carries a fresh, client-generated ephemeral public key. The worker HPKE-seals the reply to it (info = "pendra-pi-v1:response"), deriving a 32-byte response key; a streamed response encrypts each chunk under a monotonic 12-byte counter nonce. Only your client can decrypt the reply.

What Pendra can and cannot see

Never: prompt or completion content, uploaded audio or images, or tool arguments — ciphertext end to end. Still visible (worker-reported metadata, needed for routing and billing): the model id, request/response sizes and timing, token counts, and an opaque routing token. The private key never leaves your worker.

Go further

  • Chat — the core request shape, unchanged with encryption on.
  • Python SDK — full client reference.
  • Node SDK — full client reference.