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 Php Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup in Laravel

  1. Install the package (v1.17.0 or later):

    composer require resend/resend-php
    
  2. Initialize the client (e.g., in config/services.php):

    'resend' => [
        'api_key' => env('RESEND_API_KEY'),
        'api_url' => env('RESEND_API_URL', 'https://api.resend.com'),
    ],
    

    Then bind a singleton in app/Providers/AppServiceProvider.php:

    public function register()
    {
        $this->app->singleton(Resend::class, function ($app) {
            return Resend::client($app['config']['services.resend.api_key'], [
                'api_url' => $app['config']['services.resend.api_url'],
            ]);
        });
    }
    

    Bind in register(), not boot() — resolving config in boot() works, but register() makes the binding available to other providers and to queued jobs resolved early.

  3. First call — sanity check the key, the quota, and (new in v1.17.0) the inbox surface:

    use Resend\Resend;
    
    $resend = app(Resend::class);
    
    // Account-level usage & quota (v1.16.0)
    $usage = $resend->usage->get();
    
    // New in v1.17.0: inbox endpoints. Start with a read so you learn the
    // response shape before you provision anything.
    $inboxes = $resend->inboxes->list();
    
    // Then a real send
    $response = $resend->emails->send([
        'from' => '[email protected]',
        'to'   => '[email protected]',
        'subject' => 'Welcome!',
        'html' => '<strong>Welcome to our platform!</strong>',
    ]);
    

Where to Look First

  • src/Resend.php / the client() factory — the container shape for every service.
  • src/Services/ — the service list (emails, contacts, domains, templates, events, usage, and now inboxes). Read the newest service first: inboxes for v1.17.0, then usage from v1.16.0 — together they tell you what you can receive and how much you can send before you write a line of application code.
  • src/Exceptions/ — the exception hierarchy you'll be catching in jobs.

Key First-Use Cases

  • Inbound email (new in v1.17.0): an inbox is a real, addressable destination. Support replies, reply+token@ threading, forwarded receipts, and "email-to-create-a-record" workflows all become in-app features instead of a third-party parsing service.
  • Quota visibility: pull usage daily into your admin panel so "why did the newsletter silently fail?" stops being a support ticket.
  • Transactional emails: password resets, order confirmations, receipts.
  • Marketing / batch: newsletters and digests, gated on remaining quota.
  • Webhooks: bounce/complaint handling — the thing that protects your sending reputation — plus inbound delivery now that you have inboxes.

Note: the other changelog entry (a setup-php digest bump from Renovate) is CI housekeeping and has no runtime impact.


Implementation Patterns

