overtrue/sendcloud
A PHP SDK for SendCloud email delivery. It offers a clean, framework-agnostic API to send transactional and bulk email, use templates, manage address lists and tags, and query delivery statistics. Works with Laravel and any PHP app, handling authentication and HTTP requests so you can focus on your...
Install and wire up credentials
composer require overtrue/sendcloud
You need two things from the SendCloud console (发送云 / sendcloud.net) before any code runs:
from addresses on unverified domains with errors that look like auth failures.Put them in env, not config files:
SENDCLOUD_API_USER=your_api_user
SENDCLOUD_API_KEY=your_api_key
[email protected]
SENDCLOUD_FROM_NAME="Your App"
First use case: a transactional email
use Overtrue\SendCloud\SendCloud; // confirm exact class name in src/
$client = new SendCloud(
config('services.sendcloud.user'),
config('services.sendcloud.key')
);
$client->send([
'from' => config('services.sendcloud.from'),
'fromName' => config('services.sendcloud.from_name'),
'to' => ['[email protected]'],
'subject' => 'Your receipt',
'html' => view('emails.receipt', $data)->render(),
]);
Where to look first
README.md — usually one example and the minimum viable call. It is the quickest way to confirm the exact send() signature.src/ — this is a thin wrapper; the client class and its allowed parameters are literally the API. Read it before assuming a method exists.sendcloud.net/doc/email-v2/) — the SDK mirrors endpoint params, so the docs tell you what keys the array accepts (attachments, cc/bcc, headers, x_smtpapi, etc.).Start with a raw HTML send to one address, then move to templates (below) once deliverability is confirmed.
Wrap it behind your own service. Don't inject SendCloud into controllers. Create App\Mail\SendCloudMailer (or a Mailer contract) that constructs the client from config, exposes sendHtml(), sendTemplate(), and translates SDK exceptions into your domain exceptions. This is also your seam for tests and for replacing the SDK later.
Laravel binding. Register a singleton in a service provider:
$this->app->singleton(SendCloud::class, fn () => new SendCloud(
config('services.sendcloud.user'),
config('services.sendcloud.key')
));
Keep a config/services.php entry for user/key/from/from_name so config:cache picks it up.
Prefer templates for anything content-owned. Raw html sends are fine for one-off transactional mail, but marketing/product copy should live in SendCloud templates and be sent with the template endpoint, passing variables. This decouples copy changes from deploys and keeps large HTML blobs out of PHP.
Always send from a queued job.
class SendWelcomeEmail implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public $tries = 3;
public $backoff = [10, 60, 300];
public function handle(SendCloudMailer $mailer): void
{
$mailer->sendTemplate('welcome', $this->user->email, [
'name' => $this->user->name,
]);
}
}
Retries belong on the queue, not inside the SDK. Do not retry validation/bounce-type failures; do retry network and 5xx.
Batch and multi-recipient sends. For newsletters or notifications to many users, use the batch endpoint rather than looping a single send — it is dramatically fewer HTTP calls and easier on rate limits. Keep each batch under SendCloud's per-request recipient cap and chunk accordingly.
Inject the HTTP client for tests. The package sits on Guzzle. If the constructor accepts a client, pass a MockHandler and assert the form params your wrapper produces. If it doesn't, hide the SDK behind your Mailer interface and fake that in tests; keep one contract test that hits SendCloud staging to catch upstream param changes.
Webhooks belong in their own controller. SendCloud can POST delivery/bounce/spam events. Accept them at a dedicated route, verify the token/secret, persist raw payloads, and process idempotently (events can be retried). Wire bounces back to your user table so you stop mailing dead addresses.
Log the response, not the secret. Capture statusCode, message, and the message id from each send; log the recipient domain, not the full PII payload. Never log apiKey.
200 with {"result": false, "statusCode": 4xx, "message": "..."}. If you only check the HTTP status, failed sends look successful and you'll silently lose mail. Check result and statusCode on every response.'true', '1'), and recipients may need array vs comma-joined forms depending on the endpoint. When a field "doesn't work," dump the exact request body first.from isn't on a verified domain you can waste time rotating API keys. Verify the domain and sender in the console first.%name% vs {{name}} differ across SendCloud API generations. A mismatch sends literal placeholders to real users — test template sends to a real inbox, not a mocked client.timeout and connect_timeout (e.g. 10s/5s) when constructing the client.api.sendcloud.net; from outside China expect higher latency. The authoritative docs are Chinese — budget time or use the API reference's parameter tables rather than prose.Mailer interface, and be ready to swap to raw HTTP calls if maintenance stalls.from in staging can't burn production reputation.How can I help you explore Laravel packages today?