v0.2 LIVE · iframe isolation + zero-knowledge + React + benchmarks

Integrate Zoza Vault

Encrypt sensitive form fields in the browser with your server's public key, so your CDN, WAF, and reverse proxy never see plaintext. SDK + REST API + apply flow — 15 lines of HTML to get started.

Quick Start

The fastest path: include the SDK, point it at a form selector, decrypt on your server. Three steps.

1. Include the SDK on your page
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>
2. Point it at your form
<form id="payment">
  <input name="card_number">
  <input name="cvv">
  <input name="ssn">
</form>

<script>
  const vault = new ZozaVault('app_a99b96dd23fb8414d17ec48df7984802');
  vault.protectForm('#payment');
</script>
3. Decrypt on your server
// Node.js
const res = await fetch('https://vault-api.zoza.world/v1/decrypt', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.VAULT_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ payload: req.body._vault_payload })
});
const { fields } = await res.json();
// fields.card_number, fields.cvv, fields.ssn — plaintext

That's it. Your CDN, WAF, reverse proxy, and load balancer never see the field values in plaintext. Keep reading for authenticated endpoints, React/Node helpers, and the apply flow.

Pick Your Integration

Vault ships four integration surfaces. Most teams combine the browser SDK and one server example.

🌐
Web frontend

Browser SDK (vault.js)

Drop-in script for any HTML form. Generates ephemeral X25519 keys, seals fields with AES-256-GCM, submits ciphertext. No build step. Works with or without a framework.

⚛️
React / Vue

Framework components (v0.2)

<VaultInput name="ssn" /> renders a normal input that auto-seals on change. Works with react-hook-form, Formik, Vue 3 Composition API.

🔌
Any language

REST API

HTTP/JSON endpoints for register, public-key fetch, and decrypt. Call from Go, Python, Rust, Java — whatever your backend runs. Bearer-token auth, rate-limit headers.

🖥️
Your server

Local decrypt (no network)

For air-gapped or ultra-low-latency paths, decrypt with the Go or Node server SDK using your private key directly — no round-trip to Vault's API.

Apply for an API Key

Registering apps and calling decrypt endpoints require an API key. Public-key fetch (what the browser SDK does) is keyless and open to anyone. Submit the form below — we review within 24 hours and email your vk_live_... key on approval. Keys are shown once at issuance — store yours somewhere safe.

Authentication

Decrypt endpoints require your app's API key as a Bearer token:

Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Every authenticated response includes rate-limit headers:

X-RateLimit-Limit: 100000          # your daily quota
X-RateLimit-Remaining: 99872      # calls left today
X-RateLimit-Reset-UTC: 00:00       # quota resets at UTC midnight

If a key is invalid, revoked, or over quota, the response is 401 with an error body explaining which of those it was.

⚠ Never ship your API key in browser JS

The browser SDK does not need your API key — it only fetches the public key. Keep the vk_live_... key on your server. If you accidentally commit it, email us and we'll revoke within an hour.

Base URL

https://vault-api.zoza.world

All API paths below are relative to that base. The browser SDK defaults to this host — override via new ZozaVault(appId, 'https://your-proxy.example.com') if you proxy through your own infra.

Browser SDK — Installation

HTML script tag
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>

Or via npm for bundlers:

package.jsonnpm / pnpm / yarn
npm install @zoza/vault
pnpm add @zoza/vault
yarn add @zoza/vault

Zero runtime dependencies. Ships ESM + CJS + TypeScript types. Works in browsers (Chrome 110+, Safari 17+, Firefox 115+), Node ≥18, Deno, Bun. Older browsers fall back to a @noble/curves pure-JS polyfill (~8KB gzipped).

Initialize
import { ZozaVault } from '@zoza/vault';

const vault = new ZozaVault(
  'app_a99b96dd23fb8414d17ec48df7984802',                             // your app ID
  'https://vault-api.zoza.world'            // default; override if self-hosted
);

Construction fires a single request for the app's public key. All subsequent sealField calls reuse the same cached key.

Seal a single field

Encrypt one value with the field name as domain separator. Returns a SealedField object ready for transport.

const sealed = await vault.sealField('ssn', '123-45-6789');
// {
//   e: "base64 ephemeral public key",
//   n: "base64 nonce",
//   c: "base64 ciphertext+tag",
//   f: "ssn"
// }
ℹ Field name as AAD

