Official PHP SDK for the Orboto Mail Service. Drop-in transactional-mail client with auto-quota-tracking, retry-with-backoff, quota lifecycle events, typed DTOs, and a Laravel Service-Provider out of the box. EU-hosted, GDPR-compliant.
orboto/mail is a Laravel package for official php sdk for the orboto mail service. drop-in transactional-mail client with auto-quota-tracking, retry-with-backoff, quota lifecycle events, typed dtos, and a laravel service-provider out of the box. eu-hosted, gdpr-compliant..
It currently has 0 GitHub stars and 254 downloads on Packagist (latest version 0.8.0).
Install it with composer require orboto/mail.
Discover more Laravel packages by orboto
or browse all Laravel packages to compare alternatives.
Last updated
Official PHP SDK for the Orboto Mail Service. Drop-in transactional-mail client with auto-quota-tracking, retry-with-backoff, quota lifecycle events, typed DTOs, and a Laravel Service-Provider out of the box. EU-hosted, GDPR-compliant.
Same API surface as the TypeScript SDK @orboto/mail - release versions stay in lockstep.
composer require orboto/mail
Requires PHP 8.2+ and any PSR-18 HTTP client (Guzzle is auto-discovered if present; see HTTP client below if you want to plug in a different one).
use Orboto\Mail\OrbotoMail;
$mail = new OrbotoMail(['apiKey' => $_ENV['OMS_API_KEY']]);
$result = $mail->send([
'from' => '[email protected]',
'to' => '[email protected]',
'subject' => 'Welcome',
'html' => '<h1>Welcome!</h1>',
]);
echo $result->messageId; // server-issued message id
echo $result->status; // 'queued' at success-time
echo $result->remainingQuota->percentUsed; // 0.0 .. 1.x
Get an API key at account.orboto.io/mail/api-keys.
$result = $mail->send([
'from' => '[email protected]',
'to' => '[email protected]',
'cc' => ['[email protected]', '[email protected]'], // visible to all (max 50)
'bcc' => ['[email protected]'], // envelope-only (max 50)
'subject' => 'Quarterly report',
'html' => '<p>See attached.</p>',
]);
Max 50 entries each. cc recipients show in the recipient's headers; bcc recipients receive the mail but never appear in any header (envelope-only per RFC 2822). One quota decrement per call regardless of recipient count.
$result = $mail->send([
'from' => '[email protected]',
'to' => '[email protected]',
'subject' => 'Your invoice',
'html' => '<p>Find your invoice attached.</p>',
'attachments' => [
[
'filename' => 'invoice-2026-06.pdf',
'content' => base64_encode(file_get_contents('/tmp/invoice.pdf')),
'contentType' => 'application/pdf',
],
],
]);
Up to 20 attachments per send; total decoded size capped at 30 MB. Pass an optional contentId to reference an attachment inline from your HTML (<img src="cid:logo-1">).
$batch = $mail->sendBatch([
'messages' => [
['from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Hi 1', 'text' => 'Hello 1'],
['from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Hi 2', 'text' => 'Hello 2'],
],
]);
foreach ($batch->results as $item) {
if (!$item->ok) {
error_log("Send #{$item->index} failed: {$item->reason}");
}
}
echo "{$batch->summary->queued} queued, {$batch->summary->suppressed} suppressed.";
Capped at 100 messages per call. Per-item processing - the call as a whole always returns 200; inspect $batch->summary and per-item ok flags to decide whether to retry indices.
$tpl = $mail->templates->create([
'name' => 'welcome',
'subject' => 'Welcome to {{company}}',
'bodyHtml' => '<p>Hi {{name}}!</p>',
'variablesSchema' => [
'type' => 'object',
'required' => ['name', 'company'],
'properties' => [
'name' => ['type' => 'string'],
'company' => ['type' => 'string'],
],
],
]);
$mail->sendTemplate([
'templateId' => $tpl->id,
'to' => '[email protected]',
'variables' => ['name' => 'Ada', 'company' => 'ACME'],
]);
use Orboto\Mail\Dto\QuotaState;
use Orboto\Mail\Dto\ConnectionRevokedEvent;
$mail->on('quota-warning', fn (QuotaState $q) => error_log("80%: {$q->current}/{$q->total}"));
$mail->on('quota-low', fn (QuotaState $q) => error_log("95%: {$q->current}/{$q->total}"));
$mail->on('quota-exhausted', fn (QuotaState $q) => error_log("100%: tier cap hit"));
$mail->on('connection-revoked', fn (ConnectionRevokedEvent $e) => error_log("disabled: {$e->message}"));
Each threshold event fires once per quota-reset period - a customer who lingers at 81 % doesn't get a quota-warning for every send.
Every non-2xx response surfaces as a typed exception:
use Orboto\Mail\Exception\OrbotoMailException;
use Orboto\Mail\Exception\QuotaExhaustedException;
use Orboto\Mail\Exception\PaymentRequiredException;
use Orboto\Mail\Exception\WalletUnavailableException;
use Orboto\Mail\Exception\SuppressedRecipientException;
use Orboto\Mail\Exception\ConnectionRevokedException;
try {
$mail->send([...]);
} catch (PaymentRequiredException $e) {
// Monthly quota used up + wallet balance too low - prompt a top-up
} catch (WalletUnavailableException $e) {
// Transient billing outage; send NOT dispatched - retry shortly
} catch (QuotaExhaustedException $e) {
// Render "upgrade your plan" - $e->getRemainingQuota() has the snapshot
} catch (SuppressedRecipientException $e) {
// Skip + log - recipient on suppression list
} catch (ConnectionRevokedException $e) {
// Disable the integration UX
} catch (OrbotoMailException $e) {
// Generic fallback
if ($e->isRetryable()) { /* server-side transient */ }
}
Overage billing (wallet). Once the monthly included quota is used up, above-quota sends draw on the account wallet. A successful overage send returns
overage = true; an empty wallet throwsPaymentRequiredException(402) and a brief billing outage throwsWalletUnavailableException(503, fail-closed - the send is not dispatched). Both extendOrbotoMailException. Catch the specific classes BEFOREQuotaExhaustedException/OrbotoMailException(PHP matches the first compatiblecatch).
| HTTP status | reason value | Exception |
|---|---|---|
| 400 | recipient_suppressed | SuppressedRecipientException |
| 400 | from_domain_not_authorized | OrbotoMailException - add domain at account.orboto.io/mail/sender-domains |
| 401 | connection_revoked | ConnectionRevokedException |
| 402 | payment_required | PaymentRequiredException - top up at account.orboto.io/mail/billing |
| 402 | base_quota / quota_exhausted_daily / etc. | QuotaExhaustedException |
| 503 | wallet_unavailable | WalletUnavailableException (auto-retried first) |
| 502 / 503 / 504 | any other | OrbotoMailException (auto-retried with backoff) |
Constructor options (all optional except apiKey):
$mail = new OrbotoMail([
'apiKey' => 'oms_live_xxx', // or set OMS_API_KEY env var
'baseUrl' => 'https://mail.orboto.io/api', // default
'timeout' => 10.0, // seconds per request
'maxRetries' => 3, // transient-error retry budget
'httpClient' => $myPsr18Client, // override the auto-discovered HTTP client
'requestFactory' => $myPsr17RequestFactory,
'streamFactory' => $myPsr17StreamFactory,
]);
Environment variables (used when the corresponding option is omitted):
| Variable | Default |
|---|---|
| OMS_API_KEY | (required) |
| OMS_BASE_URL | https://mail.orboto.io/api |
The SDK uses PSR-18 + PSR-17. By default it auto-discovers an installed client via php-http/discovery. If you have Guzzle installed (composer require guzzlehttp/guzzle) it picks Guzzle. If you prefer Symfony's HTTP client or anything else PSR-18-compatible, install that package and discovery routes to it - or inject explicitly via the httpClient / requestFactory / streamFactory options.
The SDK ships a Service-Provider + Facade + Notification Channel for Laravel 11+. Auto-discovery wires them in on composer require.
Publish the config:
php artisan vendor:publish --tag=orboto-mail-config
Then in code:
use Orboto\Mail\Laravel\OrbotoMailFacade as OrbotoMail;
OrbotoMail::send([
'from' => config('mail.from.address'),
'to' => $user->email,
'subject' => 'Welcome',
'html' => view('mail.welcome', ['user' => $user])->render(),
]);
Or as a Notification Channel:
use Orboto\Mail\Laravel\OrbotoMailChannel;
class Welcome extends Notification
{
public function via($notifiable): array
{
return [OrbotoMailChannel::class];
}
public function toOrbotoMail($notifiable): array
{
return [
'from' => '[email protected]',
'to' => $notifiable->email,
'subject' => 'Welcome to ACME',
'html' => view('mail.welcome', ['user' => $notifiable])->render(),
];
}
}
All resources are accessible as public properties on the OrbotoMail instance:
| Resource | Methods |
|---|---|
| $mail->suppression | check, add, remove, list |
| $mail->templates | list, get, create, update, remove |
| $mail->webhooks | list, get, create, update, remove, rotateSecret |
| $mail->sends | list, get |
| $mail->inbound | list, get |
| $mail->senderDomains | cloudflareDetect, cloudflareAutoSetup |
| $mail->apiKeys | list, get, create, revoke, rotate |
Plus top-level send methods: send, sendBatch, sendTemplate, getQuota.
Full DTO list in src/Dto/. Every type is a readonly class with a fromArray() constructor for round-tripping over the wire.
Releases stay in lockstep with the TypeScript SDK and the API backend - same vX.Y.Z tag bumps @orboto/mail (npm) + orboto/mail (Packagist) + the API container together. Patch releases are pure bug-fix / docs; minor releases add API surface; major releases imply a breaking change to the public method signatures (none yet).
MIT (see LICENSE.md). The PHP SDK is open-source; the OMS API backend it talks to is under the Sustainable Use License (see the repo root).