resend/resend-php
The official PHP client for the Resend email API. Install via Composer on PHP 8.1+, then send email in a few lines with a simple client interface inspired by OpenAI PHP.
Pros:
config, services.php, and Mail facade.emails, contacts, templates, automations, events, usage, and now inboxes), which maps cleanly to Laravel’s service-layer organization and keeps each capability independently testable and injectable.usage service: A first-class resource for account-level usage and quota data (PR #151). Architecturally significant for a TPM because it closes the biggest observability gap in the prior assessment: quota and consumption are now retrievable through the same SDK client, so capacity signals can be surfaced inside the application (admin dashboards, health checks, alerting) rather than only in Resend’s external console.inboxes resource (PR #153): The SDK’s first resource that is not an outbound action. Where every prior resource is "call it and something is sent/created," inboxes implies a managed email address or inbound surface, which introduces a receiving path into the application. Architecturally this is the most consequential release since the SDK baseline: it moves the integration from "outbound client behind a queue" to potentially "outbound client plus an ingress endpoint that must terminate, verify, persist, and route inbound mail." The resource pattern itself is consistent, so the container binding, facade abstraction, and test seams extend without redesign — the new work is all on the application side.Idempotency-Key and rate-limiting error handling aligns with Laravel’s queue/retry systems (e.g., Illuminate\Queue).Cons:
inboxes scope is undocumented in the release: The changelog lists "Added inboxes endpoints" with no description, no docs link, and no example. For a TPM this is a blocking ambiguity rather than a minor gap: whether inboxes means (a) inbound email receiving, (b) provisioning of inbox/alias addresses, or (c) a management surface over sending identities, the integration design differs completely. Do not plan against inboxes until the shape of the endpoints is confirmed in a spike.laravel-notification-center or spatie/laravel-mailables, this SDK lacks built-in integration with Laravel’s Notifiable trait or Mail classes. A TPM would need to bridge this gap (e.g., via a custom facade or service provider).Illuminate\Bus\Dispatchable) isn’t natively integrated. A TPM would need to design a listener layer (e.g., ResendWebhookHandler) to translate Resend events into Laravel events.usage is read-only and unopinionated: The new service returns account-level data but provides no scheduling, caching, or threshold alerting. Integrating it means owning a polling cadence and a cache strategy — an application-layer concern the TPM must design, not a capability the package delivers.inboxes is even more unopinionated: If it is an inbound surface, the SDK will hand back messages; it will not provide storage, threading, deduplication, attachment handling, spam filtering, PII retention policy, or a reply-routing model. That is a meaningful new application tier, not a client call.inboxes is the first that doesn’t inherit the existing outbound pattern for free.Resend::client() can be registered in config/services.php and bound to the container, enabling dependency injection in controllers/services.Mail facade (e.g., via a custom ResendMailer class).Resend::emails->send() wrapped in a job).usage fits naturally into Laravel’s task scheduler — a scheduled command can poll usage/quota on a fixed cadence, cache the payload, and dispatch alerts on threshold breach. No new infrastructure is required.inboxes is a receiving surface, Laravel is a good host — a signed webhook route, a queue job to normalize the payload, Storage for raw MIME and attachments, an Eloquent model for persisted messages, and Illuminate\Notifications for routing the inbound message to a human. All of it is app-owned, none of it comes from the package.User extending ResendContact). Usage snapshots can be persisted to a lightweight table if historical trending is required, but caching alone is sufficient for most teams. Inbound messages almost certainly need persistence — a resend_inbound_messages table (id, inbox, from, subject, raw reference, received_at, processed_at) plus a dedupe key, which contradicts the prior "no schema changes" position for that scope only.ResendException) may need wrapping to align with Laravel’s exception hierarchy (e.g., Illuminate\Mail\MailerException).contacts->upsert) could impact Laravel’s request lifecycle. A TPM should benchmark under load.usage calls are outbound HTTP requests. Naive per-request polling would add latency and burn rate limit. This must be cached and scheduled, not called inline in the request path.inboxes): Inbound email is untrusted input. HTML bodies, attachments, and headers arrive from arbitrary senders. Persisting raw payloads without verification, size limits, or sanitization is a security and storage-cost exposure. Signature/source verification is mandatory, not optional.config/cache.php to store Resend API keys/endpoints, and to cache usage responses with a TTL aligned to the polling cadence.ResendServiceProvider to centralize configuration and bindings.ResendFacade to abstract SDK calls (e.g., Resend::sendWelcomeEmail(), Resend::quotaSnapshot()).inboxes: spike the endpoints in isolation first, verify signatures, cap payload/attachment size, store to a dedicated disk with a retention policy, and dedupe on the vendor message identifier before writing.Mail facade entirely, or coexist with it? If the latter, how will conflicts (e.g., from address validation) be resolved?ResendWebhookController be created, or will events be dispatched via a service?User) sync with Resend contacts? Will a ResendContactable trait be created?usage service available, is in-app quota telemetry now in scope? What is the polling cadence, cache TTL, and alert threshold? Where does quota data surface — admin dashboard, /health endpoint, or log channel only?inboxes actually expose — inbound receiving, address provisioning, or sending-identity management? Is there a product need (support@, replies-to-transactional, inbound parsing) that justifies adopting it now, or is this a "track, don't adopt" item? If inbound: which address(es), which routing destination (ticket system, notification, mailbox), and who owns the reply path?usage now lets us detect before failure — will emails fall back to Laravel’s default mailer (e.g., log, ses)? If so, how will this be implemented? Note there is no equivalent fallback for inbound: an alternative ingress is a different vendor decision.AppServiceProvider:
$this->app->singleton(Resend::class, fn() => Resend::client(config('services.resend.key')));
resend to config/services.php:
'resend' => [
'key' => env('RESEND_API_KEY'),
'webhook_secret' => env('RESEND_WEBHOOK_SECRET'),
'usage_cache_ttl' => env('RESEND_USAGE_CACHE_TTL', 300),
'quota_alert_threshold' => env('RESEND_QUOTA_ALERT_THRESHOLD', 0.8),
'inbox_id' => env('RESEND_INBOX_ID'),
'inbound_disk' => env('RESEND_INBOUND_DISK', 'local'),
'inbound_max_bytes' => env('RESEND_INBOUND_MAX_BYTES', 26214400),
],
Mailer to use the SDK:
class ResendMailer extends Mailer {
public function send(MailableContract $mailable, array $failures = []) {
$resend = app(Resend::class);
$resend->emails->send($this->prepare($mailable));
}
}
ResendMailer facade for SDK-specific calls.SendResendEmailJob) for async processing:
class SendResendEmailJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue;
public function handle() {
Resend::emails()->send($this->emailData);
}
}
class PollResendUsageJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue;
public function handle() {
$usage = Cache::remember(
'resend.usage',
config('services.resend.usage_cache_ttl'),
fn () => Resend::usage()->get()
);
if ($this->ratio($usage) >= config('services.resend.quota_alert_threshold')) {
// Notify ops channel; optionally degrade to fallback mailer
}
}
}
Route::post('/resend/inbound', [ResendInboundController::class, 'handle'])
->withoutMiddleware(['csrf']);
public function handle(Request $request) {
abort_unless(
Resend::verifyWebhook($request->getContent(), $request->header('x-resend-signature'), config('services.resend.webhook_secret')),
401
);
ProcessResendInboundMessage::dispatch($request->all());
return response()->noContent();
}
ResendInboundMessage::firstOrCreate(
['provider_message_id' => $payload['id']],
['inbox_id' => config('services.resend.inbox_id'), 'from' => $payload['from'], 'subject' => $payload['subject'], 'received_at' => now()]
);
Notification to the owning team./resend/webhook to a controller with signature verification:
public function handleWebhook(Request $request) {
$payload = $request->getContent();
$signature = $request->header('x-resend-signature');
if (!Resend::verifyWebhook($payload, $signature, config('services.resend.webhook_secret'))) {
abort(401);
}
event(new ResendWebhookReceived($payload));
}
composer require resend/resend-php.AppServiceProvider.Resend::emails()->send()).ResendFacade or ResendService to abstract SDK calls.// app/Services/ResendService.php
class ResendService {
public function sendWelcomeEmail(User $user) {
return Resend::emails()->send([
'from' => '[email protected]',
'to' => $user->email,
'subject' => 'Welcome!',
'html' => view('emails.welcome', ['user' => $user]),
]);
}
}
// app/Listeners/HandleResendEvent.php
class HandleResendEvent {
public function handle(ResendWebhookReceived $event) {
$data = $event->payload;
if ($data['event'] === 'email.bounce') {
// Update user's email status in DB
}
}
}
// app/Observers/UserObserver.php
class UserObserver {
public function saved(User $user) {
Resend::contacts()->upsert($user->email, [
'name' => $user->name,
'metadata' => ['user_id' => $user->id],
]);
}
}
PollResendUsageJob (e.g., every 5–15 minutes) in app/Console/Kernel.php or via Schedule::job()./health endpoint extension, or a log channel consumed by an existing alerting stack.inboxes endpoints in a sandbox to establish what they return and what lifecycle they imply.ResendService abstraction and treat it as a Phase 1-level capability with no new infrastructure.illuminate/mail, guzzlehttp/guzzle). Neither usage nor inboxes introduces new transitive dependencies. The v1.17.0 dependency change is a CI action digest pin (shivammathur/setup-php) and is not shipped code — no runtime impact.resend_usage_snapshots table only if historical trending is a product requirement. Inbound scope is the exception: it will require a messages table plus a storage disk and a retention job.Mail facade). Resolve the inboxes scope question in parallel — it does not block Phases 1–5 but it does determine whether Phase 6 exists at all.ResendService abstraction. Ship behind a feature flag for one low-risk transactional email.usage polling, caching, and threshold alerting — including wiring quota detection into the documented fallback path.ResendServiceProvider, the ResendService abstraction, the usage polling schedule and cache TTL, and — if adopted — the entire inbound pipeline (route, dedupe, storage, retention job, routing destination). These are application artifacts, not package concerns, and will drift if unowned. The inbound pipeline in particular has an operational owner requirement that outbound never had: someone must be accountable for mail that arrives and isn't actioned.usage, then inboxes), which is a fast enough surface-area expansion that "read the changelog" needs to be a named responsibility rather than an assumption.usage shipped with enough context to design against; v1.17.0's inboxes shipped as a one-line changelog entry. Until vendor docs catch up, the internal spike output is the source of truth and should be written down.ResendService, ResendFacade) is what the team will actually support day-to-day. Document the mail strategy decision, the quota alert runbook, and — if adopted — the inbound triage runbook (who watches the inbox, what "unprocessed message" means, escalation path) alongside it.usage service lets scaling decisions be quota-informed rather than reactive. Teams can pre-emptively request quota increases, throttle non-critical sends, or route overflow to a secondary mailer before hitting hard limits.usage, a threshold alert provides lead time. The residual risk is that polling lag (cache TTL) can still allow a burst to exhaust quota between polls; mitigate with a conservative threshold (e.g., 80%).Mail, (2) fallback strategy, (3) the polling cadence and thresholds chosen for usage telemetry, and (4) whether inbound email is in scope and, if so, who owns it.inboxes addition does not change any existing behaviour and carries no breaking changes or deprecations, so it should not perturb the current roadmap. Adopt it only in response to a named product need, and not before the spike resolves scope. Keep the standing recommendation intact: adopt usage at Phase 5, keep outbound phases unchanged, and let inboxes sit as a tracked, unevaluated capability until someone owns the use case.How can I help you explore Laravel packages today?