Restaq
Plugins

Resend Plugin

Receive and verify Resend webhooks with Restaq.

The @restaq/resend plugin handles Resend webhook verification and provides typed Restaq event names for Resend email events. Like Clerk, Resend delivers webhooks via Svix, so verification follows the same Svix signature scheme.

Installation

pnpm add @restaq/resend
import { resend } from '@restaq/resend';

Setup

// relay.ts
import { restaq as createRestaq } from 'restaq';
import { resend } from '@restaq/resend';

export const restaq = createRestaq({
  plugins: [
    resend(), // reads RESEND_WEBHOOK_SECRET automatically
  ],
});
  1. In the Resend Dashboard → Webhooks, add an endpoint pointing at /api/webhook/resend on your deployed app and pick the events you want.
  2. Copy the Signing Secret (starts with whsec_) and set it as RESEND_WEBHOOK_SECRET.

Pass webhookSecret explicitly instead if you'd rather not rely on the env var:

resend({ webhookSecret: 'whsec_...', toleranceSeconds: 300 }),

Configuration

OptionTypeDefaultDescription
webhookSecretstringprocess.env.RESEND_WEBHOOK_SECRETThe endpoint's signing secret from the Resend dashboard
toleranceSecondsnumber300Max age of a signed timestamp before rejection

Signature verification

Resend signs webhooks the way Svix does: three headers (svix-id, svix-timestamp, svix-signature) instead of Stripe/GitHub's single-header scheme.

  1. Reads the svix-id, svix-timestamp, and svix-signature headers — rejects the request if any are missing
  2. Rejects if the timestamp is older than toleranceSeconds
  3. Computes HMAC-SHA256 of ${svix-id}.${svix-timestamp}.${rawBody}, using the webhook secret's base64-decoded bytes after stripping its whsec_ prefix
  4. svix-signature can contain multiple space-separated v1,<signature> values (for secret rotation) — the request is valid if any of them match, compared using constant-time comparison

Event types

Events are namespaced as resend.<resend-event-type> — see Webhook event types for the full list Resend can send:

// relay.handlers.ts
import type { AppRelay } from './relay';

export function registerHandlers(relay: AppRelay): void {
  relay.on('resend.email.delivered', async (event, ctx) => {
    ctx.log.info('email delivered', { id: event.data.email_id });
  });

  relay.on('resend.email.bounced', async (event, ctx) => {
    ctx.log.warn('email bounced', { id: event.data.email_id });
  });
}

Deduplication

Every Svix-delivered webhook carries a svix-id header that stays the same across delivery retries. The plugin uses it as the Restaq event ID, so a redelivered event collapses into the same execution instead of running your handler twice.

Testing locally

Use relay trigger to simulate a signed Resend webhook:

relay trigger resend email.delivered --data '{"email_id":"email_123"}'

Make sure RESEND_WEBHOOK_SECRET is set to the same value on both relay dev and relay trigger, or unset on both — see the Local Development gotcha for why.

On this page