Skip to content

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 |
| <-------------------------|-------| |
+----------------------------+ +-----------------------------+
  1. Your backend requests a session token from Layr8
  2. Your frontend renders a SecretField — a secure input hosted by Layr8
  3. The user types their API key
  4. Your code calls tokenize() with the session token
  5. The secret is encrypted and stored directly by Layr8
  6. 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

PackagePurposeFramework
@layr8/ztg-jsCore SDK — secure field, tokenizationVanilla JS
@layr8/ztg-reactReact 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

Terminal window
npm install @layr8/ztg-js @layr8/ztg-react
import { 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-session
Content-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 onboarding
const { tokenId } = await ztg.tokenize(sessionToken);
// tokenId = "tok_a1b2c3d4e5f6g7h8"
// Later rotation — same tokenId, new secret
const { tokenId: rotatedTokenId } = await ztg.tokenize(newSessionToken);
// rotatedTokenId === tokenId

Your 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.1
Host: ztg-proxy.your-vpc.internal
Authorization: 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

ThreatHow
XSS on your pageThe secret input is isolated in a separate origin — your JavaScript cannot read it
Malicious dependenciesThe input lives in Layr8’s secure frame, outside your application’s DOM
Man-in-the-middleAll communication is over HTTPS; secrets are never sent to your servers
Stolen session tokensTokens are bound to the originating browser via proof-of-possession and expire after 5 minutes
Secret sent to wrong APIEach secret is bound to a specific target API at tokenization time
Cross-customer accessSecrets 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");
ParameterTypeDescription
apiOriginstringYour 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" },
});
ParameterTypeDescription
selectorstringCSS selector for the container element
options.placeholderstringPlaceholder text for the input
options.labelstringAccessible label for the input
options.styleobjectCSS 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"
});
ParameterTypeDescription
sessionTokenstringSession token from your backend
options.labelstringLabel for multi-key scenarios (default: "default")

Returns: Promise<{ tokenId: string }>

field.destroy()

Removes the field and cleans up resources.

Next Steps