Node SDK
@letterapp/node is the official Node.js client for the ingestion API. It batches
events in memory, retries on transient failures, and generates idempotency keys
for you — so the common letter.track(...) call from a request handler is
non-blocking and safe to retry.
If you’d rather call the HTTP API directly, see the Ingestion API.
| Package | @letterapp/node on npm |
| License | MIT |
| Runtime | Node 20+, ESM |
| Size | ~7 KB packed, zero runtime dependencies |
Install
pnpm add @letterapp/node
# or
npm install @letterapp/node
# or
yarn add @letterapp/node
The package is published as ESM with TypeScript types. It has no runtime
dependencies — fetch and crypto.randomUUID come from Node 20+‘s standard
library. Bundles cleanly in Next.js, Hono, Express, Fastify, Lambda, and Vercel
Edge Functions.
You’ll need an API key from Dashboard → Settings → API keys before the SDK will do anything useful.
Quick start
import { Letter } from "@letterapp/node";
const letter = new Letter({
apiKey: process.env.LETTER_API_KEY!,
});
letter.identify({
userId: user.id,
email: user.email,
traits: { name: user.name, plan: "free" },
});
letter.group({
userId: user.id,
accountId: workspace.id,
name: workspace.name,
traits: { plan: workspace.plan, mrr: 49 },
});
letter.track({
userId: user.id,
event: "Workspace Created",
properties: { workspaceId: workspace.id },
});
// Required before process exit on long-running servers.
await letter.close();
Create the apiKey in Dashboard → Settings → API keys. It’s shown once
on creation and never again — store it somewhere safe.
Bootstrapping an existing user base? See Initial import for a checkpointed, re-runnable script template built on top of this SDK.
Constructor options
| Option | Default | What it does |
|---|---|---|
apiKey | — | Required. lt_live_… from Settings. |
baseUrl | https://api.letter.app | Override for self-hosting. |
flushAt | 50 | Send a batch when this many items are queued. |
flushInterval | 100 (ms) | Send queued items at most this often. |
maxRetries | 3 | Retry attempts on 5xx and 429. |
fetch | global fetch | Override for tests / proxies. |
onError | console.error | Called when a background flush fails. |
Methods
identify(opts)/group(opts)/track(opts)— enqueue, returnvoid. Errors surface viaonError. Fast path for long-running servers.identifySync(opts)/groupSync(opts)/trackSync(opts)— make the HTTP call immediately, return a promise. Use in serverless functions where there is no background time.flush()— drain the queue now. Resolves when the in-flight request settles.close()— flush, stop the timer, refuse new enqueues. Call this beforeprocess.exit()so no events are lost.
Retry behavior
429: waitRetry-Afterseconds, then retry (up tomaxRetries).5xxor network errors: exponential backoff (250 ms × 2^attempt + jitter).4xxother than 429: thrown immediately, no retry.
The SDK auto-generates a UUID messageId per track call, so retries dedupe
at the server. See Idempotency for the underlying
guarantee.
Serverless mode
In serverless / edge-function environments there’s no background time between
requests to drain the queue, so set flushAt: 1 and use the *Sync methods
(or await letter.flush() at the end of each handler):
const letter = new Letter({
apiKey: process.env.LETTER_API_KEY!,
flushAt: 1, // send immediately
});
await letter.trackSync({ userId, event: "Checkout Started" });
Or simply await letter.flush() after enqueueing in the same request handler.
Errors
Errors thrown by *Sync methods (or surfaced via onError) use the same
shape as the HTTP API — see Error format.
Versioning
The SDK follows semver. While we’re at 0.x:
- patch (
0.1.0 → 0.1.1) — bug fixes only. - minor (
0.1.0 → 0.2.0) — new options, new methods, behavior changes. Read the release notes. - major (
0.x → 1.0.0) — only once the HTTP API and method signatures are stable. Until then, pin a minor range ("@letterapp/node": "^0.1.0").
Every request sends a User-Agent: @letterapp/node@<version> header so we can
spot outdated clients in server logs and reach out before deprecating
anything.