Quickstart
Get a key, prove it with curl, then wire the bond. Swap the demo workflow for the one Kenshiki enabled for your tenant.
1 · Get a key. Nothing works without one — and Pulse is a private beta, so keys are issued manually after review, not self-serve. Request access with your origins and protected action, then wait for Kenshiki to approve and issue your keys. Once approved, export the server key your backend uses and keep it server-side.
# Kenshiki issues two credentials:
# pk_beta_… publishable key — safe in the browser, locked to your origins
# server key backend only — mints and verifies sessions
export PULSE_SERVER_KEY="server-key-from-kenshiki" 2 · Confirm the key works. Mint a session straight from the Worker with curl, before writing any app code. 200 with a session means your key and origin are live; 401/403 means access is not approved yet.
# Prove the key works before writing any app code:
curl -sS -X POST https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions \
-H "Authorization: Bearer $PULSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"publishableKey":"pk_beta_issued_by_kenshiki","workflow":"credit_application","action":"submit_application"}'
# 200 → { "session": { "id": "…", "state": "pending" } }
# 401 / 403 → key or origin not approved yet 3 · Load the SDK and declare the gate. From the Worker; runtime values come from init(), not your env system.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pulse embed</title>
</head>
<body>
<!-- Put this where the Pulse-owned trust gate should appear. -->
<div id="pulse-trust-gate"></div>
<!-- Load the Worker-hosted SDK once. Do not self-host or proxy it through your web app. -->
<script src="https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js" defer></script>
<!-- Your backend creates the short-lived session; the browser only renders and watches it. -->
<script type="module">
const PULSE_WORKFLOW = "credit_application"; // your tenant-scoped workflow, not SDK-hard-coded
const PULSE_ACTION = "submit_application"; // the exact protected action
async function startPulseGate() {
const created = await fetch("/api/pulse/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ workflow: PULSE_WORKFLOW, action: PULSE_ACTION }),
}).then((response) => {
if (!response.ok) throw new Error("pulse_session_create_failed");
return response.json();
});
const pulse = window.KenshikiPulse.init({
publishableKey: "pk_beta_issued_by_kenshiki",
workflow: PULSE_WORKFLOW,
containerId: "pulse-trust-gate",
sessionEndpoint: "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions",
});
return pulse.requireBond({ session: created.session, action: PULSE_ACTION });
}
startPulseGate().catch((error) => {
document.getElementById("pulse-trust-gate").textContent =
error instanceof Error ? error.message : "Pulse could not start.";
});
</script>
</body>
</html> 4 · Mint a session on your backend. The completion_secret stays server-side; the browser gets only the public session object.
// Your backend: POST /api/pulse/sessions
const PULSE_SESSION_ENDPOINT = process.env.PULSE_SESSION_ENDPOINT ?? "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions";
const PULSE_SERVER_KEY = mustGetEnv("PULSE_SERVER_KEY"); // issued by Kenshiki; never sent to the browser
app.post("/api/pulse/sessions", async (req, res) => {
const workflow = req.body.workflow;
const action = req.body.action;
if (workflow !== "credit_application" || action !== "submit_application") {
return res.status(400).json({ error: "unsupported_pulse_scope" });
}
const pulseRes = await fetch(PULSE_SESSION_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + PULSE_SERVER_KEY,
},
body: JSON.stringify({ workflow, action }),
});
const session = await pulseRes.json();
if (!pulseRes.ok) return res.status(pulseRes.status).json(session);
await pulseSessionStore.save({
id: session.id,
workflow,
action,
expiresAt: session.expires_at,
completionSecret: session.completion_secret,
});
const { completion_secret: _completionSecret, ...publicSession } = session;
return res.status(201).json({ session: publicSession });
}); 5 · Require the bond on submit.
<!doctype html>
<html lang="en">
<body>
<form id="application-form" action="/applications" method="post">
<!-- Keep your existing fields and validation. -->
<input type="hidden" name="bonded_session_id" />
<div id="pulse-trust-gate"></div>
<button type="submit">Submit application</button>
</form>
<script src="https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js"></script>
<script>
const EXPECTED_WORKFLOW = "credit_application";
const EXPECTED_ACTION = "submit_application";
const form = document.getElementById("application-form");
const pulse = window.KenshikiPulse.init({
publishableKey: "pk_beta_issued_by_kenshiki",
workflow: EXPECTED_WORKFLOW,
containerId: "pulse-trust-gate",
sessionEndpoint: "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions",
});
async function createPulseSession() {
const response = await fetch("/api/pulse/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ workflow: EXPECTED_WORKFLOW, action: EXPECTED_ACTION }),
});
if (!response.ok) throw new Error("pulse_session_create_failed");
return response.json(); // { session } without completion_secret
}
form.addEventListener("submit", async (event) => {
if (!form.checkValidity()) return;
event.preventDefault();
const submitter = event.submitter;
if (submitter) submitter.disabled = true;
try {
const created = await createPulseSession();
const bond = await pulse.requireBond({
session: created.session,
action: EXPECTED_ACTION,
context: { form_id: form.id || "application-form" },
});
if (bond.state !== "bonded" || !bond.sessionId) throw new Error("pulse_not_bonded");
form.elements.bonded_session_id.value = bond.sessionId;
// Submit to YOUR backend. It verifies and completes the bond server-side.
const res = await fetch("/applications", { method: "POST", body: new FormData(form) });
if (!res.ok) throw new Error("application_submit_failed");
} finally {
if (submitter) submitter.disabled = false;
}
});
</script>
</body>
</html> 6 · Verify on your server Required — enforce state, freshness, workflow, action, and evidence policy before trusting the submit.
// Your backend, handling POST /applications:
const bondedSessionId = body.bonded_session_id;
if (!bondedSessionId) return reject("missing_pulse_session");
const res = await fetch(PULSE_SESSION_ENDPOINT + "/" + encodeURIComponent(bondedSessionId), {
cache: "no-store",
headers: { Authorization: "Bearer " + PULSE_SERVER_KEY },
});
const session = await res.json();
if (!res.ok || session.state !== "bonded") return reject("unverified");
if (session.workflow !== "credit_application") return reject("wrong_workflow");
if (session.action !== "submit_application") return reject("wrong_action");
if (Date.parse(session.expires_at) <= Date.now()) return reject("expired");
const stored = await pulseSessionStore.get(bondedSessionId);
if (!stored || stored.workflow !== session.workflow || stored.action !== session.action) {
return reject("unknown_or_mismatched_session");
}
const v = session.verification;
if (!v) return reject("missing_verification");
if (!v.device_possession) return reject("device_unverified");
if (v.local_auth !== "verified") return reject("local_auth_failed");
if ((v.human_presence_confidence ?? v.assertion_confidence ?? 0) < 0.55) {
return flagForReview("weak_session_assertion");
}
if (v.carrier?.sim_swap_recent) return flagForReview("recent_sim_swap");
if ((v.sensor_continuity?.continuity_score ?? 0) < 0.6) return flagForReview("weak_continuity");
// Passport is a workflow policy, not a global requirement.
if (workflowPolicy.requiresPassport && v.passport?.tier !== "chip") {
return reject("passport_chip_required");
}
// Safe to process. After the action commits, call /form-completed with the stored completion secret. Prerequisites
- A beta publishable key for the browser and a server credential for session creation.
- Your production and staging HTTPS origins, registered with Kenshiki.
- A backend route that mints a short-lived session and keeps the completion secret server-side.
- A workflow/action pair, e.g.
credit_application/submit_application— tenant values, not SDK constants. - The Pulse iOS app to test scanning — join the TestFlight.
Current Worker script: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js. Session endpoint: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions.
UI contract
Your page owns the surrounding form; Pulse owns the QR, phone handoff, help, fail-closed state, and proof status inside the gate. Keep it visible and stable — never render qr_svg_url as a bare image in production.
/* Required: reserve visible space for the Pulse-owned trust gate. */
#pulse-trust-gate,
[data-pulse-widget] {
display: block;
min-height: 168px;
max-width: 360px;
}
/* Optional: theme around Pulse, without hiding/rewording its state. */
.application-form #pulse-trust-gate {
margin-block: 16px;
}
/* Do not set display:none, visibility:hidden, opacity:0, pointer-events:none,
position:absolute off-screen, or transform scaling on the Pulse gate. */ Close the bond
A bond is opened for one action. Once it durably succeeds, close it; the session moves bonded → completed and the phone Pulse drawer flips to success.
// Your backend, after the protected action has durably succeeded:
const stored = await pulseSessionStore.get(bondedSessionId);
if (!stored?.completionSecret) return reject("missing_completion_secret");
await fetch(
PULSE_SESSION_ENDPOINT + "/" + encodeURIComponent(bondedSessionId) + "/form-completed",
{
method: "POST",
headers: {
Authorization: "Bearer " + PULSE_SERVER_KEY,
"X-Pulse-Completion-Secret": stored.completionSecret,
},
},
);
// The session moves bonded -> completed and the phone's Pulse drawer closes on success. Testing
- Replace
pk_beta_issued_by_kenshikiwith your publishable key; keepPULSE_SERVER_KEYandcompletion_secretserver-side. - Serve from a registered HTTPS origin and confirm
api_base_urlishttps://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev. - Scan the QR with the Pulse app — state moves
pending → bonded. - Confirm your server rejects missing, expired, wrong-workflow, wrong-action, and unbonded session ids.
- After your action succeeds, complete the bond and watch the phone show the completed state.
Partner keys are origin-scoped. Test a real key by serving the starter from your registered origin and creating the session from your backend; this page does not proxy session creation through Web.
Reference
Session object
Returned by POST /api/v1/sessions and GET /api/v1/sessions/:id.
{
"id": "sess_9f2c...",
"state": "bonded",
"action": "submit_application",
"workflow": "credit_application",
"tenant_id": "example-bank",
"api_base_url": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev",
"qr_payload": "pulse://bond?session_id=sess_9f2c...&nonce=nonce_abc...",
"qr_svg_url": "/v1/sessions/sess_9f2c.../qr.svg?nonce=nonce_abc...",
"universal_link": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/bond?session_id=sess_9f2c...&nonce=nonce_abc...",
"websocket_url": "wss://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/sessions/sess_9f2c...",
"expires_at": "2026-06-28T22:40:00.000Z",
"verification": {
"device_possession": true,
"local_auth": "verified",
"assertion_confidence": 0.91,
"human_presence_confidence": 0.91,
"assertion_band": "strong",
"continuity_score_long_lived": 0.84,
"carrier": {
"number_verified": true,
"sim_swap_recent": false,
"device_status": "active"
},
"sensor_continuity": {
"motion_present": true,
"continuity_score": 0.84
},
"passport": {
"tier": "chip"
}
}
} id- Session id (sess_...). The browser puts this in bonded_session_id; your server still treats it as untrusted input until it verifies the Worker receipt.
state- pending -> bonded once the phone signs the proof; then completed after the protected action succeeds, or expired/killed. Trust only fresh bonded sessions before processing.
api_base_url- The Worker origin that minted the session. The QR/app must come back to this same origin; do not route through the Web/Vercel project.
workflow- Your tenant-scoped workflow, such as credit_application, account_opening, account_recovery, or wire_transfer. It is not hard-coded in the SDK.
action- The single protected action the bond unlocks, such as submit_application or approve_wire.
qr_payload / qr_svg_url / universal_link / websocket_url- Handoff and real-time channel values consumed by the hosted widget. Do not render qr_svg_url as a bare production QR.
completion_secret- Returned only when the session is created. In the production shape, your backend stores it and never sends it to the browser.
expires_at- ISO timestamp. Reject the submit if it has passed.
tenant_id- Your tenant id. Confirm it matches your account where present.
verification- The bounded proof summary, present only when bonded: device_possession, local_auth, assertion_confidence / human_presence_confidence, assertion_band, carrier, sensor_continuity, and optional passport.tier when that workflow asks for passport evidence.
Error responses
unauthorized- When: No credential was sent, the server credential is wrong, or a browser-only key tried to create a session in a server-authoritative beta flow.
Fix: Mint sessions from your backend with the Kenshiki-issued server credential. Keep publishable keys in the browser and server credentials on the server. forbidden- When: The origin, tenant, workflow, or action is not enabled for the key/credential.
Fix: Embed only on registered HTTPS origins. Ask Kenshiki to add an origin or enable a workflow/action. invalid_request- When: Missing session id, unsupported workflow/action, malformed JSON, or a request sent to the wrong endpoint.
Fix: Use https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions for session create/get/complete and pass the workflow/action enabled for your tenant. not_found- When: The app or browser looked up a session on a different Worker origin than the one that minted it, or the session expired.
Fix: Use the api_base_url/Worker origin returned on session creation; do not send QR/app traffic through the Web or Vercel project.
Content Security Policy
Content-Security-Policy:
script-src 'self' https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev;
connect-src 'self' https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev wss://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev;
img-src 'self' data: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev; Tenant & Worker configuration
{
"environment": "production",
"workerBaseUrl": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev",
"scriptUrl": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js",
"sessionEndpoint": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions",
"publishableKey": "pk_beta_issued_by_kenshiki",
"serverCredential": "PULSE_SERVER_KEY",
"tenantId": "example-bank",
"allowedOrigins": [
"https://www.examplebank.com",
"https://staging.examplebank.com"
],
"workflows": [
{
"name": "credit_application",
"actions": [
"submit_application"
],
"passport": "optional"
}
],
"notes": [
"The customer passes workflow/action at runtime.",
"The Worker validates that the key is allowed to use those values.",
"Do not expose the server credential or completion_secret to the browser."
]
} Agent-ready files
Hand your coding agent the embed full API contract.