-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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=5Keys:
-
RESEND_API_KEYis required before outbound API calls. -
RESEND_FROM_ADDRESSandRESEND_FROM_NAMEare used as the default sender. -
RESEND_WEBHOOK_SECRETis required only for webhook verification. -
RESEND_API_BASE_URLis configurable for tests and proxy setups.
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.
Creates the mailer from explicit configuration merged over Env defaults.
Common config keys:
apiKeyapiBaseUrlfromAddressfromNamereplyTowebhookSecrettimeoutconnectTimeoutdefaultTagsdefaultHeadersadminEmails
send(array $recipients, string $subject, string $title, string $text, array $attachments = [], array $ccs = []): voidsendTransactional(array $message, array $options = []): arraydecodeWebhookPayload(string $payload): arrayverifyWebhookPayload(string $payload, ?array $headers = null, ?int $toleranceSeconds = null): arraywebhookResponse(string $payload, ?array $headers = null, array $handlers = []): JsonResponsewebhookResponseFromEvent(array $event, array $handlers = []): JsonResponseapiKeySet(): bool
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' => 'ada@example.test'],
],
'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.
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 <ada@example.test>'],
'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.
Local file attachments use Pair's legacy attachment shape or the Resend payload shape:
$mailer->sendTransactional([
'to' => 'billing@example.test',
'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' => 'billing@example.test',
'subject' => 'Invoice',
'html' => '<p>Attached.</p>',
'attachments' => [
[
'path' => 'https://example.test/invoices/invoice-123.pdf',
'filename' => 'invoice-123.pdf',
],
],
]);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.
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, andemail.complainedshould update project state - the project wants an optional HTTP-only adapter without adding an SDK dependency
- Keep
RESEND_API_KEYandRESEND_WEBHOOK_SECRETin.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.
See also: Mailer, SmtpMailer, AmazonSes, Integrations, AdapterRegistry.