What happens
Postmark takes exactly one Tag, and the driver handles the overflow by moving tags 2..n into Metadata:
if (msg.tags.length > 0) {
payload.Tag = msg.tags[0]!.name
if (msg.tags.length > 1) {
payload.Metadata = { ...payload.Metadata, ...Object.fromEntries(msg.tags.slice(1).map((t) => [t.name, t.value])) }
}
}
The first tag's name becomes Tag; its value is read nowhere. The comment on those lines says "the rest carry as metadata so nothing the caller set is silently dropped" — but tag[0].value is exactly what gets silently dropped, and only tag[0].
Reproduction
import { createEmail } from "../src/core/email.ts"
import postmark from "../src/drivers/postmark.ts"
let body: any
const f = (async (_u: any, init: any) => {
body = JSON.parse(init.body)
return new Response(JSON.stringify({ MessageID: "m", SubmittedAt: new Date().toISOString() }), { status: 200 })
}) as unknown as typeof fetch
await createEmail({ driver: postmark({ token: "t", fetch: f }), defaults: { from: "Acme <hi@acme.com>" } })
.send({ to: "a@x.com", subject: "s", text: "t", tags: [{ name: "campaign", value: "welcome-2026" }] })
console.log("Postmark payload Tag/Metadata:", JSON.stringify({ Tag: body.Tag, Metadata: body.Metadata }))
console.log("the value 'welcome-2026' appears anywhere in the payload?", JSON.stringify(body).includes("welcome-2026"))
Observed output:
Postmark payload Tag/Metadata: {"Tag":"campaign"}
the value 'welcome-2026' appears anywhere in the payload? false
Why it matters
EmailTag is a name/value pair, and every other driver sends both halves — resend emits tags: [{ name, value }], ses emits EmailTags: [{ Name, Value }]. Only postmark discards the value, and only for the first tag, so a caller who tags every message { name: "campaign", value: "welcome-2026" } gets Postmark analytics grouped under the literal string campaign with the campaign identity gone. Add a second tag and the second one's value survives while the first one's still does not, which makes the behaviour hard to notice from the outside.
Postmark's Tag field is a single free-form string, so there is room to carry both.
Where
src/drivers/postmark.ts:179-187 — toPayload() tag handling
- compare
src/drivers/resend.ts:154 and src/drivers/ses.ts:148-150
Suggested fix
Keep Tag as the primary grouping key but stop losing the value: either set Tag to the first tag's value when there is one (value is what identifies the campaign; name is the dimension), or keep Tag = name and additionally write Metadata[name] = value for every tag including the first — one line, and it makes the existing comment true.
What happens
Postmark takes exactly one
Tag, and the driver handles the overflow by moving tags 2..n intoMetadata:The first tag's
namebecomesTag; itsvalueis read nowhere. The comment on those lines says "the rest carry as metadata so nothing the caller set is silently dropped" — but tag[0].value is exactly what gets silently dropped, and only tag[0].Reproduction
Observed output:
Why it matters
EmailTagis a name/value pair, and every other driver sends both halves — resend emitstags: [{ name, value }], ses emitsEmailTags: [{ Name, Value }]. Only postmark discards the value, and only for the first tag, so a caller who tags every message{ name: "campaign", value: "welcome-2026" }gets Postmark analytics grouped under the literal stringcampaignwith the campaign identity gone. Add a second tag and the second one's value survives while the first one's still does not, which makes the behaviour hard to notice from the outside.Postmark's
Tagfield is a single free-form string, so there is room to carry both.Where
src/drivers/postmark.ts:179-187—toPayload()tag handlingsrc/drivers/resend.ts:154andsrc/drivers/ses.ts:148-150Suggested fix
Keep
Tagas the primary grouping key but stop losing the value: either setTagto the first tag'svaluewhen there is one (valueis what identifies the campaign;nameis the dimension), or keepTag = nameand additionally writeMetadata[name] = valuefor every tag including the first — one line, and it makes the existing comment true.