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
- Create a Postmark server under Servers → New server, then open it and copy the Server API Token from API Tokens.
- Add a Sender Signature (single address) or, for production, configure a full Sender Domain with DKIM and Return-Path records.
- Decide which Message Streamyou'll use:
outbound— default transactional streambroadcast— bulk/marketing (must be enabled on the server)
2. Install
pnpm add postmark3. Environment variables
# .env
POSTMARK_SERVER_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
POSTMARK_FROM="Acme <no-reply@acme.example.com>"
POSTMARK_STREAM=outbound4. Plugin
// 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
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:
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.