Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lettr.com/llms.txt

Use this file to discover all available pages before exploring further.

The client.emails resource sends transactional email. Every call returns a Result — destructure { data, error } and handle error before using data.
import { Lettr } from "lettr";

const client = new Lettr(process.env.LETTR_API_KEY!);

Send an Email

const { data, error } = await client.emails.send({
  from: "sender@yourdomain.com",
  from_name: "Your App",
  to: ["recipient@example.com"],
  subject: "Hello from Lettr",
  html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
});

if (error) {
  console.error(error.message);
  return;
}

console.log(data.request_id); // unique id for tracking
console.log(data.accepted);   // number of accepted recipients
console.log(data.rejected);   // number of rejected recipients
A send requires from, to, subject, and at least one of html, text, or template_slug. The full recipient cap is 50 addresses across to/cc/bcc.

API Reference

POST /emails

Plain Text & Multipart

await client.emails.send({
  from: "sender@yourdomain.com",
  to: ["recipient@example.com"],
  subject: "Plain text update",
  text: "This is a plain text email.",
  // include both html and text for a multipart message
});

Send with a Template

When sending a template, subject is optional — the template’s own subject is used unless you override it. The substitution_data keys map to merge tags (e.g. {{name}}):
await client.emails.send({
  from: "sender@yourdomain.com",
  to: ["customer@example.com"],
  template_slug: "welcome",
  substitution_data: { name: "John" },
  template_version: 2, // optional: pin a version
});
See Templates for managing templates programmatically.

Tracking, Tags & Metadata

await client.emails.send({
  from: "sender@yourdomain.com",
  to: ["recipient@example.com"],
  subject: "Newsletter",
  html: "<p>...</p>",
  tag: "newsletter",
  metadata: { user_id: "123", campaign: "weekly" },
  headers: { "X-Entity-Ref-ID": "order-456" },
  options: {
    open_tracking: true,
    click_tracking: true,
    transactional: true,
  },
});

Attachments

await client.emails.send({
  from: "billing@yourdomain.com",
  to: ["customer@example.com"],
  subject: "Your invoice",
  html: "<p>Invoice attached.</p>",
  attachments: [
    {
      name: "invoice.pdf",
      type: "application/pdf",
      data: base64EncodedString, // base64-encoded file content
    },
  ],
});

Scheduling

Pass an ISO-8601 scheduled_at to schedule a send, then manage it by transmission id:
const { data } = await client.emails.schedule({
  from: "sender@yourdomain.com",
  to: ["recipient@example.com"],
  subject: "Reminder",
  html: "<p>Don't forget!</p>",
  scheduled_at: "2026-06-01T09:00:00Z",
});

// Look up or cancel later
await client.emails.getScheduled(data!.request_id);
await client.emails.cancelScheduled(data!.request_id);

API Reference

POST /emails/schedule

Error Handling

The SDK never throws for API or network errors — it returns them in error. Discriminate on error.type first, then optionally on error.error_code:
const { data, error } = await client.emails.send({ /* ... */ });

if (error) {
  switch (error.type) {
    case "validation":
      // 422 — invalid request. Field validation populates error.errors
      // (a field → messages map); precondition 422s (e.g. campaign_not_sendable)
      // leave it empty and carry error.error_code instead.
      if (Object.keys(error.errors).length > 0) {
        console.error("Field errors:", error.errors);
      } else {
        console.error("Precondition failed:", error.error_code);
      }
      break;
    case "api":
      // Other API errors. error.error_code is always present
      // (e.g. "unauthorized", "quota_exceeded", "not_found").
      console.error("API error:", error.error_code, error.message);
      break;
    case "network":
      // Connection failure, timeout, DNS — no response received.
      console.error("Network error:", error.message);
      break;
  }
  return;
}
Always handle error before reading data. TypeScript narrows data to non-null only inside the if (!error) branch — reading data without the check leaves it typed as possibly null.
error.typeWhenKey fields
validation422 — bad request data or failed preconditionerrors (field map), error_code
apiOther 4xx/5xxerror_code, message
networkNo response (timeout, DNS, connection)message

What’s Next

Templates

Manage Lettr templates

Webhooks

Receive delivery and engagement events