Skip to main content

Guides

Hooks

Run awaitable callbacks around every send — observe, rewrite, cancel, and report.


Pass hooks to any provider to run awaitable callbacks around every send. before.send can observe, rewrite, or cancel a message; the rest are best-effort observers — an error they throw is swallowed, so logging and telemetry can never break a send. In a bulk send(array), hooks run once per message.

Setting the same hooks on every instance? Define them once in postboi.settings.ts and they apply everywhere — including the zero-config send().

const mail = new Resend({
	api_key: RESEND_API_KEY,
	default: { from: 'no-reply@example.com' },
	hooks: {
		// observe, mutate, or throw to cancel — runs before the request
		before: {
			send({ message }) {
				if (process.env.NODE_ENV !== 'production') return { ...message, to: 'qa@example.com' };
			}
		},
		// success — analytics / audit
		after: {
			send({ provider, message, duration_ms }) {
				track('email.sent', { provider, to: message.to, duration_ms });
			}
		},
		// any failure — report it
		on: {
			error({ error, message }) {
				Sentry.captureException(error, {
					tags: { provider: error.provider },
					extra: { to: message?.to }
				});
			},
			// each retry — observe provider flakiness
			retry({ provider, attempt, status }) {
				console.warn(`${provider} retry ${attempt} (${status})`);
			}
		}
	}
});
const mail = new Resend({
	api_key: RESEND_API_KEY,
	default: { from: 'no-reply@example.com' },
	hooks: {
		// observe, mutate, or throw to cancel — runs before the request
		before: {
			send({ message }) {
				if (process.env.NODE_ENV !== 'production') return { ...message, to: 'qa@example.com' };
			}
		},
		// success — analytics / audit
		after: {
			send({ provider, message, duration_ms }) {
				track('email.sent', { provider, to: message.to, duration_ms });
			}
		},
		// any failure — report it
		on: {
			error({ error, message }) {
				Sentry.captureException(error, {
					tags: { provider: error.provider },
					extra: { to: message?.to }
				});
			},
			// each retry — observe provider flakiness
			retry({ provider, attempt, status }) {
				console.warn(`${provider} retry ${attempt} (${status})`);
			}
		}
	}
});

The hooks

Hook When it runs Can it change the send?
before.send before the request yes — return a new message, or throw
after.send after a success no — observer only
on.error on any failure no — observer only
on.retry before each retry no — observer only

Cancelling a send

Cancel a send from before.send by throwing SkipSendError (e.g. a suppressed or unsubscribed recipient). It’s a PostboiError with code: "skipped", and it does not trigger on.error:

import Resend from 'postboi/resend';
import { SkipSendError } from 'postboi';

const mail = new Resend({
	api_key: RESEND_API_KEY,
	hooks: {
		before: {
			async send({ message }) {
				if (await isSuppressed(message.to)) throw new SkipSendError(`suppressed: ${message.to}`);
			}
		}
	}
});
import Resend from 'postboi/resend';
import { SkipSendError } from 'postboi';

const mail = new Resend({
	api_key: RESEND_API_KEY,
	hooks: {
		before: {
			async send({ message }) {
				if (await isSuppressed(message.to)) throw new SkipSendError(`suppressed: ${message.to}`);
			}
		}
	}
});