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_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.
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:
apiKeyapiBaseUrlfromAddressfromNamereplyTowebhookSecrettimeoutconnectTimeoutdefaultTagsdefaultHeadersadminEmails
Main methods
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
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, andemail.complainedshould update project state - the project wants an optional HTTP-only adapter without adding an SDK dependency
Operational notes
- 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.
Resend references
See also: Mailer, SmtpMailer, AmazonSes, Integrations, AdapterRegistry.