TypeScript Quickstart

Send your first email from a Node.js or TypeScript project in under 5 minutes. Two SDKs available — pick the one that matches your goals.

1. Pick an SDK

You have two ways to talk to Mailstorm from JavaScript:

PackageWhen to use
@mailstorm/resend You already wrote code against Resend, or you want to. Same Resend class, same methods, same response envelopes — change one import.
@mailstorm/sdk You're starting from scratch and want our native API surface (no Resend ergonomics, slightly leaner types).

This guide uses @mailstorm/resend because it's what most developers will pick. Behavior is identical between the two — only the import and class name change.

2. Install

terminal
npm install @mailstorm/resend
# or pnpm / yarn / bun

3. Get an API key

Sign up at mailstorm.dev. After verifying your email, generate a key at /api-keys. Two types:

Don't commit keys. Put the key in .env.local (gitignored) or your secret manager. Mailstorm will never email you the key after creation — if you lose it, generate a new one and rotate.

4. Send your first email

send.ts
import { Resend } from "@mailstorm/resend";

const resend = new Resend(process.env.MAILSTORM_API_KEY!);

const { data, error } = await resend.emails.send({
  from: "onboarding@yourdomain.com",
  to: "user@example.com",
  subject: "Hello",
  html: "<p>It works.</p>",
});

if (error) console.error(error);
else console.log("sent:", data?.id);

With a ms_test_* key this returns immediately with an id; the row appears in /logs as captured. With a ms_live_* key plus a verified domain, it actually sends.

5. Verify your sending domain

Live sends require ownership of the FROM domain. In /domains, click Add domain and paste the DKIM, SPF, and DMARC records into your DNS provider. Verification polls automatically — typically green within 5 minutes after DNS propagation.

Until verified, live sends from that domain return:

response
{
  "data": null,
  "error": {
    "name": "api_error",
    "message": "domain yourdomain.com is registered but not verified...",
    "statusCode": 403
  }
}

6. Send to multiple recipients

typescript
await resend.emails.send({
  from: "alerts@yourdomain.com",
  to: ["a@example.com", "b@example.com"],
  cc: ["c@example.com"],
  reply_to: "support@yourdomain.com",
  subject: "System update",
  html: "<p>...</p>",
});

v0.1 limit: The platform records the first recipient only. Multi-recipient fan-out is a follow-up; if you need it now, file an issue and we'll bump priority.

7. Send a batch

typescript
await resend.batch.send([
  { from: "alerts@yourdomain.com", to: "a@example.com", subject: "x", html: "<p>1</p>" },
  { from: "alerts@yourdomain.com", to: "b@example.com", subject: "y", html: "<p>2</p>" },
]);
// → { data: { data: [{id:"..."},{id:"..."}] }, error: null }

Up to 100 emails per batch. The whole batch is rejected with 429 if it would exceed your daily cap.

8. Handle errors

Every method returns { data, error }. Network errors and API errors both populate error — never check data without checking error first.

typescript
const { data, error } = await resend.emails.send({...});
if (error) {
  if (error.statusCode === 429) {
    // daily cap or rate limit
  } else if (error.statusCode === 403) {
    // unverified domain
  }
  throw new Error(error.message);
}

9. Idempotency

Pass an Idempotency-Key header to make retries safe. Same key + same body within 24h returns the original response without sending twice.

typescript
await resend.emails.send(
  { from: "...", to: "...", subject: "x", html: "<p>...</p>" },
  { idempotencyKey: "order-12345-confirmation" }
);

What's next