The field name is bound into the AES-GCM authenticated-data parameter. That means moving ciphertext from one field name to another fails decryption — attackers cannot swap an encrypted "amount" into a "discount" field.

Seal an entire form

Pass a plain object; every value is sealed under its key name.

const payload = await vault.sealForm({
  full_name:    'Jane Doe',
  ssn:          '123-45-6789',
  dob:          '1985-03-14',
  card_number:  '4111111111111111',
  cvv:          '123'
});

// Submit payload as JSON body or form field:
await fetch('/api/patient-intake', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload)
});

protectForm() — drop-in form intercept

The fastest integration. Attach to a form selector; submit is intercepted, fields are sealed, and the submission proceeds with a single _vault_payload hidden field instead of the plaintext ones.

// Default: replace all text fields with a single _vault_payload field
vault.protectForm('#patient-form');

// Exclude fields (e.g. CSRF, sentinel values)
vault.protectForm('#patient-form', {
  exclude: ['_csrf', 'form_revision']
});

// Custom submit handler — receive the sealed payload, do whatever
vault.protectForm('#patient-form', {
  onSealed: async (payload) => {
    await fetch('/api/intake', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
    showSuccessPage();
  },
  onError: (err) => showErrorToast(err.message)
});

React integration (@zoza/vault-react) — planned

🗺 Roadmap — not yet released

@zoza/vault-react is planned for v0.2. The code samples below are an API preview — they will not run today. Use the core @zoza/vault package (vanilla JS, shipped) in the meantime; the React layer is purely ergonomic sugar over it. Follow CoreCogitAI/vault-js-sdk for release notifications.

Drop-in form (auto seal-on-submit) — v0.2 API preview
import { VaultForm, VaultInput } from '@zoza/vault-react';

export function PatientIntake() {
  return (
    <VaultForm appId="app_a99b96dd23fb8414d17ec48df7984802" action="/api/patient-intake">
      <VaultInput name="full_name"  placeholder="Jane Doe" />
      <VaultInput name="ssn"        placeholder="123-45-6789" />
      <VaultInput name="dob"        placeholder="1985-03-14" />
      <VaultInput name="card_number" type="password" />
      <button type="submit">Submit</button>
    </VaultForm>
  );
}
react-hook-form / Formik (useVault hook)
import { useForm } from 'react-hook-form';
import { useVault, VaultInput } from '@zoza/vault-react';

export function PaymentForm() {
  const form = useForm();
  const vault = useVault({ appId: 'app_a99b96dd23fb8414d17ec48df7984802' });

  const onSubmit = async () => {
    const payload = await vault.sealPayload();
    await fetch('/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
  };

  return (
    <vault.Provider>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <VaultInput name="card_number" type="password" />
        <VaultInput name="cvv"         type="password" />
        <button type="submit" disabled={!vault.isReady}>Pay</button>
      </form>
    </vault.Provider>
  );
}
Manual per-field control (refs)
import { useRef } from 'react';
import { VaultInput, VaultInputHandle } from '@zoza/vault-react';

const ssnRef = useRef<VaultInputHandle>(null);

async function validate() {
  const sealed = await ssnRef.current?.seal();
  // sealed = { e, n, c, f } — ship to server for decrypt + validate
}

<VaultInput ref={ssnRef} name="ssn" appId="app_a99b96dd23fb8414d17ec48df7984802" />

Iframe isolation (vanilla JS)

Same defense as the React package, without the React dependency. Works with any framework or no framework at all.

HTML + script tag
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>

<form id="intake">
  <div data-zoza-vault-field="ssn"         data-app-id="app_a99b96dd23fb8414d17ec48df7984802"></div>
  <div data-zoza-vault-field="card_number" data-app-id="app_a99b96dd23fb8414d17ec48df7984802" data-type="password"></div>
  <button type="submit">Submit</button>
</form>

<script>
  const vault = ZozaVaultIframe.mount({ appId: 'app_abc' });
  document.querySelector('#intake').onsubmit = async (e) => {
    e.preventDefault();
    const sealed = await vault.sealAll();
    // sealed = { ssn: {e,n,c,f}, card_number: {e,n,c,f} }
    await fetch('/api/intake', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ app_id: 'app_abc', fields: sealed })
    });
  };
