Pulse beta

Embed Pulse before one high-risk action.

Start with one high-risk action. The hosted gate handles the phone handoff, but your server remains the enforcement point before the action is accepted.

Hardened gate model

Treat the embed as a protected server workflow, not a client widget.

The public demo now follows the same boundary we expect partners to use: your backend creates short-lived Worker sessions, the browser can only render and ask for a phone bond, and the backend verifies the result before accepting the protected action.

Origin and action scoped

Beta keys and server credentials are issued for named origins, workflows, and actions. A bond for one submit should not unlock a different flow.

The browser is not trusted

The hosted gate owns the QR, phone handoff, status, help, and fail-closed UI. Your server still rejects missing, forged, stale, or wrong-action session ids.

Completion follows the real commit

For high-stakes flows, keep the completion capability server-side and close the bond only after your protected action durably succeeds.

Access path

What we need to issue a beta key.

The action you want to protect

Name the workflow and exact moment Pulse guards, such as credit application submit.

Your approved origins

Send production and staging domains. The beta key only works from origins Kenshiki approves.

Your server boundary

Your backend must mint the Pulse session, verify the returned receipt, enforce freshness and action scope, and complete the bond only after the action succeeds.

Agent-ready integration

Give your developer or coding agent the whole Pulse context.

Copy these files into your developer workflow. They define the beta API contract, browser embed point, and required server verification.

---
title: Pulse LLM Integration Prompt
description: Agent-ready instructions for integrating the Pulse beta embed into an existing customer form.
owner: product
status: beta
version: 1.3.0
lastReviewed: 2026-06-28
nextReview: 2026-09-28
---

# Prompt for LLM: integrate Kenshiki Pulse into an existing form

You are an implementation agent integrating Kenshiki Pulse into an existing customer-owned web form
and backend workflow.

Do not build a new application unless the customer explicitly asks for a standalone sample. Preserve
the customer's current form, validation, styling, analytics, accessibility behavior, CSRF handling,
and backend submit path while adding Pulse immediately before the protected action proceeds.

## What Pulse is

Pulse is not OTP, 2FA, push approval, GPS tracking, reCAPTCHA, or an identity-file score.

Pulse proves continuity of life from the applicant's carried phone: private presence, motion, device
physics, radios, and day rhythm that support a real human behind the application. The browser is not
the root of trust. The server is the enforcement point.

## Current production integration shape

Use the Worker-first, server-authoritative shape:

1. Browser loads `https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js`.
2. Browser asks the customer's backend for a Pulse session, for example `POST /api/pulse/sessions`.
3. Customer backend calls the Pulse Worker session endpoint with its server credential.
4. Customer backend stores `completion_secret` server-side and returns only `{ session }` to the browser.
5. Browser calls `window.KenshikiPulse.init(...)`.
6. Browser calls `pulse.requireBond({ session, action })`.
7. Browser includes `bonded_session_id` in the protected submit.
8. Customer backend fetches the Worker receipt, validates state/freshness/workflow/action/evidence, and only then processes the action.
9. Customer backend completes the bond after the action durably succeeds.

Do not route session creation through the Kenshiki Web/Vercel project. Do not rely on `VITE_*` or
host build env vars. A customer's runtime config is the publishable key, workflow, action, and their
own backend route.

## Customer must provide before implementation

Ask for these values if they are not already present:

- Framework and runtime: plain HTML, React, Next.js, Astro, Rails, Laravel, Django, etc.
- Existing form selector or component name.
- Existing submit endpoint and HTTP method.
- Existing client-side validation behavior that must remain intact.
- Existing server-side handler that processes the protected action.
- Protected workflow name, for example `credit_application`, `benefits_claim`, `account_recovery`,
  `wire_transfer`, or `job_application`.
- Protected action name, for example `submit_application`, `submit_claim`, `recover_account`, or `approve_wire`.
- Pulse publishable key for the browser, for example `pk_beta_...`.
- Pulse server credential env var, for example `PULSE_SERVER_KEY`.
- Approved production and staging origins.
- What should happen if Pulse cannot bond: block, show retry, route to manual review, or save draft.
- Whether this workflow requires passport. Passport must not be globally mandatory for every Pulse user.
- Where to store the verified `bonded_session_id` and bounded verification summary for audit.

If the customer cannot provide an answer, make the safest assumption and clearly mark it in a short
integration note. Never bypass server verification.

## Required frontend shape

1. Keep the existing form.
2. Add a hidden `bonded_session_id` field.
3. Add a Pulse container near the protected submit area.
4. Reserve visible space for that container with CSS. Do not hide or rewrite the Pulse status.
5. Load the Worker-hosted `pulse.js` once.
6. Initialize `window.KenshikiPulse` with `publishableKey`, `workflow`, `containerId`, and
   `sessionEndpoint`.
7. After normal client-side validation, call the customer's backend to create a Pulse session.
8. Call `pulse.requireBond({ session, action })`.
9. If bonded, write `result.sessionId` into the hidden field and continue the existing submit.
10. Do not call `bond.complete()` in the production shape. Completion belongs on the backend after
    the protected action commits.

## Required backend shape

Add a route like `POST /api/pulse/sessions`:

```js
const PULSE_SESSION_ENDPOINT =
  process.env.PULSE_SESSION_ENDPOINT ??
  "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions";
const PULSE_SERVER_KEY = process.env.PULSE_SERVER_KEY;

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 });
});
```

## Required trust-gate CSS

```css
#pulse-trust-gate,
[data-pulse-widget] {
  display: block;
  min-height: 168px;
  max-width: 360px;
}
```

