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.
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>
<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>
// 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.
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.
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.
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.
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.
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
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>
Or via npm for bundlers:
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).
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"
// }
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
@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.
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>
);
}
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>
);
}
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.
<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>
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.
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)."
}
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.
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 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)
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.
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)
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.
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)
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.
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
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
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
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
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)
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)
| Field | Type | Description |
|---|---|---|
| e | base64 · 32 bytes | Ephemeral Curve25519 public key (fresh per field) |
| n | base64 · 12 bytes | AES-GCM nonce (cryptorandom) |
| c | base64 · variable | Ciphertext + 16-byte GCM tag (plaintext length + 16) |
| f | string | Field 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
| Status | Error | Meaning |
|---|---|---|
| 400 | invalid_request | JSON malformed or required field missing |
| 401 | invalid_api_key | Missing / malformed Authorization header |
| 401 | revoked_api_key | Key was revoked — apply for a new one |
| 403 | wrong_app | Sealed payload's app_id does not match this API key's app |
| 422 | decryption_failed | Ciphertext tampered, AAD mismatch, or wrong key |
| 429 | rate_limited | Quota exceeded — see X-RateLimit headers |
| 500 | internal_error | Something broke on our side; check audit log |
Rate limits & plans
Free
- 1 registered app
- Community support
- All SDKs + REST
- Audit log access
Pro
- Unlimited apps
- Email support · 24h
- Zero-knowledge mode
- Export audit log CSV
Enterprise
- 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
Every item on this list is a real failure mode we've seen in customer integrations.
- API key is server-side only. Grep your build output — if
vk_live_appears in any bundled JS, rotate immediately. - Private key in a secret manager (AWS Secrets Manager, Fly secret, HashiCorp Vault). Never on disk in plaintext.
- Sentry / Datadog / New Relic request-body capture is disabled for your form submission endpoints — or configured to only capture after decryption so you control the plaintext.
- Test retry behavior — a user submitting the same form twice should produce two different ciphertexts (fresh ephemeral keys each time).
- Tampered payload test — flip one bit of a ciphertext, confirm server returns
422and doesn't leak partial plaintext. - Public-key fetch is cached — browser SDK caches for the session, server-side cache the key with 1h TTL.
- Rate-limit plumbing — handle
429with exponential backoff +Retry-After. - Audit-log monitoring — pipe your Vault activity to your SIEM. Alert on unexpected geos / volumes.
Support
- Docs: you're looking at them. Source lives in
frontend-web/public/developers/vault.html. - Status: status.zoza.world — incidents and maintenance windows.
- Security: security@zoza.world — bugs that matter, key rotation requests. PGP key at /about/vault-bounty.
- Product: hello@zoza.world — enterprise pricing, HIPAA BAA, zero-knowledge mode.
- Social: @zoza_world — release notes and outage comms.
© 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.