Node.js SDK
Last updated
@whisperr/node sends events and identify calls from a Node.js backend.
Use it for facts only your server knows, such as payment failures,
cancellations and plan changes, and to confirm contact consent with the
server key.
Install
Section titled “Install”npm install @whisperr/nodeThe package is @whisperr/node
on npm. The current version is 0.2.0. It needs Node.js 18 or later and has no
runtime dependencies.
Quickstart
Section titled “Quickstart”import { createWhisperr } from "@whisperr/node";
// WHISPERR_API_KEY is the server key (wrk_…).export const whisperr = createWhisperr({ apiKey: process.env.WHISPERR_API_KEY! });
// A server-side event:whisperr.track("user_8842", "payment_failed", { amount_cents: 4900, reason: "card_declined" });
// Traits and a contact the user agreed to:whisperr.identify("user_8842", { traits: { first_name: "Ada", plan: "pro" }, channels: [ { type: "email", address: "ada@example.com", optedIn: true, verified: true }, ],});
// Before the process exits:await whisperr.shutdown();Create the server key as a Server key in Settings → API keys. Hold one client for the life of the process.
track(userId, eventType, properties?, context?) always takes the user id
first. Pass the same id your app passes to identify(), and backend and
frontend events land on one timeline.
Event names must be lowercase snake_case; the SDK drops any other name and
reports it to onError as dropped.
How do I confirm contact consent with the Node SDK?
Section titled “How do I confirm contact consent with the Node SDK?”The Node SDK runs with the server key, so its identify() is how you confirm
consent. Send optedIn: true only when the user agreed to messages on that
channel, and verified: true only for an email address your app confirmed.
Send optedIn: false to record an opt-out.
See Contact consent.
Express
Section titled “Express”import express from "express";import { createWhisperr } from "@whisperr/node";import { whisperrExpress } from "@whisperr/node/express";
const app = express();const whisperr = createWhisperr({ apiKey: process.env.WHISPERR_API_KEY! });
// After your auth middleware:app.use(whisperrExpress(whisperr));
app.post("/plan/upgrade", (req, res) => { req.whisperr.track("plan_upgraded", { plan: "pro" }); res.sendStatus(200);});req.whisperr.track() and req.whisperr.identify() use the request’s user.
By default the middleware reads req.user.id, req.user.sub,
req.auth.userId or req.userId, and only string values. When it finds no
user, the calls do nothing. For a numeric id or another location, pass a
resolver:
app.use(whisperrExpress(whisperr, { resolveUser: (req) => req.session.userId?.toString() }));For work with no signed-in request, such as payment webhooks and cron jobs,
call whisperr.track(userId, …) with the id from your own data.
How do I set the message language from Node?
Section titled “How do I set the message language from Node?”Pass the language the user picked in your product as the app_language
trait. Use a BCP 47 tag such as de or pt-BR. The Node SDK has no separate
method for it and never guesses a language.
whisperr.identify("user_8842", { traits: { app_language: "pt-BR" } });What the Node SDK adds to every event
Section titled “What the Node SDK adds to every event”context key | Value |
|---|---|
sdk_name | whisperr-node |
sdk_version | The SDK version, for example 0.2.0 |
app_version | The appVersion option. Not set: no key. |
A key you pass in the context argument of track() wins over these.
How do I send billing events from Node?
Section titled “How do I send billing events from Node?”When Whisperr has no billing source connected for your app (such as
RevenueCat), send membership changes from your
backend with the reserved names in BillingEvent. Never send them from the
browser or the app.
import { BillingEvent, type BillingEventProperties } from "@whisperr/node";
const money: BillingEventProperties = { plan_id: "pro_monthly", price: 9.99, // major units, not cents currency: "USD", // ISO 4217, upper case plan_period: "P1M", // ISO 8601 duration transaction_id: "txn_123", store: "stripe",};
whisperr.track("user_8842", BillingEvent.membershipStarted, { ...money, is_first_ever: true, access_source: "self_subscription", current_period_ends_at: "2026-11-12T10:00:00Z",});| Constant | Event name | Event properties |
|---|---|---|
trialStarted | trial_started | trial_length_days, trial_ends_at, plan_period |
trialConverted | trial_converted | plan_period, current_period_ends_at |
trialAutoRenewDisabled | trial_auto_renew_disabled | trial_ends_at, store_cancel_reason |
membershipStarted | membership_started | plan_period, is_resubscribe, is_first_ever, access_source, entitlements, current_period_ends_at |
renewalSucceeded | renewal_succeeded | paid_periods_count, current_period_ends_at |
autoRenewDisabled | auto_renew_disabled | access_ends_at, store_cancel_reason |
autoRenewEnabled | auto_renew_enabled | — |
membershipEnded | membership_ended | end_reason, plan_period, paid_periods_count |
billingIssueStarted | billing_issue_started | has_access, grace_ends_at |
billingRecovered | billing_recovered | — |
refundGranted | refund_granted | — |
Closed value lists:
store_cancel_reason:unsubscribe,billing_error,developer_initiated,price_increase,customer_support,unknown.end_reason:voluntary,billing,price_increase,product_unavailable,developer_initiated,refund,other.access_source:self_subscription,lifetime,family_share,promotional,sponsored,web_other.
Register these event names for your app; setup with your AI agent registers the events your app sends. Whisperr stores an event with an unregistered name but does not map it to your app’s events.
Serverless
Section titled “Serverless”In Lambda, Vercel functions and other short-lived runtimes, await whisperr.flush() before the handler returns. A frozen runtime cannot send
the queue later.
How does the Node SDK deliver events?
Section titled “How does the Node SDK deliver events?”track()andidentify()add to an in-memory queue and return at once.await flush()waits until the queue is sent.- Events go to
POST /v1/events/batchand identify calls toPOST /v1/identify, in order, with theX-API-Keyheader. - Every event has a
$message_idthat stays the same across retries, so Whisperr removes duplicates. 429,5xxand network errors are retried with backoff. ARetry-Afterheader on429or503is honored, up to 60 seconds. AftermaxRetries, the batch stays queued for the next flush.- On
401or403the SDK stops sending and keeps the batch. Other4xxresponses drop the batch and reportdropped. - The queue is not written to disk. A crash loses unsent events.
- The flush timer does not keep the process alive.
See Delivery, retries, idempotency.
Options
Section titled “Options”| Option | Default | Notes |
|---|---|---|
apiKey | — | Server key (wrk_…). Required. |
baseUrl | https://api.whisperr.net | Ingestion base URL. |
appVersion | — | Your release version. Sent as context.app_version. |
flushAt | 20 | Send when this many events are queued. |
flushIntervalMs | 10000 | Background send interval (ms). 0 turns the timer off. |
maxQueueSize | 10000 | Oldest events drop when the queue is full. |
maxBatchSize | 500 | Events per batch request. 500 is the maximum. |
maxRetries | 6 | Retries before the batch waits for the next flush. |
requestTimeoutMs | 10000 | Per-request timeout (ms). |
disabled | false | Captures and sends nothing. Useful in tests. |
debug | false | Logs warnings to the console. |
onError | — | (error) => void. error.type is auth, dropped or retry_exhausted. |
fetch | global fetch | Inject your own for tests or custom runtimes. |
Common questions
Which API key does @whisperr/node use?
The server key (wrk_…), shown as a Server key in Settings → API keys. Keep it in your backend secrets. It can always opt a contact in.
Why does track() in the Node SDK need a user id?
A backend has no signed-in session to read it from. Pass the same user id your app passes to identify(), so backend and frontend events land on one timeline.
Do I lose events if my Node process exits?
The queue lives in memory. Call await whisperr.shutdown() before the process exits, or await whisperr.flush() at the end of a serverless handler.
What Node.js version does @whisperr/node need?
Node.js 18 or later. The SDK uses the global fetch and has no runtime dependencies.