Getting Started
This is the SDK for driving Hapbeat from JavaScript / TypeScript (npm @hapbeat/sdk).
It targets WebXR, three.js / Babylon.js, p5.js, jsPsych experiments, Electron, Node servers, and more.
It separates the fire side (when and where to play) from the tuning side (what and how to play),
tying them together by event id alone.
One API, three transports
Section titled “One API, three transports”There is a single connect(), but the transport switches automatically depending on
the runtime (determined by the package’s exports map).
- Node (Electron / server / CLI / creative coding) → sends Wi-Fi UDP broadcasts directly.
- React Native (Android / iOS mobile apps) → a phone is not sandboxed like a browser,
so it can open a real UDP socket. It sends Wi-Fi UDP broadcasts directly through
react-native-udp, so no hapbeat-helper is needed. - Browser (WebXR / three.js / p5.js / React / jsPsych) → since browsers cannot open
a raw UDP socket, it relays over WebSocket (
ws://localhost:7703) to the locally running hapbeat-helper.
The code is the same in every case (connect() → play(id)). For the differences and
constraints between transports, see Transports — Node UDP / React Native UDP / Browser helper.
Installation
Section titled “Installation”npm install @hapbeat/sdkIt is ESM-only ("type": "module"). If you use the Browser path, the helper daemon
is required.
pip install hapbeat-helper # install oncehapbeat-helper # and keep it runningYour first event (Node)
Section titled “Your first event (Node)”import { connect } from "@hapbeat/sdk";
const hb = await connect({ appName: "MyApp" }); // UDP broadcast + keep-alivehb.play("sample-kit.sine_100hz", { gain: 0.3 }); // fire by event id (gain is 0..1)hb.play("sample-kit.sine_100hz"); // gain omitted → kit / EventMap defaulthb.stopAll();await hb.close();connect()opens a UDP broadcast socket and sends keep-alives to show the app name (appName, up to 16 characters) on the device OLED.play(eventId, opts)is a fire-and-forget call that sends a playback instruction.gainis 0..1 (clamped by the SDK). If omitted, the EventMap described below supplies the default (the kit’s intensity).- Always
await hb.close()on shutdown. It tells the device the app has left and cancels any streams currently playing.
"sample-kit.sine_100hz" must be an event id contained in a kit deployed to the device
(written via Hapbeat Studio). The SDK only sends instructions;
the waveform lives in the kit on the device (command mode). For clip mode, where the waveform
is sent from the SDK, see Command vs Clip.
Your first event (Browser)
Section titled “Your first event (Browser)”The code is the same in the browser, but the helper must be running
(connect() rejects if it cannot reach ws://localhost:7703).
import { connect } from "@hapbeat/sdk";
const hb = await connect({ appName: "MyWebXR" }); // → ws://localhost:7703 (helper)hb.play("sample-kit.sine_100hz", { gain: 0.5 });The bundler picks the browser build automatically, and the helper performs the UDP broadcast
on your behalf. Pass onConnectionLost to react when the helper goes down. The browser-specific
constraints (clip playback reaches every device the helper knows about; targetTimeUs is ignored)
are summarized in Transports — Node UDP / React Native UDP / Browser helper.
It works the same in React. Call connect() exactly once (in an effect or a module-level
singleton), then just call hb.play(...) from your event handlers.
Your first event (React Native)
Section titled “Your first event (React Native)”The code is the same in an Android / iOS mobile app, sending UDP directly without the
helper through react-native-udp (verified on a physical Android device, RN 0.86 / Hermes).
const hb = await connect({ appName: "MyApp" });hb.play("sample-kit.sine_100hz", { gain: 0.5 });For setup (installing react-native-udp / fast-text-encoding, the metro.config.js
resolver, and Android / iOS permissions), see
Transports — Node UDP / React Native UDP / Browser helper.
Discovering devices
Section titled “Discovering devices”for (const d of await hb.discover(1500)) { console.log(d.ip, d.address, d.firmwareVersion);}discover(timeoutMs = 1500) collects devices via broadcast PING / PONG (not mDNS).
Separating the fire side from the tuning side (EventMap)
Section titled “Separating the fire side from the tuning side (EventMap)”Instead of writing “haptic tuning values” such as intensity into your firing code, collect
them in the kit manifest (= EventMap). play("id") resolves the defaults from there.
import { connect, EventMap } from "@hapbeat/sdk";
const manifest = await fetch("/my-kit/my-kit-manifest.json").then((r) => r.json());const hb = await connect({ eventMap: EventMap.fromManifest(manifest) });hb.play("sample-kit.sine_100hz"); // fires with the intensity from the kit manifestYou can swap “when to play (code)” and “how strong (kit)” independently. See EventMap reference for details.
Specifying a target
Section titled “Specifying a target”hb.play("sample-kit.sine_100hz", { target: "player_1/chest" }); // one devicehb.play("sample-kit.sine_100hz", { target: "*/chest" }); // all chest deviceshb.play("sample-kit.sine_100hz", { target: "" }); // broadcast to all (default)The target resolution order is “the target at call time” → “connect()’s defaultTarget”.
"" is a broadcast. For the notation (player_1/chest / */chest / group_<N>), see
Address System.
Read next
Section titled “Read next”- Transports — Node UDP / React Native UDP / Browser helper — differences and constraints of Node (UDP) vs Browser (helper WS)
- Command vs Clip — choosing between command and clip (Fire vs. Clip)
- EventMap reference — resolving default intensity from the kit manifest (Event ID and Kit)
- Project Structure — how to lay out kits and clips in your project
- Examples — a walkthrough of working samples