Skip to content

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.

npm install @whisperr/node

The 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.

whisperr.ts
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.

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.

server.ts
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" } });
context keyValue
sdk_namewhisperr-node
sdk_versionThe SDK version, for example 0.2.0
app_versionThe appVersion option. Not set: no key.

A key you pass in the context argument of track() wins over these.

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",
});
ConstantEvent nameEvent properties
trialStartedtrial_startedtrial_length_days, trial_ends_at, plan_period
trialConvertedtrial_convertedplan_period, current_period_ends_at
trialAutoRenewDisabledtrial_auto_renew_disabledtrial_ends_at, store_cancel_reason
membershipStartedmembership_startedplan_period, is_resubscribe, is_first_ever, access_source, entitlements, current_period_ends_at
renewalSucceededrenewal_succeededpaid_periods_count, current_period_ends_at
autoRenewDisabledauto_renew_disabledaccess_ends_at, store_cancel_reason
autoRenewEnabledauto_renew_enabled—
membershipEndedmembership_endedend_reason, plan_period, paid_periods_count
billingIssueStartedbilling_issue_startedhas_access, grace_ends_at
billingRecoveredbilling_recovered—
refundGrantedrefund_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.

In Lambda, Vercel functions and other short-lived runtimes, await whisperr.flush() before the handler returns. A frozen runtime cannot send the queue later.

  • track() and identify() add to an in-memory queue and return at once. await flush() waits until the queue is sent.
  • Events go to POST /v1/events/batch and identify calls to POST /v1/identify, in order, with the X-API-Key header.
  • Every event has a $message_id that stays the same across retries, so Whisperr removes duplicates.
  • 429, 5xx and network errors are retried with backoff. A Retry-After header on 429 or 503 is honored, up to 60 seconds. After maxRetries, the batch stays queued for the next flush.
  • On 401 or 403 the SDK stops sending and keeps the batch. Other 4xx responses drop the batch and report dropped.
  • 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.

OptionDefaultNotes
apiKey—Server key (wrk_…). Required.
baseUrlhttps://api.whisperr.netIngestion base URL.
appVersion—Your release version. Sent as context.app_version.
flushAt20Send when this many events are queued.
flushIntervalMs10000Background send interval (ms). 0 turns the timer off.
maxQueueSize10000Oldest events drop when the queue is full.
maxBatchSize500Events per batch request. 500 is the maximum.
maxRetries6Retries before the batch waits for the next flush.
requestTimeoutMs10000Per-request timeout (ms).
disabledfalseCaptures and sends nothing. Useful in tests.
debugfalseLogs warnings to the console.
onError—(error) => void. error.type is auth, dropped or retry_exhausted.
fetchglobal fetchInject 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.