</script>
ℹ How the iframe isolation works

The data-zoza-vault-field div is replaced with an <iframe> whose origin is vault-api.zoza.world (not your merchant origin). Same-origin policy means your app's JavaScript cannot read the iframe's DOM, intercept its keystrokes, or snapshot the value before encryption.

On sealAll(), a postMessage arrives inside each iframe; the iframe runs Web Crypto X25519+HKDF+AES-GCM locally and returns only the ciphertext. The plaintext never leaves the iframe memory.

Zero-knowledge registration

For workloads where you want Zoza removed from your compliance scope entirely. We generate the keypair, return the private key to you exactly once, then destroy our copy. After that response is flushed, our server cannot decrypt anything sealed to this app's public key — only you can, using the private key we just handed you.

Admin-only: POST /v1/apps/zk
curl -X POST https://vault-api.zoza.world/v1/apps/zk \
  -H "Authorization: Bearer $ZOZA_VAULT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Healthcare Zero-Knowledge Inc."}'

# Response 201 — shown ONCE
{
  "id":             "app_a99b96dd23fb8414d17ec48df7984802...",
  "name":           "Healthcare Zero-Knowledge Inc.",
  "api_key":        "vk_live_XXXX...",
  "public_key":     "4f2c...a9",
  "private_key":    "<64 hex chars — YOUR ONLY COPY>",
  "zero_knowledge": true,
  "warning":        "This is the ONE AND ONLY time the private key will be shown. Zoza has destroyed its copy. If you lose this key, every payload sealed to this app becomes permanently unrecoverable. Store it in an HSM or KMS immediately.",
  "decrypt_hint":   "Use the Go / Node / Python server SDK with this private key for local decrypt. POST /v1/decrypt will return 422 for all payloads under this app (we literally cannot decrypt)."
}
⚠ You hold the only copy

Store the private key in an HSM (AWS CloudHSM, GCP Cloud KMS, HashiCorp Vault) or equivalent immediately. Never in source control. Never in a secrets manager you don't control. Loss = permanent data loss for every payload sealed to this app's public key.

Rotation on ZK apps is refused by the API — we don't have the key to rotate. To "rotate" a ZK app, register a new one, migrate data, retire the old.

Key rotation

Rotate a compromised (or just periodically-rotated) app's keypair + API key in one call. Old API key is revoked at that instant; new public key replaces the old for all future browser fetches.

POST /v1/apps/{id}/rotate
curl -X POST https://vault-api.zoza.world/v1/apps/app_a99b96dd23fb8414d17ec48df7984802/rotate \
  -H "Authorization: Bearer vk_live_..."

# Response 200 — new API key shown ONCE
{
  "id":         "app_a99b96dd23fb8414d17ec48df7984802",
  "api_key":    "vk_live_NEW...",
  "public_key": "8d44...c1",
  "rotated_at": "2026-04-17T10:30:00Z",
  "message":    "Keypair rotated. Copy the new API key now — old key revoked."
}

No support ticket required. Competitors like Basis Theory require opening a ticket and waiting for a human. Vault customers rotate during active incidents, not after.

Server SDK — Decrypt

Two paths: hit our REST API with Bearer auth, or decrypt locally with your stored private key. Both produce identical plaintext.

Decrypt locally (no network round-trip)

Your app's private key is yours — we store a copy by default for convenience, but you can request a zero-knowledge mode at registration where we forget the private key after handing it to you once. After that, only your server can decrypt.

✓ Zero-knowledge mode recommended for regulated workloads

Zero-knowledge mode removes Vault from your compliance scope entirely. We see only ciphertext in transit; the private key lives in your HSM / KMS / memory. The REST decrypt endpoint won't work (we can't decrypt without the key), but local decrypt is faster anyway.

Node.js — local decrypt (@zoza/vault-server planned)

🗺 Roadmap — @zoza/vault-server not yet released

Server-side convenience package is planned for v0.2. Until then, decrypt locally using your existing crypto libraries against the token format spec in src/crypto.ts — the parse + decrypt primitives are already exported from @zoza/vault via decryptFieldWithPrivateKey(). The VaultServer wrapper below is an API preview.

v0.2 API preview — npm install @zoza/vault-server (planned)
import { VaultServer } from '@zoza/vault-server';

