Search docs

Jump between documentation pages.

Send email from DaloyJS with Postmark

Postmark is a transactional-first email provider known for very high inbox placement and detailed delivery analytics. This guide uses the official postmark SDK.

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 Streamyou'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";

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

app.route({
  method: "POST",
  path: "/receipts",
  operationId: "sendReceipt",
  request: {
    body: z.object({
      to: z.string().email(),
      orderId: z.string().min(1),
      total: z.number().nonnegative(),
    }),
  },
  responses: {
    202: { description: "Sent", body: z.object({ id: z.string() }) },
  },
  handler: 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 axiosunder the hood, so it targets Node and Node-compatible runtimes (Bun, Deno's Node-compat, AWS Lambda). For Cloudflare Workers or Vercel Edge, call the REST endpoint directly with fetch:POST https://api.postmarkapp.com/email with the header X-Postmark-Server-Token.

See also Resend, Mailgun, and the email integrations overview.