Docs Go to app →

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
LicenseMIT
RuntimeNode 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 dependenciesfetch 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

OptionDefaultWhat it does
apiKeyRequired. lt_live_… from Settings.
baseUrlhttps://api.letter.appOverride for self-hosting.
flushAt50Send a batch when this many items are queued.
flushInterval100 (ms)Send queued items at most this often.
maxRetries3Retry attempts on 5xx and 429.
fetchglobal fetchOverride for tests / proxies.
onErrorconsole.errorCalled when a background flush fails.

Methods

  • identify(opts) / group(opts) / track(opts) — enqueue, return void. Errors surface via onError. 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 before process.exit() so no events are lost.

Retry behavior

  • 429: wait Retry-After seconds, then retry (up to maxRetries).
  • 5xx or network errors: exponential backoff (250 ms × 2^attempt + jitter).
  • 4xx other 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.