Core Workflows

  1. Email Sending

    • Basic: emails->send() with from, to, subject, plus html/text.
    • Templates:
      $resend->emails->send([
          'from' => '[email protected]',
          'to' => '[email protected]',
          'template_id' => 'abc123',
          'template_data' => ['name' => 'John'],
      ]);
      
    • Batch: chunk your audience (~100 per call) rather than relying on one giant batch — a single oversized batch fails atomically.
  2. Inboxes (v1.17.0) — the first inbound primitive in the SDK.

    • The inboxes service follows the same conventions as the rest of src/Services/, so expect the familiar create / get / list / remove shape. Dump the response once in tinker before writing against field names.
    • Provision one inbox per tenant or per purpose, and persist the identifier — an inbox is stateful, unlike a send:
      $inbox = $resend->inboxes->create([
          'name' => "support-{$tenant->id}",
      ]);
      
      $tenant->update(['resend_inbox_id' => $inbox->id]);
      
    • Do this in a command or an onboarding job, never inline in a controller. Address provisioning is a slow, fallible external call — exactly the thing you don't want in the request path.
    • Receiving: treat inbound messages like a webhook-driven feed — register the inbound/received event alongside your existing bounce/complaint webhook, verify the signature with the same verifier, then enqueue. Store the raw payload in object storage and keep only metadata in the database:
      Storage::disk('s3')->put("inbound/{$event->id}.json", $request->getContent());
      ProcessInboundEmail::dispatch($event->id);
      
    • Reply threading: encode a thread/ticket id in the outbound reply_to (e.g. reply+{ticket}@inbox.example.com) and match it back when the message lands in the inbox. This is the pattern that makes an inbox useful rather than just a mailbox.
  3. Usage & Quota (v1.16.0) — still the day-to-day ops workhorse.

    • The usage service exposes account-level usage/quota data, so you can answer "how much headroom do I have left this period?" from inside the app instead of the dashboard.
    • Pre-flight guard before bulk sends:
      $usage = $resend->usage->get();
      
      // Shape varies by plan — log it once, then code against the real keys.
      Log::debug('resend.usage', (array) $usage);
      
      if ($remaining < $recipientCount) {
          // defer, downscale, or alert — do not fire the batch
          return;
      }
      
    • Internal quota gauge for an admin dashboard: cache the response (Cache::remember('resend.usage', now()->addMinutes(10), ...)) — it is not a per-request call.
    • Alerting: a scheduled command that compares usage against your plan ceiling and pings Slack/email at 80% and 95%.
    • Post-send reconciliation: after a large campaign, re-read usage to confirm the batch actually consumed quota (a good proxy for "did it really send?").
  4. Contacts & Segments

    • contacts->create(), contacts->upsert() for bulk property sync.
    • Segment membership drives which lists you feed into batch sends.
  5. Webhooks & Events

    • Verify signatures before touching the payload:
      $verifier = new \Resend\WebhookSignature($request->header('X-Resend-Signature'));
      if (! $verifier->verify($request->getContent(), config('services.resend.webhook_secret'))) {
          abort(400);
      }
      
    • Enqueue the event immediately, process later. Don't do bounce logic — or inbound parsing — inside the webhook request.
  6. Templates & Domains

    • templates->create() / update() for copy changes without deploys.
    • Domain verification is a one-time setup step — do it via the dashboard, not code, and keep it out of your runtime path. Note that inbox addresses depend on a verified domain, so this ordering matters more now.

Laravel-Specific Patterns

  • Container binding + DI: inject Resend into constructors; never new Resend(...) in a controller.
  • Jobs for anything user-facing:
    class SendWelcomeEmail implements ShouldQueue
    {
        use Queueable;
    
        public function __construct(public int $userId) {}
    
        public function handle(Resend $resend): void
        {
            $resend->emails->send([...]);
        }
    }
    
  • Laravel Mail integration: Resend ships a Symfony/Laravel mailer transport — configure MAIL_MAILER=resend in config/mail.php rather than hand-rolling a Mailable. Use the SDK directly only for features the transport doesn't expose (templates, batch, usage, inboxes).
  • Notifications channel: a thin ResendChannel keeps notification logic in Notification classes where it belongs.
  • Scheduled quota check: Schedule::command('resend:usage-check')->hourly(); — cheap and catches quota exhaustion before customers do.
  • Inbound processor command: pair it with a resend:inboxes-sync command that persists newly created inbox ids, so provisioning is retryable and reconcilable rather than fire-and-forget.

Gotchas and Tips

