Commercial screen recording SDK for Electron, Swift, Tauri, and Node apps.
The SDK exposes the capture primitives from the native screenpipe stack: record an MP4, grab JPEG preview snapshots, read a mic level for preflight UI, and inspect the focused app.
| Surface | Source | Example |
|---|---|---|
| Node | index.js, index.d.ts | examples/record-10s.mjs |
| Electron | electron, session | examples/electron-app |
| Swift | Package.swift, Sources/Screenpipe | examples/swift-app |
| Tauri | tauri | examples/tauri-app |
Detailed embed notes live in docs/integration.md.
| Electron | Swift | Tauri |
|---|---|---|
![]() |
![]() |
![]() |
| examples/electron-app | examples/swift-app | examples/tauri-app |
See examples/README.md for run commands and smoke checks for all three apps.
npm install @screenpipe/sdk
# or
bun add @screenpipe/sdkThis package is source-available under the Screenpipe Commercial License, the same license as the root repository. See LICENSE.md at the repository root (bundled into the npm package at publish time).
import { Recorder, requestPermissions } from "@screenpipe/sdk";
const permissions = await requestPermissions();
if (!permissions.screen) {
throw new Error("Screen Recording permission is required");
}
const recorder = new Recorder({
output: "/tmp/session.mp4",
// Optional privacy filters — recording pauses (hard cut in the MP4)
// while a matching window/URL is focused.
// Plain strings match anywhere; `App::Title` scopes to one window of one app.
ignoredWindows: ["1password", "private", "Slack::#hr"],
ignoredUrls: ["wellsfargo.com", "chase"],
});
await recorder.start();
// ... user does stuff ...
await recorder.stop();-
options.output(string, required): path where the MP4 is written. -
options.monitorId(number, optional): display id; defaults to the primary display. -
options.microphone(boolean, optional): accepted for forward compatibility. -
options.systemAudio(boolean, optional): accepted for forward compatibility. -
options.ignoredWindows(string[], optional): substring patterns matched case-insensitively against the focused app name and window title. While a matching window is in focus, the recorder skips writing frames — the MP4 contains a hard cut over the filtered period. Mirrors the engine's--ignored-windowsCLI flag.Each pattern may use an optional
App::Titlescope:"Slack::#hr"skips only the #hr window inside Slack and leaves other Slack channels recording."::Confidential"matches any app whose title contains "Confidential". Plain"Slack"keeps the legacy "app OR title contains" behavior. -
options.includedWindows(string[], optional): substring whitelist. If non-empty, frames are written ONLY while a matching window is focused. Scoped entries ("Greenhouse::Candidates") create a per-app whitelist — other apps stay unaffected. Unscoped entries keep the legacy "must match app or title" global semantics. Mirrors--included-windows. -
options.ignoredUrls(string[], optional): URL patterns to skip (case-insensitive, domain-aware match —chasematcheschase.comandonline.chase.combut notpurchase.com). When the focused browser is on a matching URL, the recorder skips writing frames. Mirrors--ignored-urls.
Filtering uses the macOS Accessibility API; without that permission the filter fails open (records everything). Without any filter list set, the recorder stays on the zero-overhead fast path — no a11y polling is done.
| Method | Purpose |
|---|---|
start() |
Start screen capture and write frames into the MP4. |
stop() |
Stop capture, flush the MP4 trailer, and close the file. Safe to call more than once. |
snapshot() |
Capture the recorder's monitor as a JPEG preview. |
framesWritten() |
Return frames written since start(). |
audioLevel() |
Return a smoothed microphone RMS level in [0, 1] for preflight UI. |
focusedApp() |
Return best-effort focused-window metadata; requires Accessibility permission on macOS. |
filterStatus() |
Return { paused, reason } for the window/URL filter. Poll, or subscribe via the session wrapper's paused/resumed events. |
setFilters(patch) |
Replace the active filter lists at runtime — { ignoredWindows?, includedWindows?, ignoredUrls? }. Takes effect within ≤ 1 s. |
requestPermissions() |
Trigger or check supported OS permissions. |
createScreenpipeSession emits paused and resumed events whenever the
filter verdict flips. Payload: { paused: boolean, reason: string | null }
where reason is one of "ignored_window", "included_window_mismatch",
"ignored_url", "incognito", "excluded_app".
session.on("paused", ({ reason }) => {
showBanner(`recording paused — ${reason}`);
});
session.on("resumed", () => {
hideBanner();
});
// Runtime toggle (e.g. user flips "Pause on banking" in your settings UI):
await session.setFilters({ ignoredUrls: ["chase", "wellsfargo.com"] });Audio is not muxed into the MP4 in v0.1.0.
The session wrapper reports a small, PII-scrubbed set of crash and usage
events to screenpipe so we can keep the SDK healthy in the wild. Pass a
userId and that identifier is attached to every event, so a specific end
user of your app shows up in screenpipe's Sentry (crashes) and PostHog
(usage) dashboards.
import { createScreenpipeSession } from "@screenpipe/sdk/session";
const session = createScreenpipeSession({
userId: currentUser.id, // identifies this user in screenpipe's dashboards
appName: "acme-recorder", // optional segmentation tag
});- What is sent. Crashes/errors go to Sentry (with the error message).
Usage goes to PostHog:
recording_started,recording_stopped(frame/byte/duration counts only),recording_paused/recording_resumed(with the enum reason),permissions_changed, plus onesession_initializedping. Window titles, app names, URLs and output file paths are never sent to PostHog. - What is NOT sent. No screen content, no audio, no clipboard, no
app_switchedstream, noframes_progressticks. - Opt out. Set
telemetry: falsein the options, or set the env varSCREENPIPE_SDK_TELEMETRY=0(also honorsDO_NOT_TRACK=1andSCREENPIPE_DISABLE_ANALYTICS=1). When off, the SDK makes no network calls.
Without a userId, events fall back to a per-session anonymous id, so set
userId if you want stable identification across sessions.
All four surfaces accept the same userId / appName / telemetry knobs.
Electron (via the session passed to registerScreenpipeIpc):
registerScreenpipeIpc({ ipcMain, app, sessionOptions: { userId: currentUser.id } });Swift (forwarded to the bundled Node bridge as env vars):
let config = ScreenpipeClient.Configuration(
sdkRoot: sdkRoot,
userId: currentUser.id, // identifies this user in screenpipe's dashboards
appName: "acme-recorder", // optional
telemetryEnabled: true // set false to disable
)
let client = try ScreenpipeClient(configuration: config)Tauri — reporting happens natively in the Rust plugin (no webview
fetch, so no Content-Security-Policy to configure). The JS client forwards
the identity to the plugin via screenpipe_identify on creation:
const client = createScreenpipeTauriClient({ userId: currentUser.id });You can also set it (or a Rust-side default) when registering the plugin:
tauri::Builder::default()
.plugin(screenpipe_tauri::init(
screenpipe_tauri::ScreenpipeConfig::default().user_id("user-123"),
))bun install
bun run build:debug
node --test --test-concurrency=1 "__test__/**/*.test.mjs"
swift testExample app smoke checks:
npm --prefix examples/electron-app run smoke
npm --prefix examples/tauri-app run smoke
SCREENPIPE_SWIFT_EXAMPLE_SMOKE=1 swift run --package-path examples/swift-app ScreenpipeExampleRun the optional native Tauri example compile with:
SCREENPIPE_RUN_NATIVE_EXAMPLE_BUILDS=1 node --test --test-concurrency=1 __test__/examples_e2e.test.mjsBefore publishing:
cargo test --lib
bun run build
bun run prepublishOnly
npm pack --dry-runPublish generated platform packages first, then publish the root
@screenpipe/sdk package.
| OS | Architecture | Status |
|---|---|---|
| macOS | Apple Silicon | Supported |
| macOS | Intel | Supported |
| Windows | x64 | Builds in CI; runtime validation required before public launch |
| Windows | ARM64 | Builds in CI; runtime validation required before public launch |
| Linux | - | Not supported in v0.1.0 |


