Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Resend Mailer Laravel Package

symfony/resend-mailer

Symfony Resend Mailer provides a Symfony Mailer transport for sending email through Resend’s API. Configure it with a Resend API key and use familiar Symfony Mailer messages, DSNs, and events to deliver transactional email from PHP applications.

Deep Wiki
Context7

Getting Started

Install and wire it up

composer require symfony/resend-mailer

The package is a Mailer bridge (not a bundle). Installing it pulls in symfony/mailer and the resend/resend-php SDK, and — with Symfony Flex — the resend+api transport factory is discovered automatically. No bundle registration needed in a standard app.

Configure the DSN

# .env
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
MAILER_DSN="resend+api://${RESEND_API_KEY}@default"
# config/packages/mailer.yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'

The API key goes in the user position of the DSN; default is just a syntactic host placeholder. If the key ever contains @, : or /, URL-encode it.

First send

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final class WelcomeMailer
{
    public function __construct(private MailerInterface $mailer) {}

    public function send(string $to): void
    {
        $email = (new Email())
            ->from('[email protected]')
            ->to($to)
            ->subject('Welcome aboard')
            ->text('Thanks for signing up.')
            ->html('<p>Thanks for signing up.</p>');

        $this->mailer->send($email);
    }
}

Where to look first

  1. Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory — confirms the accepted scheme(s) and how the DSN is parsed.
  2. ResendTransport::doSend() — the exact mapping from Symfony's Email to the Resend payload. This tells you what is and isn't forwarded (custom headers, tags, scheduled_at, attachments).
  3. The resend/resend-php SDK docs for the underlying payload shape, since the bridge is a thin adapter over it.

Verify with bin/console debug:container --tag=mailer.transport_factory that the Resend factory is registered before you start debugging anything else.

First realistic use case: transactional email (password reset, receipts, invites). Before sending to real users, verify a sending domain in the Resend dashboard and use a From on that domain — otherwise every send fails with a 403.

Implementation Patterns

Inject the mailer, not the transport. Use MailerInterface for the default transport. Reach for a specific transport only when you have several configured:

framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
        transports:
            resend: '%env(RESEND_DSN)%'
            transactional: 'resend+api://%env(RESEND_API_KEY)%@default'

Then either inject the named transport with an autowiring alias, or route per-message with the X-Transport header:

$email->getHeaders()->addTextHeader('X-Transport', 'transactional');

Capture the provider message ID. Send through TransportInterface directly when you need the Resend email ID for webhook correlation or idempotency bookkeeping:

use Symfony\Component\Mailer\Transport\TransportInterface;

$sent = $transport->send($email);      // SentMessage
$resendId = $sent->getMessageId();     // Resend's email id

Store $resendId alongside your own domain entity so delivery/bounce webhooks can be matched back.

Twig emails. Use TemplatedEmail exactly as with any other Symfony transport; the bridge sees a fully rendered Email, so nothing changes:

$email = (new TemplatedEmail())
    ->from('[email protected]')
    ->to($user->getEmail())
    ->subject('Your receipt')
    ->htmlTemplate('email/receipt.html.twig')
    ->context(['order' => $order]);

Attachments and inline images. Standard MIME API works; inline images need a CID reference in the HTML:

$email->embed(fopen($path, 'r'), 'logo', 'image/png');
// <img src="cid:logo">

Async delivery + retries via Messenger. Route the mailer message to a queue; this gives you automatic retries for transient Resend failures (429/5xx) for free:

framework:
    messenger:
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': async

Add a retry strategy on the transport rather than hand-rolling backoff around MailerInterface::send().

Observability via events. The bridge extends AbstractTransport, so MessageEvent and SentMessageEvent are dispatched. Hook a listener to log recipients/subjects and the returned message ID — useful because the Resend request body is built inside the bridge and isn't otherwise visible:

public function onMessage(MessageEvent $event): void { /* dump envelope + headers */ }

Testing. Never hit the real API in tests. Swap the DSN to null://null in config/packages/test/mailer.yaml, then assert with MailerAssertionsTrait (assertEmailCount, assertEmailSubjectContains, …). If you need to inspect the Resend-specific payload, write a small decorator implementing TransportInterface that records the SentMessage and delegates to a null transport.

Local development. resend+api:// always talks to the live API. Use null://null for tests and an SMTP catcher (Mailpit) or the [email protected] sender for dev — that address only delivers to the account owner's own email, which is fine for smoke tests.

Gotchas and Tips

  • The DSN user is the key. A common mistake is writing resend+api://default:re_xxx@default or putting the key in the host. Keep it as resend+api://KEY@default, URL-encoded if necessary.
  • Key leakage. mailer.dsn often ends up in debug:config output and in exception traces. Use env vars or Symfony Secrets (bin/console secrets:set RESEND_API_KEY) and reference %env(RESEND_API_KEY)%; never commit the literal key.
  • The bridge is a thin adapter. Anything Resend-specific — scheduled_at, tag taxonomy, custom headers, batch endpoints, idempotency keys — may not be forwarded by doSend(). Read the transport source first; if it's missing, either open a PR or call $resend->emails->send([...]) from a dedicated service. Don't mix both paths for the same email type.
  • Errors arrive as TransportException. Inspect the previous exception / getDebug() for the Resend API message. The high-frequency ones: 403 (unverified domain or From not on a verified domain), 422 (empty body — Resend requires at least text or html), 429 (rate limit).
  • Rate limits and retries. Symfony Mailer does not retry. Wrap sends in Messenger (recommended) or your own retry logic; treat 429 and 5xx as retryable and 4xx payload errors as permanent.
  • Attachments are read into memory. attachFromPath() and streamed parts get base64-encoded into one HTTP request. Keep total message size well under Resend's limit and avoid attaching multi-MB files through the mailer — link to them instead.
  • Envelope vs. headers. ->returnPath(), ->sender() and envelope overrides are largely ignored by API-based transports; Resend derives From/Reply-To from the message. Set ->replyTo() explicitly if you need replies to land somewhere other than the sender.
  • Custom headers are not guaranteed. If you rely on X-Campaign-Id-style headers, verify in doSend() that they're mapped. Prefer Resend's native tagging if the bridge exposes it.
  • From-address verification is per domain. Switching environments (staging vs. prod domain) means verifying each domain in Resend. Budget for this before a release.
  • Message IDs are your correlation key. Persist SentMessage::getMessageId(); without it, Resend webhooks (email.delivered, email.bounced) are hard to reconcile with your records.
  • Non-Flex / manual registration. If the factory isn't picked up, register ResendTransportFactory as a service tagged mailer.transport_factory. Confirm with bin/console debug:container --tag=mailer.transport_factory.
  • Version pinning. This is a low-traffic bridge; pin both symfony/resend-mailer and resend/resend-php in composer.json. The bridge tracks the SDK's payload shape, so a minor SDK bump can change behavior.
  • Debugging tip. When a send fails silently, dump the Email object's headers/body in a MessageEvent listener and compare against the Resend API reference — the bridge's own error messages are often just the wrapped HTTP response.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource