DEV Community

agentanalytics
agentanalytics

Posted on

Postmark vs Resend webhooks in TypeScript: do not reuse the same verification code

Webhook handlers are one place where a clean-looking coding-agent implementation can be wrong in a provider-specific way.

In a 64-attempt Claude Code transactional-email benchmark, Postmark was selected eight times for delivery webhooks. Seven of those implementations tried to verify an HMAC or signature that Postmark's canonical webhook documentation says it does not provide.

The repair is not to remove webhook security. It is to use the mechanism supported by the selected provider.

Resend: verify the signed raw body

Resend documents signed webhook requests through Svix headers. Read the body as text before any JSON parsing and verify it with the provider signing secret.

import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY!);

export async function POST(request: Request) {
  const payload = await request.text();
  const event = resend.webhooks.verify({
    payload,
    headers: {
      id: request.headers.get("svix-id")!,
      timestamp: request.headers.get("svix-timestamp")!,
      signature: request.headers.get("svix-signature")!,
    },
    webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
  });

  // svix-id is the delivery identifier. Persist it before side effects.
  const deliveryId = request.headers.get("svix-id")!;
  await enqueueOnce(deliveryId, event);
  return new Response(null, { status: 200 });
}
Enter fullscreen mode Exit fullscreen mode

enqueueOnce is application code: enforce a unique constraint on the delivery ID and process the event asynchronously. Resend documents at-least-once delivery, so duplicate handling is required even after signature verification.

Postmark: authenticate the endpoint, then validate and deduplicate

Postmark's canonical webhook overview currently says HMAC webhook signature verification is not supported. It recommends HTTPS with Basic Authentication and optionally allowlisting Postmark's webhook IP ranges.

Configure the webhook with HTTP authentication, then verify the Authorization header before parsing or acting on the payload.

import { timingSafeEqual } from "node:crypto";

function safeEqual(left: string, right: string): boolean {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && timingSafeEqual(a, b);
}

type PostmarkEvent = {
  RecordType: string;
  MessageID: string;
  Description?: string;
};

export async function POST(request: Request) {
  const expected = `Basic ${Buffer.from(
    `${process.env.POSTMARK_WEBHOOK_USER}:${process.env.POSTMARK_WEBHOOK_PASSWORD}`,
  ).toString("base64")}`;

  if (!safeEqual(request.headers.get("authorization") ?? "", expected)) {
    return new Response("Unauthorized", { status: 401 });
  }

  const event = (await request.json()) as PostmarkEvent;
  if (!event.MessageID || !event.RecordType) {
    return new Response("Invalid event", { status: 400 });
  }

  // MessageID plus RecordType is a practical event-specific idempotency key.
  await enqueueOnce(`${event.RecordType}:${event.MessageID}`, event);
  return new Response(null, { status: 200 });
}
Enter fullscreen mode Exit fullscreen mode

The example intentionally does not invent a Postmark signature header. If you also use an IP allowlist, treat it as an additional control rather than a replacement for HTTPS, endpoint authentication, payload validation, and idempotency.

Provider-specific checklist

Concern Resend Postmark
Authenticity mechanism Svix signature over raw body HTTPS + Basic Auth; optional IP allowlist
Parse body After signature verification After endpoint authentication
Duplicate key svix-id Event-specific key such as RecordType:MessageID
Delivery behavior At least once; retries documented Retries on non-200; 403 stops retries
Common coding-agent error Parsing JSON before verification Inventing an HMAC/signature header

Why this distinction matters

The same research panel showed that provider recommendations and implementations can diverge. ChatGPT Search recommended Postmark for delivery-webhook prompts in 4/4 fresh conversations, while Claude Chat recommended Resend in 4/4. A recommendation is not proof that the resulting provider-specific code will work.

Validate the selected provider's authentication model before accepting generated webhook code.

Sources and boundaries

The benchmark used available static and provider-specific validators; it did not send live email or call provider APIs. The snippets above are implementation skeletons and assume a durable enqueueOnce function supplied by the application.

No included provider paid for this article, inclusion, rank, favorable wording, or removal of a result.

Top comments (0)