@letterapp/node
v0.2.0
Published
Official Node.js client for letter.app — onboarding email drip campaigns. Auto-batching, retries, idempotency.
Downloads
226
Maintainers
Readme
@letterapp/node
Official Node.js client for letter.app — onboarding email drip campaigns for product teams.
pnpm add @letterapp/node
# or: npm install @letterapp/node
# or: yarn add @letterapp/nodeRequires Node 20+. Ships as ESM with TypeScript types.
Quick start
import { Letter } from "@letterapp/node";
const letter = new Letter({
apiKey: process.env.LETTER_API_KEY!, // from Dashboard → Settings → API keys
});
// Long-running server: enqueue, fire-and-forget, auto-batched.
letter.track({
userId: user.id,
event: "Workspace Created",
properties: { workspaceId: workspace.id },
});
// Required before process exit so no events are lost.
await letter.close();In serverless / edge handlers, 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 });
await letter.trackSync({ userId, event: "Checkout Started" });Transactional email
send() mails one person right now: a receipt, a password reset, a
verification link. It is never batched and never waits for flush().
const result = await letter.send({
to: "[email protected]",
subject: "Reset your password",
html: "<p>Click <a href='https://...'>here</a> to reset.</p>",
tag: "password-reset",
idempotencyKey: `password-reset:${token}`,
});
result.messageId; // provider id, appears in delivery eventsOnly to, subject and one of html / text are required. from defaults
to the project's sender and must be on a verified domain. A plain-text part is
derived from the HTML when you don't supply one.
Pass an idempotencyKey whenever the call can be retried (a queue worker, a
webhook handler): a replay returns the original send rather than mailing the
recipient twice, and it's what lets the SDK retry a 5xx safely.
Failures throw a LetterError with status, code and reason. The reason
is what tells "this recipient is unreachable" apart from "our account is
blocked":
try {
await letter.send({ to, subject, html });
} catch (err) {
if (err instanceof LetterError && err.reason === "suppressed") return; // hard-bounced or complained; nothing to fix
throw err;
}Transactional mail ignores marketing unsubscribes (an opted-out user still gets their password reset) but respects bounces, complaints, and addresses suppressed by hand.
What it does
- Auto-batching —
identify/group/trackare queued and flushed every 100ms or 50 events.sendalways goes out immediately. - Retries —
429waitsRetry-After,5xxand network errors back off exponentially with jitter, up tomaxRetries(default 3). Asendwithout anidempotencyKeyis never retried, since a duplicate email is worse than a failed one. - Idempotent — every
trackgets a UUIDmessageIdso retries are deduplicated server-side;sendtakes your own key. - Typed — full TypeScript signatures for every method.
Full documentation
The complete reference, including all constructor options, methods, retry behavior, and the underlying HTTP API, lives at:
- SDK reference: https://letter.app/docs/node-sdk
- Ingestion API: https://letter.app/docs/api
- Transactional API: https://letter.app/docs/transactional
License
MIT — see LICENSE.