Common Pitfalls

  1. API key handling — .env only, config/services.php for the mapping, and never env() outside config files (it returns null once config is cached).

  2. Assuming a usage shape — usage returns plan-dependent data. Dump it once in tinker, then write your alerting against the actual keys. Don't hardcode a field name you only saw in a blog post. The same discipline applies to the brand-new inboxes responses.

  3. Calling usage per request — it's an API call with the same rate limits as everything else. Cache it; 5–10 minutes of staleness is irrelevant for a quota gauge.

  4. Inbox provisioning is not free or idempotent — creating an inbox on every page load, or in a retried job without a guard, will litter your account with duplicates. Persist the id, check for it first, and make the command safe to re-run.

  5. Don't poll inboxes for new mail — a Schedule::everyMinute() loop over inboxes->list() burns rate limit for nothing. Drive receipt off webhooks and use list()/get() only for reconciliation and backfill.

  6. Inbound payloads are big — attachments and full MIME bodies can be megabytes. Store the raw JSON in S3 and keep parsed metadata in the DB; don't json_encode a full message into a text column and don't log it.

  7. Idempotency — for payments/receipts, send an idempotency key so a retried job doesn't double-charge a customer's inbox:

    $resend->emails->send([
        // ...
        'headers' => ['Idempotency-Key' => "receipt-{$order->id}"],
    ]);
    
  8. Template data drift — template variables change when marketing edits a template. Validate keys before sending, or a rename becomes a silent 422 in production.

  9. Rate limits / 429s — back off and retry with jitter; don't retry() in a tight loop. Let the queue do the waiting.

  10. Quota exhaustion looks like a bug — once you're over the plan limit, sends fail while your code looks correct. The usage service is exactly the tool that stops this from being a mystery.

  11. Webhook latency — delivery can lag by minutes, inbound and outbound alike. Never use webhooks as your source of truth for a synchronous user action.

Debugging Tips

  • Debug mode in the client config logs requests/responses — enable locally, never in production (it logs payloads containing PII, and inbound payloads are the worst offender).
  • Catch the specific exception, not a blanket \Exception:
    try {
        $resend->emails->send([...]);
    } catch (\Resend\Exception\RateLimitException $e) {
        $this->release($e->getRetryAfter());
    } catch (\Resend\Exception\ResendException $e) {
        Log::error('resend.failed', ['message' => $e->getMessage()]);
        throw $e;
    }
    
  • Log the full response object, not just success — Resend's error bodies carry the actual field-level reason.
  • Probe a new service before writing code against it: php artisan tinker → app(Resend::class)->inboxes->list(). Thirty seconds here saves a refactor when the response shape isn't what you assumed.
  • DNS: for custom domains, verify TXT/MX propagation with dig before opening a support ticket — and before you expect an inbox address to receive anything.

Extension Points

  1. Custom HTTP client — swap in a Guzzle client with timeouts/middleware:
    Resend::client($key, [
        'http_client' => new \GuzzleHttp\Client(['timeout' => 30]),
    ]);
    
  2. Decorator service — wrap Resend in your own Mailer class that combines send + usage-check + logging. Keeps controllers clean and gives you one place to instrument.
  3. Inbox provisioner — a small InboxProvisioner class around inboxes->create() that handles the persist-then-verify dance and returns a domain object, so onboarding code never touches the SDK directly.
  4. Mocking in tests — bind a fake into the container; assert on the payload array rather than on HTTP:
    $this->mock(Resend::class, function ($mock) {
        $mock->shouldReceive('emails->send')->once();
        $mock->shouldReceive('usage->get')->andReturn([...]);
        $mock->shouldReceive('inboxes->create')->once()->andReturn((object) ['id' => 'inbox_123']);
    });
    
  5. Usage-driven feature flags — gate premium/bulk features on remaining quota instead of a static config value.

Pro Tips

  • Pre-flight every bulk send with usage — still the cheapest reliability win available.
  • Cache usage, alert on it, graph it — three small pieces of code that turn a black-box dependency into a monitored one.
  • Roll out inboxes behind a single feature flag while the endpoint surface is young — v1.17.0 is its first release, so keep the blast radius to one code path you can switch off.
  • emails->list() for auditing what actually went out, filtered by created_at.
  • contacts->upsert() for CRM-style syncs; it's far cheaper than a create/update loop.
  • Scheduled sends via the schedule option for timezone-correct reminders — but check quota at schedule time and at send time.
  • Read the changelog per minor version. v1.16.0 gave you usage, v1.17.0 gives you inboxes; both are additive, so upgrading is safe — but the assessment's "where to look first" changes each time, and that's the five minutes that keeps you current.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib