Skip to content

Search docs

Jump between documentation pages.

Browse docs

Send email from DaloyJS with Postmark

Postmark is a transactional-first email provider known for 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.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, 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.