Skip to main content
Two client-side packages shrink your PCI/PII compliance scope the way Stripe Elements does:
PackageWhat it does
@knoxcall/browserPure client-side ECIES sealing (P-256 → HKDF-SHA256 → AES-256-GCM), single-use reveal via KnoxClient, and the framework-agnostic Elements iframe protocol
@knoxcall/react<CardCollect> and <Reveal> — iframe-isolated React components built on the browser package
The idea: a sensitive value (a PAN, an SSN, an API credential) is sealed into a portable kc: ciphertext in the page, using your tenant’s public key — plaintext never reaches your servers. The kc: string is byte-compatible with KnoxCall’s server-side format, so only KnoxCall (holding the private key) can decrypt it. These are client-side packages, not management-API SDKs — they never hold an API key.

Install

Not yet published to npm — install from a monorepo checkout by path. @knoxcall/browser requires WebCrypto (any modern browser, or Node ≥ 18 for SSR/tests); @knoxcall/react needs react >= 18 and @knoxcall/browser as a peer.
npm install /path/to/KnoxCall/sdk/knoxcall-browser /path/to/KnoxCall/sdk/knoxcall-react

Seal in the browser

1. Server side — fetch the sealing bundle with your API key (the bundle contains only public material, so it is safe to hand to the page):
// Your backend (Node management SDK), e.g. GET /api/sealing-bundle
import { KnoxCall } from '@knoxcall/sdk';

const knox = new KnoxCall({ apiKey: process.env.KNOXCALL_API_KEY });
app.get('/api/sealing-bundle', async (_req, res) => {
  res.json(await knox.crypto.getSealingBundle()); // GET /v1/encrypt/sealing-bundle
});
2. Browser side — seal the value before it ever leaves the page:
import { createEncryptor } from '@knoxcall/browser';

const bundle = await fetch('/api/sealing-bundle').then((r) => r.json());
const encryptor = createEncryptor(bundle, { purpose: 'pci' });

const ciphertext = await encryptor.encrypt('4242424242424242');
// -> "kc:1:s:<key_ref>:<eph_pubkey>:<iv>:<ct||tag>:$"

// Send the ciphertext to your backend and store it as-is. Your servers only
// ever see the kc: string; decryption happens via POST /v1/decrypt with your
// API key (or never, if you proxy it onward with the Ephemeral Proxy).
encrypt() accepts strings, finite numbers, booleans, null, and JSON-serializable objects/arrays; the original type is preserved through decryption. See Browser-Side Encryption for the underlying format.

Capture cards with <CardCollect>

<CardCollect> embeds a KnoxCall-hosted cross-origin iframe that captures card details and seals the PAN inside the iframe. Your page — and any XSS on it — only ever receives the ciphertext (plus display-safe last4 / brand). That collapses your frontend PCI scope toward SAQ A.
import { useEffect, useState } from 'react';
import { CardCollect } from '@knoxcall/react';
import type { SealingBundle } from '@knoxcall/browser';

function CheckoutForm() {
  const [bundle, setBundle] = useState<SealingBundle | null>(null);
  const [complete, setComplete] = useState(false);

  useEffect(() => {
    fetch('/api/sealing-bundle').then((r) => r.json()).then(setBundle);
  }, []);

  if (!bundle) return null;
  return (
    <CardCollect
      bundle={bundle}
      purpose="pci"
      onChange={setComplete}
      onToken={(ciphertext, { last4, brand }) => {
        // ciphertext is a kc: string — safe to send to and store on your
        // backend as-is. The PAN itself never left the KnoxCall iframe.
        fetch('/api/cards', { method: 'POST', body: JSON.stringify({ ciphertext, last4, brand }) });
      }}
      onError={(msg) => console.error('card element error:', msg)}
    />
  );
}

Reveal with capability tokens

Reading a value back in the browser never uses an API key. Your backend mints a single-use, payload-pinned capability token (kct_…) bound to the exact ciphertext or vault token, and hands only that token to the page:
// Your backend: mint a capability token bound to one ciphertext.
app.post('/api/reveal-token', async (req, res) => {
  // POST /v1/client-tokens -> { token: "kct_…", expires_at, action }
  res.json(await knox.crypto.mintClientToken({ action: 'decrypt', data: storedCiphertext }));
});
Consume it browser-side — either headless with KnoxClient:
import { KnoxClient } from '@knoxcall/browser';

const { token } = await fetch('/api/reveal-token', { method: 'POST' }).then((r) => r.json());
const knox = new KnoxClient(); // optionally { baseUrl, fetchImpl }

const pan = await knox.reveal(token, storedCiphertext);        // POST /v1/client/decrypt
const raw = await knox.detokenize(token, 'tok_visa_abc123');   // POST /v1/client/detokenize
…or with the <Reveal> component, which renders the plaintext to the user inside the iframe — your JavaScript never sees it:
import { Reveal } from '@knoxcall/react';

<Reveal
  capabilityToken={token}        // kct_… from your backend, single-use
  ciphertext={storedCiphertext}  // the kc: value bound into that token
  action="decrypt"               // or "detokenize" for vault tokens
  onError={(msg) => setError(msg)}
/>
The token is consumed server-side on first use; replaying it fails. KnoxClient refuses anything that is not a kct_… token before making a network call, so an API key pasted into the browser by mistake never leaves the page.

Security model

  • No API key in the browser. Sealing needs only the public bundle; revealing needs only a short-lived kct_ capability token minted by your backend. Nothing shipped to the page can decrypt at will.
  • Single-use, payload-pinned reveal tokens. A kct_ token is bound to one specific ciphertext/vault token and is consumed on first use.
  • Context-bound ciphertexts. The HKDF info pins tenant, app key, key version, datatype, and optional purpose (data-role, e.g. "pci") into the derived key; the kc: header rides as AES-GCM AAD. Decrypting under the wrong purpose, key, or a tampered header simply fails.
  • Exact-origin iframe trust. Element messages are only accepted from an allowlisted origin (exact string match — no prefix or substring semantics, so https://api.knoxcall.com.evil.com never passes), from the expected iframe window, and only after strict shape validation. Outbound messages are posted with an explicit targetOrigin, never *.
If you override elementBase (the origin the iframe is served from), you must also pass origins — the default allowlist covers production and sandbox only, so messages from any other origin are silently dropped.

Full reference

Component props, the raw postMessage protocol (parseKnoxMessage, isTrustedElementOrigin, KNOX_MESSAGE), and development setup: sdk/knoxcall-browser/README.md and sdk/knoxcall-react/README.md in the monorepo.