Do not render `qr_svg_url` as a bare image in production. Do not apply `display: none`,
`visibility: hidden`, `opacity: 0`, `pointer-events: none`, off-screen positioning, clipping, or
transform scaling to the Pulse gate or its children.

## Frontend code for a plain existing HTML form

Replace the key, workflow, action, and form id with the customer's values.

```html
<input type="hidden" name="bonded_session_id" />
<div id="pulse-trust-gate"></div>

<script src="https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js"></script>
<script>
  const form = document.getElementById("application-form");
  const pulse = window.KenshikiPulse.init({
    publishableKey: "pk_beta_issued_by_kenshiki",
    workflow: "credit_application",
    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: "credit_application", action: "submit_application" }),
    });
    if (!response.ok) throw new Error("pulse_session_create_failed");
    return response.json();
  }

  form.addEventListener("submit", async (event) => {
    if (!form.checkValidity()) return;
    event.preventDefault();

    const created = await createPulseSession();
    const result = await pulse.requireBond({
      session: created.session,
      action: "submit_application",
      context: { form_id: form.id || "application-form" },
    });

    if (result.state !== "bonded" || !result.sessionId) return;
    form.elements.bonded_session_id.value = result.sessionId;

    HTMLFormElement.prototype.submit.call(form);
  });
</script>
```

## Backend verification code

Use this logic in the customer's existing protected-action handler before creating the application,
approving the action, moving money, opening the account, or sending the record to underwriting.

```ts
type PulseSession = {
  id: string;
  state: "pending" | "bonded" | "step_up_required" | "completed" | "killed" | "expired";
  workflow: string | null;
  action: string | null;
  api_base_url?: string;
  expires_at: string;
  verification: null | {
    device_possession: boolean;
    local_auth: "verified" | "failed";
    assertion_confidence?: number;
    human_presence_confidence?: number;
    assertion_band?: "strong" | "good" | "forming" | "limited" | "review";
    continuity_score_long_lived?: number;
    carrier?: {
      number_verified: boolean;
      sim_swap_recent: boolean;
      device_status: "active" | "inactive" | "unknown";
    };
    sensor_continuity?: {
      motion_present: boolean;
      continuity_score: number;
    };
    passport?: {
      tier?: "none" | "document" | "chip";
    };
  };
};

async function verifyPulseBond(input: {
  bondedSessionId: string | undefined;
  expectedWorkflow: string;
  expectedAction: string;
  requiresPassport?: boolean;
}): Promise<PulseSession> {
  if (!input.bondedSessionId) throw new Error("missing_pulse_session");

  const response = await fetch(
    `${PULSE_SESSION_ENDPOINT}/${encodeURIComponent(input.bondedSessionId)}`,
    {
      cache: "no-store",
      headers: { Authorization: `Bearer ${process.env.PULSE_SERVER_KEY}` },
    },
  );
  const session = (await response.json()) as PulseSession;

  if (!response.ok) throw new Error("pulse_lookup_failed");
  if (session.state !== "bonded") throw new Error("unbonded_pulse_session");
  if (session.workflow !== input.expectedWorkflow) throw new Error("wrong_pulse_workflow");
  if (session.action !== input.expectedAction) throw new Error("wrong_pulse_action");
  if (Date.parse(session.expires_at) <= Date.now()) throw new Error("expired_pulse_session");

  const verification = session.verification;
  if (!verification) throw new Error("missing_pulse_verification");
  if (!verification.device_possession) throw new Error("device_not_possessed");
  if (verification.local_auth !== "verified") throw new Error("local_auth_failed");
  if ((verification.human_presence_confidence ?? verification.assertion_confidence ?? 0) < 0.55) {
    throw new Error("weak_session_assertion");
  }
  if (verification.carrier?.sim_swap_recent) throw new Error("recent_sim_swap");
  if ((verification.sensor_continuity?.continuity_score ?? 0) < 0.6) {
    throw new Error("weak_continuity");
  }
  if (input.requiresPassport && verification.passport?.tier !== "chip") {
    throw new Error("passport_chip_required");
  }

  return session;
}
```

## Complete the bond

After the protected action durably succeeds, complete the Pulse session with the stored
`completion_secret`.

```js
const stored = await pulseSessionStore.get(bondedSessionId);
await fetch(`${PULSE_SESSION_ENDPOINT}/${encodeURIComponent(bondedSessionId)}/form-completed`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PULSE_SERVER_KEY}`,
    "X-Pulse-Completion-Secret": stored.completionSecret,
  },
});
```

## UX and copy constraints

- Do not call Pulse a one-time code, 2FA, phone confirmation, push approval, CAPTCHA, or GPS check.
- Say Pulse proves continuity of life, presence, or existence behind the application.
- Desktop should show the Pulse QR inside the configured container.
- Mobile should show the Pulse handoff / Continue with Pulse path, not QR.
- Pulse owns the help link, reassurance copy, and status inside its container.

Step 1

Request a publishable key

Request a Pulse beta key for exact origins you control. Kenshiki reviews production FI requests before activation.

  • The key is not a secret; it ships in your page but only works from approved origins.
  • It is scoped to approved workflows and actions, such as credit_application.
  • localhost is enabled only for the Kenshiki demo key, not partner keys.

Request received. We will review origins, workflow, and license acknowledgement.

Exact HTTPS origin only. No paths or wildcards.

Leave blank only if staging is not available yet.

Only required if workflow is Other.

Only required if action is Other.

Optional. Share only if useful.

What browser action will Pulse gate? No customer/member data.

Optional. Keep it high level.

Use only from approved origins. Production FI use requires written Kenshiki approval.

By submitting, you agree to the Terms and Privacy Notice.

Prefer email? Send the same details to hello@kenshikilabs.com.

Email instead