ZTG Web Element
The ZTG Web Element lets your users enter API keys through a secure input field embedded in your app. The secret never touches your JavaScript — it goes directly from the browser to Layr8’s secure infrastructure, and your application only receives an opaque tokenId reference.
Think of it like Stripe Elements for API keys: your users type sensitive values into a field that looks native to your app, but the input is isolated from your code.
How It Works
Your App (your-app.com) Layr8 (js.ztg.layr8.cloud)+----------------------------+ +-----------------------------+| | | || <SecretField /> | iframe| <input type="password"> || +----------------------+ | ----> | || | ******************** | | | Secret stays here || +----------------------+ | | || | | || ztg.tokenize(token) | | Encrypts & stores secret || --------------------------|------>| || | | || Promise<{ tokenId }> | | Returns reference only || <-------------------------|-------| |+----------------------------+ +-----------------------------+- Your backend requests a session token from Layr8
- Your frontend renders a
SecretField— a secure input hosted by Layr8 - The user types their API key
- Your code calls
tokenize()with the session token - The secret is encrypted and stored directly by Layr8
- Your code receives a
tokenId— an opaque reference, not the secret
When your application later needs the API key, it sends requests through the ZTG proxy with ${tokenId} placeholders in headers. Layr8 resolves the placeholder to the real secret and forwards the request to the target API. Your application never handles the plaintext.
Packages
| Package | Purpose | Framework |
|---|---|---|
@layr8/ztg-js | Core SDK — secure field, tokenization | Vanilla JS |
@layr8/ztg-react | React bindings — <ZTGProvider>, <SecretField>, useZTG() | React 18+ |
The npm packages are thin loaders. The SDK loads at runtime from Layr8’s CDN — identical to how @stripe/stripe-js works. Security patches ship instantly without waiting for npm update.
Quick Start
Script Tag (No Build Step)
<script src="https://js.ztg.layr8.cloud/v1/ztg.js"></script><div id="secret-field"></div><button onclick="save()">Save Key</button>
<script> async function init() { const ztg = await ZTG.loadZTG("https://myorg.ztg.layr8.cloud"); const field = ztg.createField("#secret-field", { placeholder: "Enter API key", }); }
async function save() { // sessionToken comes from your backend (see Backend Setup below) const { tokenId } = await ztg.tokenize(sessionToken); // Send tokenId to your backend — it's safe, it's not the secret await fetch("/api/save-key", { method: "POST", body: JSON.stringify({ tokenId }), }); }
init();</script>React
npm install @layr8/ztg-js @layr8/ztg-reactimport { ZTGProvider, SecretField, useZTG } from "@layr8/ztg-react";
function App() { return ( <ZTGProvider apiOrigin="https://myorg.ztg.layr8.cloud"> <KeyOnboarding /> </ZTGProvider> );}
function KeyOnboarding() { const ztg = useZTG();
const handleSubmit = async () => { const { tokenId } = await ztg.tokenize(sessionToken); // Store tokenId in your database — never the secret };
return ( <form onSubmit={handleSubmit}> <SecretField placeholder="Enter API key" /> <button type="submit">Save</button> </form> );}Backend Setup
Before the frontend can tokenize, your backend creates a session token. Session tokens are short-lived (5 minutes) and scoped to a specific customer. A single session token can be used for multiple tokenize() calls — useful for multi-key onboarding flows.
POST /ztg/tokenize-sessionContent-Type: application/json
{ "salted_customer_id": "c8f3e2a1b4d5f6e7..."}Response:
{ "session_token": "eyJhbGciOiJFZERTQSIs..."}Pass the session_token to your frontend. The frontend passes it to tokenize().
The salted_customer_id is a SHA-256 hash of a salt plus your internal customer identifier. Layr8 never sees your raw customer IDs.
Example Backend Endpoint (Node.js)
app.post("/api/onboarding/session", async (req, res) => { // Hash the customer ID — Layr8 never sees the raw ID const saltedId = crypto .createHash("sha256") .update(SALT + req.user.id) .digest("hex");
// Request a session token from the ZTG proxy in your infrastructure const response = await fetch(`${ZTG_PROXY_URL}/ztg/tokenize-session`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ salted_customer_id: saltedId }), });
const { session_token } = await response.json(); res.json({ sessionToken: session_token });});Multi-Key Onboarding
Capture multiple API keys in a single form using one session token:
function MultiKeyForm({ apis, sessionToken }) { const ztg = useZTG();
const handleSubmit = async () => { for (const api of apis) { await ztg.tokenize(sessionToken, { label: api.label }); } };
return ( <form onSubmit={handleSubmit}> {apis.map((api) => ( <div key={api.label}> <label>{api.displayName}</label> <SecretField id={api.label} placeholder={api.placeholder} /> </div> ))} <button>Save All Keys</button> </form> );}The label parameter differentiates keys for the same customer and API. For example, use "read" and "write" to capture separate keys with different permission levels.
Key Rotation
When a user rotates their API key, create a new session token and call tokenize() again. Layr8 uses deterministic token IDs — same customer, same API, same label always produces the same tokenId. The old secret is replaced; no code changes needed on your end.
// First onboardingconst { tokenId } = await ztg.tokenize(sessionToken);// tokenId = "tok_a1b2c3d4e5f6g7h8"
// Later rotation — same tokenId, new secretconst { tokenId: rotatedTokenId } = await ztg.tokenize(newSessionToken);// rotatedTokenId === tokenIdYour backend stores the tokenId once. When the secret changes, the same tokenId resolves to the new value automatically.
Using Stored Keys
After tokenization, your application makes API calls through the ZTG proxy with ${tokenId} placeholders in the headers:
GET /v1/charges HTTP/1.1Host: ztg-proxy.your-vpc.internalAuthorization: Bearer ${tok_a1b2c3d4e5f6g7h8}X-Ztg-Customer-Id: c8f3e2a1b4d5f6e7...Layr8 resolves ${tok_a1b2c3d4e5f6g7h8} to the actual API key and forwards the request to the target API. Your application never sees the plaintext secret.
Security
What the Web Element Protects Against
| Threat | How |
|---|---|
| XSS on your page | The secret input is isolated in a separate origin — your JavaScript cannot read it |
| Malicious dependencies | The input lives in Layr8’s secure frame, outside your application’s DOM |
| Man-in-the-middle | All communication is over HTTPS; secrets are never sent to your servers |
| Stolen session tokens | Tokens are bound to the originating browser via proof-of-possession and expire after 5 minutes |
| Secret sent to wrong API | Each secret is bound to a specific target API at tokenization time |
| Cross-customer access | Secrets are scoped per-customer — one customer cannot access another’s keys |
Limitations
These limitations apply to any browser-based input and are outside the ZTG threat model:
- Browser DevTools — A privileged debugging tool with full access to the page
- Malicious browser extensions — Extensions with broad permissions can read any input on any page
- Compromised browser — If the browser itself is compromised, no web-based protection is possible
- Physical keyloggers — Hardware-level input capture
How Secrets Are Protected
Secrets are encrypted with HSM-backed keys and stored in hardware-isolated infrastructure. Your application, your servers, and Layr8’s application layer never have access to plaintext secrets. Secrets are decrypted only at the moment they are injected into outbound API requests, inside isolated hardware.
API Reference
loadZTG(apiOrigin)
Loads the SDK and returns a ZTG instance.
const ztg = await ZTG.loadZTG("https://myorg.ztg.layr8.cloud");| Parameter | Type | Description |
|---|---|---|
apiOrigin | string | Your organization’s ZTG endpoint |
ztg.createField(selector, options)
Mounts a secure input field into a DOM element.
const field = ztg.createField("#container", { placeholder: "Enter API key", label: "OpenAI API Key", style: { fontSize: "16px" },});| Parameter | Type | Description |
|---|---|---|
selector | string | CSS selector for the container element |
options.placeholder | string | Placeholder text for the input |
options.label | string | Accessible label for the input |
options.style | object | CSS properties applied to the input |
field.on(event, callback)
Listens for field state changes. The callback receives state information — never the secret value.
field.on("change", (state) => { console.log(state.empty); // true if the field is empty console.log(state.focused); // true if the field has focus});ztg.tokenize(sessionToken, options?)
Encrypts and stores the secret from the active field. Returns an opaque token reference.
const { tokenId } = await ztg.tokenize(sessionToken, { label: "default", // optional, defaults to "default"});| Parameter | Type | Description |
|---|---|---|
sessionToken | string | Session token from your backend |
options.label | string | Label for multi-key scenarios (default: "default") |
Returns: Promise<{ tokenId: string }>
field.destroy()
Removes the field and cleans up resources.
Next Steps
- Architecture — How Layr8 fits into your infrastructure
- Getting Started — Set up your first Layr8 node
- Boundaries & Assumptions — What Layr8 guarantees and what it doesn’t