---
title: Pulse beta embed starter kit
description: Downloadable starter files for the Pulse beta browser-to-phone embed.
owner: product
status: published
version: 1.3.0
lastReviewed: 2026-06-28
nextReview: 2026-09-28
---

# Pulse beta embed — starter kit

Lock a browser action until the carried phone proves the same human is present. The full guide
lives at <https://kenshikilabs.com/developers/pulse-beta>.

## What's in here

| File | Use it for |
| --- | --- |
| `plain-html.html` | Frontend-only shell that loads the Worker-hosted SDK and calls your backend for a session. |
| `protected-submit.html` | A form whose submit is gated behind a server-minted Pulse session. |
| `react-example.tsx` | The same gated-submit pattern as a React component. |
| `llm-integration-prompt.md` | Paste this into a coding agent so it understands Pulse's trust model and integration shape. |
| `openapi.yaml` | Machine-readable API contract for toolchains that prefer YAML. |
| `openapi.json` | Machine-readable API contract for toolchains that prefer JSON. |

## Before you start

1. A Pulse beta publishable key (`pk_beta_...`) for the browser.
2. A Kenshiki-issued server credential (`PULSE_SERVER_KEY`) for your backend. Do not ship it to the browser.
3. Your HTTPS origins registered with Kenshiki.
4. A backend route that can call the Worker, store `completion_secret`, and return only the public session to the browser.
5. A workflow/action pair enabled for your tenant, for example `credit_application` / `submit_application`.
6. The Pulse iOS app to scan with: <https://testflight.apple.com/join/KcpAvHaf>.

## Endpoints

- SDK script: `https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/v1/pulse.js`
- Session API: `https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1/sessions`

Do not proxy these through the Kenshiki Web/Vercel project. A customer integration should use the
Worker origin returned in the session's `api_base_url` and QR payload.

## Required backend route

Your browser should call your own route, for example `POST /api/pulse/sessions`. That route calls
the Worker with `PULSE_SERVER_KEY`, stores `completion_secret`, and returns only `{ session }`.

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

## Hardened integration checklist

- Load `pulse.js` from the Worker, not from your app bundle.
- Pass `workflow` and `action` at runtime. They are not hard-coded in the SDK.
- Keep the Pulse-owned trust gate visible; do not replace it with a raw QR image.
- Treat `bonded_session_id` as untrusted input until your server verifies it.
- Reject missing, forged, stale, wrong-workflow, wrong-action, and unbonded sessions before processing.
- Make passport a workflow policy. Do not require passport globally for every Pulse user.
- Complete the bond only after the protected action durably succeeds, using the stored server-side `completion_secret`.

## Required trust-gate CSS

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

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.

## Verify the bond on your server — required

```js
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 v = session.verification;
if (!v || !v.device_possession || v.local_auth !== "verified") return reject("weak_device_proof");
if ((v.human_presence_confidence ?? v.assertion_confidence ?? 0) < 0.55) return review("weak_session_assertion");
if (v.carrier?.sim_swap_recent) return review("recent_sim_swap");

// If this workflow requires passport:
if (workflowPolicy.requiresPassport && v.passport?.tier !== "chip") return reject("passport_chip_required");
```

## Complete the bond after the action succeeds

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

## Troubleshooting

| Response | Cause | Fix |
| --- | --- | --- |
| `401 unauthorized` | Missing or wrong server credential. | Use `PULSE_SERVER_KEY` only from your backend. |
| `403 forbidden` | Origin, workflow, or action is not enabled. | Embed only on registered origins; ask Kenshiki to enable the workflow/action. |
| `400 invalid_request` | Missing session id, unsupported scope, malformed JSON, or wrong endpoint. | Use the Worker session endpoint and a tenant-enabled workflow/action. |
| `404 not_found` | Session looked up on the wrong Worker origin, or expired. | Use the `api_base_url` / Worker origin from the session response. |

Questions: <hello@kenshikilabs.com>.
