ResendMailer - viames/pair GitHub Wiki

Pair framework: ResendMailer

Pair\Services\ResendMailer is a Resend-backed mailer for transactional email and webhook processing.

It extends Mailer, so existing Pair email flows can keep using the common send(...) contract while newer code can call Resend-specific methods for tags, custom headers, idempotency, and webhook events.

The Pair core does not require the Resend SDK. ResendMailer uses cURL directly.

Configuration

RESEND_API_KEY=
RESEND_API_BASE_URL="https://api.resend.com"
RESEND_FROM_ADDRESS=
RESEND_FROM_NAME=
RESEND_WEBHOOK_SECRET=
RESEND_TIMEOUT=20
RESEND_CONNECT_TIMEOUT=5

Keys:

  • RESEND_API_KEY is required before outbound API calls.
  • RESEND_FROM_ADDRESS and RESEND_FROM_NAME are used as the default sender.
  • RESEND_WEBHOOK_SECRET is required only for webhook verification.
  • RESEND_API_BASE_URL is configurable for tests and proxy setups.

Extension path

Pair v4 integrations should be registered explicitly. An application or optional pair/resend package can expose the mailer as the mail adapter:

use Pair\Core\AdapterKeys;
use Pair\Core\Application;
use Pair\Services\ResendMailer;

$app = Application::getInstance();
$app->setAdapter(AdapterKeys::MAILER, new ResendMailer());

$mailer = $app->adapter(AdapterKeys::MAILER, ResendMailer::class);

This keeps Resend optional and avoids automatic package discovery.

Constructor

__construct(array $config = [])

Creates the mailer from explicit configuration merged over Env defaults.

Common config keys:

  • apiKey
  • apiBaseUrl
  • fromAddress
  • fromName
  • replyTo
  • webhookSecret
  • timeout
  • connectTimeout
  • defaultTags
  • defaultHeaders
  • adminEmails

Main methods

  • send(array $recipients, string $subject, string $title, string $text, array $attachments = [], array $ccs = []): void
  • sendTransactional(array $message, array $options = []): array
  • decodeWebhookPayload(string $payload): array
  • verifyWebhookPayload(string $payload, ?array $headers = null, ?int $toleranceSeconds = null): array
  • webhookResponse(string $payload, ?array $headers = null, array $handlers = []): JsonResponse
  • webhookResponseFromEvent(array $event, array $handlers = []): JsonResponse
  • apiKeySet(): bool

Legacy Pair send

Use send(...) when replacing an existing Pair mail provider without changing calling code:

use Pair\Services\ResendMailer;

$mailer = new ResendMailer();

$mailer->send(
	[
		['name' => 'Ada Lovelace', 'email' => '[email protected]'],
	],
	'Welcome',
	'Welcome to Pair',
	'<p>Your account is ready.</p>'
);

The legacy method uses Pair's environment-aware recipient conversion from Mailer: development and staging can redirect recipients according to existing framework rules.

Transactional email

Use sendTransactional(...) when the application needs Resend-specific fields such as tags, custom headers, Bcc, attachments, or idempotency.

use Pair\Services\ResendMailer;

$mailer = new ResendMailer();

$response = $mailer->sendTransactional([
	'to' => ['Ada <[email protected]>'],
	'subject' => 'Invoice ready',
	'html' => '<p>Your invoice is ready.</p>',
	'text' => 'Your invoice is ready.',
	'tags' => [
		'invoice_id' => 'invoice_123',
		'customer_id' => 'customer_456',
	],
], [
	'idempotency_key' => 'invoice_123',
]);

sendTransactional(...) returns the Resend API response, usually including the email id.

Attachments

Local file attachments use Pair's legacy attachment shape or the Resend payload shape:

$mailer->sendTransactional([
	'to' => '[email protected]',
	'subject' => 'Invoice',
	'html' => '<p>Attached.</p>',
	'attachments' => [
		[
			'filePath' => APPLICATION_PATH . '/invoices/invoice-123.pdf',
			'filename' => 'invoice-123.pdf',
		],
	],
]);

Remote attachments can use path:

$mailer->sendTransactional([
	'to' => '[email protected]',
	'subject' => 'Invoice',
	'html' => '<p>Attached.</p>',
	'attachments' => [
		[
			'path' => 'https://example.test/invoices/invoice-123.pdf',
			'filename' => 'invoice-123.pdf',
		],
	],
]);

Webhooks

Resend signs webhooks with Svix headers. Use the raw request body for verification.

use Pair\Services\ResendMailer;

$mailer = new ResendMailer();

return $mailer->webhookResponse(
	(string)file_get_contents('php://input'),
	null,
	[
		'email.delivered' => function (array $event): void {
			$emailId = $event['data']['email_id'] ?? null;

			if (!$emailId) {
				return;
			}

			// Persist delivery state in project tables.
		},
		'email.bounced' => function (array $event): void {
			// Mark the project-side email as bounced.
		},
	]
);

webhookResponse(...) verifies svix-id, svix-timestamp, and svix-signature before invoking handlers. Use webhookResponseFromEvent(...) only after the event has already been verified.

Choosing SMTP, SES, or Resend

Use SmtpMailer when:

  • the project already has a reliable SMTP provider
  • portability is more important than provider-specific events
  • webhook feedback is not required

Use AmazonSes when:

  • the application is already deployed inside AWS
  • SES deliverability, IAM, and AWS billing are operationally convenient
  • the project can accept an SDK-style provider dependency

Use ResendMailer when:

  • transactional developer experience matters
  • provider tags and per-email idempotency are useful
  • webhook events such as email.delivered, email.bounced, and email.complained should update project state
  • the project wants an optional HTTP-only adapter without adding an SDK dependency

Operational notes

  • Keep RESEND_API_KEY and RESEND_WEBHOOK_SECRET in .env, never in Git.
  • Use deterministic idempotency keys for retryable transactional emails.
  • Store delivery state in project tables, not in the mailer.
  • Webhook handlers should be safe to run more than once because providers may retry deliveries.
  • Do not log full email bodies or personal recipient data by default.

Resend references

See also: Mailer, SmtpMailer, AmazonSes, Integrations, AdapterRegistry.