overtrue/laravel-sendcloud
A Laravel integration for the SendCloud email delivery service, providing a simple facade and configuration to send transactional and template emails through SendCloud's API. It wraps the SendCloud PHP SDK so you can fire off mail from any Laravel app with minimal setup.
Version 2.0.0 is a hard platform bump. Before anything else, confirm you are on PHP ^8.3 and Laravel ^13.30. Laravel 9–11 and every Laravel version below 13.30 are unsupported, so applications on those versions must stay on overtrue/laravel-sendcloud:^1.0 or upgrade the framework first.
composer require overtrue/laravel-sendcloud:^2.0
This pulls overtrue/sendcloud:^2.0 and requires Guzzle 7.15.2+. Check for a conflicting Guzzle pin in your composer.lock before you run the update; a locked-down Guzzle is the most common reason this install fails.
Where to look first, in order:
config/services.php — the sendcloud block is still the source of truth (api_user, api_key, and any base/domain options your app sets). Fill it from env, e.g. SENDCLOUD_API_USER / SENDCLOUD_API_KEY.composer.json → extra.laravel.aliases rather than guessing; discovery still registers the facade, the SDK binding, and the sendcloud alias.overtrue/sendcloud 2.0 README for the endpoint methods themselves. This package is API bindings only — it is not a Laravel Mail transport. If your mental model is Mail::to(...)->send(), reset it now.First use case — one transactional call through the container:
use Overtrue\SendCloud\SendCloud; // SDK client bound by the package
$client = app(SendCloud::class);
$result = $client->send([
'from' => '[email protected]',
'to' => '[email protected]',
'subject' => 'Your receipt',
'html' => view('emails.receipt', $data)->render(),
]);
Wrap this in a small application-owned service class immediately. That single indirection is what makes the 2.0 facade-caching behaviour (below) a non-issue and keeps your call sites testable.
Prefer constructor injection over the facade. Client construction is transient, but the facade caches its resolved client until Laravel resets facade state. Injection gives you a predictable per-resolution instance:
final class SendCloudMailer
{
public function __construct(private readonly SendCloud $client) {}
public function sendReceipt(Order $order): void
{
$this->client->send([...]);
}
}
Resolve inside the job, not in the constructor. Because runtime config changes never propagate to an already-constructed or cached client, queue workers that live for hours must resolve the client in handle():
public function handle(SendCloudMailer $mailer): void
{
$mailer->sendReceipt($this->order);
}
Multi-tenant / per-account credentials. Since configured credentials take precedence over anything passed per call, you cannot override credentials on a single shared client. Build a dedicated client per tenant and bind it, or set config and resolve a fresh client in a scoped container binding:
app()->forgetInstance(SendCloud::class);
config()->set('services.sendcloud.api_user', $tenant->api_user);
config()->set('services.sendcloud.api_key', $tenant->api_key);
$client = app(SendCloud::class); // reads config from the container at resolve time
This only works because 2.0 reads configuration from the container when the client is resolved — the application/container isolation fix. Config set after resolution is silently ignored, so keep ordering strict.
Attachments and multipart. Use the SDK's upload() helper or pass multipart options. Do not hand-build a multipart body and post it — 2.0 explicitly steers you away from prebuilt multipart payloads, and hand-rolled bodies are the classic source of malformed-request 400s.
Testing. The package's own suite (13 Testbench tests / 36 assertions) mocks the transport; do the same. Swap the binding in your feature tests rather than hitting the API:
$this->mock(SendCloudMailer::class)
->shouldReceive('sendReceipt')
->once();
For integration tests, inject a Guzzle MockHandler so you assert on the request path and auth headers without live credentials — matching how upstream verifies request paths and error responses.
If you want Mail:: integration, you still have to write it. Add a custom transport that delegates to this SDK client; do not expect a mail.driver from this package in 2.x.
config()->set() after the facade or a singleton has resolved the client changes nothing. Either set config first, or App::forgetInstance() the SDK class and reset the facade before re-resolving./apiv2/. The SDK preserves /apiv2/ for leading-slash paths. Do not manually prefix /apiv2/ yourself — you will end up with /apiv2//apiv2/....http:// should be removed; combined with the downgrade-redirect block, plain-HTTP setups fail hard rather than quietly.composer config audit.block-insecure true or equivalent in CI) and run composer audit in pipelines — otherwise the platform guarantee is nominal.UPGRADE.md in the 2.0.0 tag covers the SDK 2.0 migration; the CI run link confirms the verified matrix (PHP 8.3/8.4/8.5 latest, plus 8.3 lowest-secure) — mirror that matrix in your own CI if this package is on a critical path.Extension points worth knowing: the container binding for the SDK client (decorate or swap it for a configured subclass), and the package singleton for the facade behaviour. Both are the intended seams — reach for them rather than monkey-patching the facade or vendoring the SDK.
How can I help you explore Laravel packages today?