const vault = new VaultServer({
  appId: process.env.VAULT_APP_ID,
  privateKey: process.env.VAULT_PRIVATE_KEY_B64   // base64 32-byte X25519
});

app.post('/api/intake', async (req, res) => {
  const plaintext = await vault.openForm(req.body);
  // plaintext = { full_name: "Jane Doe", ssn: "123-...", ... }

  await db.insert('patients', plaintext);
  res.json({ ok: true });
});

Go — local decrypt (roadmap)

🗺 Roadmap — Go SDK not yet released

The Zoza Vault Go SDK is planned for v0.2. Until then, decrypt in Go using golang.org/x/crypto/curve25519 + crypto/aes (cipher.NewGCM) directly against the token format documented at src/crypto.ts. The code below is an API preview.

v0.2 API preview — zoza/vault-server (planned)
package main

import (
    "encoding/base64"
    // Zoza Vault Go SDK — install path published at open-source launch.
    // vault "zoza/vault-server"
)

func handleIntake(w http.ResponseWriter, r *http.Request) {
    var payload vault.SealedPayload
    json.NewDecoder(r.Body).Decode(&payload)

    privBytes, _ := base64.StdEncoding.DecodeString(os.Getenv("VAULT_PRIVATE_KEY"))
    var privKey [32]byte
    copy(privKey[:], privBytes)

    plaintext, err := vault.OpenForm(privKey, &payload)
    if err != nil {
        http.Error(w, "decryption failed", 422)
        return
    }
    // plaintext["ssn"] = []byte("123-45-6789")
}

Python — local decrypt (roadmap)

🗺 Roadmap — Python SDK not yet released

zoza-vault on PyPI is planned for v0.2. Until then, decrypt server-side in Python using cryptography (PyNaCl for X25519, cryptography.hazmat.primitives.ciphers.aead.AESGCM for AES-GCM) against the token format documented at src/crypto.ts. The code below is an API preview.

v0.2 API preview — pip install zoza-vault (planned)
from zoza_vault import VaultServer
import os

vault = VaultServer(
    app_id=os.environ["VAULT_APP_ID"],
    private_key_b64=os.environ["VAULT_PRIVATE_KEY"]
)

@app.post("/api/intake")
async def intake(req: Request):
    payload = await req.json()
    plaintext = vault.open_form(payload)
    # plaintext = {"full_name": "Jane Doe", "ssn": "123-45-6789", ...}
    await db.insert("patients", plaintext)
    return {"ok": True}

REST API

Full endpoint reference. All paths are relative to https://vault-api.zoza.world.

Register app

POST /v1/apps ADMIN

Create a new app with a fresh Curve25519 keypair. Returns the API key and public key once — store both safely. This endpoint is gated by the admin token; developers reach it via the apply form above, which admins approve in the Zoza dashboard.

curl -X POST https://vault-api.zoza.world/v1/apps \
  -H "Authorization: Bearer $ZOZA_VAULT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Example Health Inc."}'

# Response 201
{
  "id":         "app_a99b96dd23fb8414d17ec48df7984802...",
  "name":       "Example Health Inc.",
  "api_key":    "vk_live_XXXX...",
  "public_key": "4f2c...a9",
  "created_at": "2026-04-17T09:00:00Z",
  "message":    "Save your API key — it won't be shown again."
}

Fetch public key

GET /v1/apps/{id}/public-key OPEN

Return an app's public key. This is what the browser SDK calls on construction. No auth required — public keys are public by design. Cached with Cache-Control: public, max-age=3600.

curl https://vault-api.zoza.world/v1/apps/app_a99b96dd23fb8414d17ec48df7984802/public-key

# Response 200
{
  "app_id":     "app_a99b96dd23fb8414d17ec48df7984802",
  "public_key": "4f2cf1a892...a9"
}

Decrypt a sealed payload

POST /v1/decrypt API KEY

Decrypt a full SealedPayload (all fields). The payload is base64-encoded JSON as produced by the browser SDK.

curl -X POST https://vault-api.zoza.world/v1/decrypt \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"payload": "eyJhcHBfaWQi..."}'

# Response 200
{
  "app_id":       "app_a99b96dd23fb8414d17ec48df7984802",
  "fields": {
    "full_name":  "Jane Doe",
    "ssn":        "123-45-6789",
    "dob":        "1985-03-14"
  },
  "field_count":  3,
  "decrypted_at": "2026-04-17T10:22:15Z"
}

