# Send email from DaloyJS with Postmark

[Postmark](https://postmarkapp.com) is a transactional-first email provider known for high inbox placement and detailed delivery analytics. This guide uses the official [`postmark`](https://github.com/ActiveCampaign/postmark.js) SDK.

**Diagram: Send through the postmark SDK**

Participants: Route handler, ServerClient, Postmark API, Webhooks

1. **Route handler -> ServerClient** (request) - client.sendEmail({ From, To, Subject, MessageStream }) - MessageStream defaults to outbound
2. **ServerClient -> Postmark API** (request) - POST /email - header X-Postmark-Server-Token
3. **Postmark API -> Route handler** (response) - { ErrorCode, MessageID } - throw unless ErrorCode === 0, else return { id: MessageID }
4. **Postmark API -> Webhooks** (async) - delivery, bounce, spam-complaint events - verify the request before trusting payloads

Postmark returns an ErrorCode rather than throwing, so the plugin checks ErrorCode === 0 and surfaces the MessageID as { id }. Pick the outbound or broadcast MessageStream per send.

## 1. Provision

1. Create a Postmark server under **Servers -> New server**, then open it and copy the **Server API Token** from **API Tokens**.
2. Add a **Sender Signature** (single address) or, for production, configure a full **Sender Domain** with DKIM and Return-Path records.
3. Decide which **Message Stream** you'll use:
  - `outbound`: default transactional stream
  - `broadcast`: bulk/marketing (must be enabled on the server)

## 2. Install

```ts
pnpm add postmark
```

## 3. Environment variables

```ts
# .env
POSTMARK_SERVER_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
POSTMARK_FROM="Acme <no-reply@acme.example.com>"
POSTMARK_STREAM=outbound
```

## 4. Plugin

```ts
// src/plugins/postmark.ts
import { ServerClient } from "postmark";
import type { App } from "@daloyjs/core";

const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN!);

export const postmarkPlugin = {
  name: "postmark",
  register(app: App) {
    app.decorate("email", {
      async send({ to, subject, text, html }) {
        const res = await client.sendEmail({
          From: process.env.POSTMARK_FROM!,
          To: to,
          Subject: subject,
          TextBody: text,
          HtmlBody: html,
          MessageStream: process.env.POSTMARK_STREAM ?? "outbound",
        });
        // ErrorCode 0 means success
        if (res.ErrorCode !== 0) {
          throw new Error(`Postmark error ${res.ErrorCode}: ${res.Message}`);
        }
        return { id: res.MessageID };
      },
    });
  },
};

declare module "@daloyjs/core" {
  interface AppState {
    email: {
      send(msg: {
        to: string;
        subject: string;
        text?: string;
        html?: string;
      }): Promise<{ id: string }>;
    };
  }
}
```

## 5. Use it in a route

```ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { postmarkPlugin } from "./plugins/postmark.ts";

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 10 }));
app.register(postmarkPlugin);

app.post(
  "/receipts",
  {
    operationId: "sendReceipt",
    request: {
      body: z.object({
        to: z.email(),
        orderId: z.string().min(1),
        total: z.number().nonnegative(),
      }),
    },
    responses: {
      202: { description: "Sent", body: z.object({ id: z.string() }) },
    },
  },
  async ({ body, state }) => {
    const { id } = await state.email.send({
      to: body.to,
      subject: `Receipt for order ${body.orderId}`,
      text: `Total: $${body.total.toFixed(2)}`,
      html: `<p>Total: <strong>$${body.total.toFixed(2)}</strong></p>`,
    });
    return { status: 202, body: { id } };
  },
);
```

## Server-side templates

Create a template in **Templates** (Mustachio syntax), then send it by `TemplateAlias`:

```ts
await client.sendEmailWithTemplate({
  From: process.env.POSTMARK_FROM!,
  To: "user@example.com",
  TemplateAlias: "order-receipt",
  TemplateModel: {
    name: "Devlin",
    orderId: "1024",
    total: "42.00",
  },
  MessageStream: "outbound",
});
```

## Batch sending

Use `client.sendEmailBatch([...])` or `client.sendEmailBatchWithTemplates([...])` to send up to 500 messages per request, the response is an array with one result per message so you can inspect per-recipient errors.

## Runtimes

The `postmark` SDK currently uses `axios` under the hood, so it targets Node and Node-compatible runtimes (Bun, Deno's Node-compat, AWS Lambda). For [Cloudflare Workers](/docs/adapters), call the REST endpoint directly with `fetch`: `POST https://api.postmarkapp.com/email` with the header `X-Postmark-Server-Token`.

See also [Resend](/docs/email/resend), [Mailgun](/docs/email/mailgun), and the [email integrations overview](/docs/email).

---

Source: https://daloyjs.dev/docs/email/postmark