PHPackages                             orboto/mail - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. orboto/mail

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

orboto/mail
===========

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.

0.8.0(1mo ago)0225MITPHPPHP ^8.2

Since May 17Pushed 2mo agoCompare

[ Source](https://github.com/orboto/orboto-mail-php)[ Packagist](https://packagist.org/packages/orboto/mail)[ Docs](https://mail.orboto.io)[ RSS](/packages/orboto-mail/feed)WikiDiscussions develop Synced 3w ago

READMEChangelogDependencies (20)Versions (13)Used By (0)

@orboto/mail - PHP SDK
======================

[](#orbotomail---php-sdk)

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`](https://www.npmjs.com/package/@orboto/mail) - release versions stay in lockstep.

Install
-------

[](#install)

```
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).

Quick start
-----------

[](#quick-start)

```
use Orboto\Mail\OrbotoMail;

$mail = new OrbotoMail(['apiKey' => $_ENV['OMS_API_KEY']]);

$result = $mail->send([
    'from'    => 'noreply@acme.orbo.to',
    'to'      => 'user@example.com',
    'subject' => 'Welcome',
    'html'    => 'Welcome!',
]);

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](https://account.orboto.io/mail/api-keys).

Send with CC + BCC
------------------

[](#send-with-cc--bcc)

```
$result = $mail->send([
    'from'    => 'support@customer.de',
    'to'      => 'primary@example.com',
    'cc'      => ['cc1@example.com', 'cc2@example.com'], // visible to all (max 50)
    'bcc'     => ['silent@example.com'],                  // envelope-only (max 50)
    'subject' => 'Quarterly report',
    'html'    => 'See attached.',
]);
```

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.

Send with attachments
---------------------

[](#send-with-attachments)

```
$result = $mail->send([
    'from'    => 'noreply@acme.orbo.to',
    'to'      => 'user@example.com',
    'subject' => 'Your invoice',
    'html'    => 'Find your invoice attached.',
    '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 (``).

Send a batch
------------

[](#send-a-batch)

```
$batch = $mail->sendBatch([
    'messages' => [
        ['from' => 'a@x.com', 'to' => 'u1@example.com', 'subject' => 'Hi 1', 'text' => 'Hello 1'],
        ['from' => 'a@x.com', 'to' => 'u2@example.com', '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.

Templates
---------

[](#templates)

```
$tpl = $mail->templates->create([
    'name' => 'welcome',
    'subject' => 'Welcome to {{company}}',
    'bodyHtml' => 'Hi {{name}}!',
    'variablesSchema' => [
        'type' => 'object',
        'required' => ['name', 'company'],
        'properties' => [
            'name' => ['type' => 'string'],
            'company' => ['type' => 'string'],
        ],
    ],
]);

$mail->sendTemplate([
    'templateId' => $tpl->id,
    'to' => 'user@example.com',
    'variables' => ['name' => 'Ada', 'company' => 'ACME'],
]);
```

Quota lifecycle events
----------------------

[](#quota-lifecycle-events)

```
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.

Errors
------

[](#errors)

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 throws `PaymentRequiredException`(402) and a brief billing outage throws `WalletUnavailableException` (503, fail-closed - the send is not dispatched). Both extend `OrbotoMailException`. Catch the specific classes BEFORE `QuotaExhaustedException` / `OrbotoMailException` (PHP matches the first compatible `catch`).

HTTP statusreason valueException400`recipient_suppressed``SuppressedRecipientException`400`from_domain_not_authorized``OrbotoMailException` - add domain at [`account.orboto.io/mail/sender-domains`](https://account.orboto.io/mail/sender-domains)401`connection_revoked``ConnectionRevokedException`402`payment_required``PaymentRequiredException` - top up at [`account.orboto.io/mail/billing`](https://account.orboto.io/mail/billing)402`base_quota` / `quota_exhausted_daily` / etc.`QuotaExhaustedException`503`wallet_unavailable``WalletUnavailableException` (auto-retried first)502 / 503 / 504any other`OrbotoMailException` (auto-retried with backoff)Configuration
-------------

[](#configuration)

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):

VariableDefault`OMS_API_KEY`*(required)*`OMS_BASE_URL``https://mail.orboto.io/api`HTTP client
-----------

[](#http-client)

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.

Laravel
-------

[](#laravel)

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'    => 'noreply@acme.orbo.to',
            'to'      => $notifiable->email,
            'subject' => 'Welcome to ACME',
            'html'    => view('mail.welcome', ['user' => $notifiable])->render(),
        ];
    }
}
```

API reference
-------------

[](#api-reference)

All resources are accessible as public properties on the `OrbotoMail` instance:

ResourceMethods`$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.

Versioning
----------

[](#versioning)

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).

License
-------

[](#license)

MIT (see [LICENSE.md](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).

Support
-------

[](#support)

- Docs:
- Customer portal:
- Issues:

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance90

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

11

Last Release

32d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5630619?v=4)[Daniel Schemann](/maintainers/schemann)[@schemann](https://github.com/schemann)

---

Tags

laravelsmtpsesgdprtransactional emailorboto

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/orboto-mail/health.svg)

```
[![Health](https://phpackages.com/badges/orboto-mail/health.svg)](https://phpackages.com/packages/orboto-mail)
```

###  Alternatives

[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

36789.4k2](/packages/telnyx-telnyx-php)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[gotenberg/gotenberg-php

A PHP client for interacting with Gotenberg, a developer-friendly API for converting numerous document formats into PDF files, and more!

3896.2M31](/packages/gotenberg-gotenberg-php)[getbrevo/brevo-php

Official Brevo provided RESTFul API V3 php library

1003.9M50](/packages/getbrevo-brevo-php)[laudis/neo4j-php-client

Neo4j-PHP-Client is the most advanced PHP Client for Neo4j

185702.8k44](/packages/laudis-neo4j-php-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