Decrypt a single field

POST /v1/decrypt/field API KEY

Decrypt one SealedField JSON object. Useful for partial decrypts (e.g. decrypt SSN for ID verification, leave everything else sealed).

curl -X POST https://vault-api.zoza.world/v1/decrypt/field \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"e":"...", "n":"...", "c":"...", "f":"ssn"}'

# Response 200
{
  "field_name": "ssn",
  "value":      "123-45-6789"
}

Submit application (apply for key)

POST /v1/applications OPEN

Submit a B2B customer application for an API key. Public endpoint — the apply form above posts to this. Rate-limited to 3/email/day and 10/IP/day.

curl -X POST https://vault-api.zoza.world/v1/applications \
  -H "Content-Type: application/json" \
  -d '{
    "company":           "Example Health Inc.",
    "email":             "security@example.com",
    "website":           "https://example.com",
    "use_case":          "dapp",
    "use_case_details":  "Patient intake form encryption before Cloudflare edge.",
    "volume_tier":       "10k-100k",
    "plan_requested":    "pro"
  }'

# Response 202
{
  "id":         "apl_abc123...",
  "status":     "pending",
  "created_at": "2026-04-17T10:45:00Z",
  "message":    "Application received. We email you within 24h."
}

Wire format

Understanding the bytes on the wire is useful if you want to reimplement the client in another language or audit what leaves the browser.

SealedField (single field)

FieldTypeDescription
ebase64 · 32 bytesEphemeral Curve25519 public key (fresh per field)
nbase64 · 12 bytesAES-GCM nonce (cryptorandom)
cbase64 · variableCiphertext + 16-byte GCM tag (plaintext length + 16)
fstringField name (used as AAD for domain separation)

SealedPayload (multi-field)

{
  "app_id":    "app_a99b96dd23fb8414d17ec48df7984802",
  "fields":    {
    "ssn":     { "e": "...", "n": "...", "c": "...", "f": "ssn" },
    "dob":     { "e": "...", "n": "...", "c": "...", "f": "dob" },
    ...
  },
  "sealed_at": "2026-04-17T10:22:00Z"
}

Key derivation

shared   = X25519(ephemeral_priv, server_pub)
info     = "ZozaVault_FieldKey_v1:" || field_name
fieldKey = HKDF-Extract-and-Expand(SHA-256, shared, salt=<empty>, info, 32)
ciphertext = AES-256-GCM-Seal(fieldKey, nonce, plaintext, AAD=field_name)

Because info includes the field name, two fields with identical plaintext produce different ciphertext — ciphertext equality reveals nothing about plaintext equality across fields.

Error codes

StatusErrorMeaning
400invalid_requestJSON malformed or required field missing
401invalid_api_keyMissing / malformed Authorization header
401revoked_api_keyKey was revoked — apply for a new one
403wrong_appSealed payload's app_id does not match this API key's app
422decryption_failedCiphertext tampered, AAD mismatch, or wrong key
429rate_limitedQuota exceeded — see X-RateLimit headers
500internal_errorSomething broke on our side; check audit log

Rate limits & plans

Free

$0/mo
1,000 decrypt calls/day
  • 1 registered app
  • Community support
  • All SDKs + REST
  • Audit log access

Enterprise

Custom
10M+ calls/day, HSM keys
  • Dedicated subdomain
  • SOC 2 report
  • HIPAA BAA (post-audit)
  • Priority bug triage

Rate limits are per-API-key and reset at UTC midnight. Burst rate: 100 req/sec per key. Over quota returns 429 with Retry-After.

CORS

The browser-facing endpoints (/v1/apps/{id}/public-key and /v1/applications) return Access-Control-Allow-Origin: *. Decrypt endpoints are server-side only — never call them from the browser (the API key would be exposed). If you see a CORS error on /v1/decrypt, you're calling it from the wrong side of the network.

Going-live checklist

ℹ Before you flip the production flag

Every item on this list is a real failure mode we've seen in customer integrations.

Support

© 2026 Zoza. Source code copyright LD-16949/2026-CO. Client SDKs open-source: @zoza/vault on npm + github.com/CoreCogitAI/vault-js-sdk. Backend source-available under NDA.