Search docs

Jump between documentation pages.

Browse docs

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.

Send through the postmark SDK
Route handlerServerClientPostmark APIWebhooks
  1. 01requestRoute handlerServerClientclient.sendEmail({ From, To, Subject, MessageStream })MessageStream defaults to outbound
  2. 02requestServerClientPostmark APIPOST /emailheader X-Postmark-Server-Token
  3. 03responsePostmark APIRoute handler{ ErrorCode, MessageID }throw unless ErrorCode === 0, else return { id: MessageID }
  4. 04asyncPostmark APIWebhooksdelivery, bounce, spam-complaint eventsverify 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";

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 axios under the hood, so it targets Node and Node-compatible runtimes (Bun, Deno's Node-compat, AWS Lambda). For Cloudflare Workers or Vercel, 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.