# Send emails with PHP

> Send email from PHP with the official mailblastr/mailblastr Composer package — no runtime dependencies beyond the curl and json extensions.

The official [`mailblastr/mailblastr`](https://packagist.org/packages/mailblastr/mailblastr) package is the fastest way to send email from PHP. It has **no Composer runtime dependencies** — just the built-in `curl` and `json` extensions — and is built around a single factory: `Mailblastr::client($apiKey)` returns a client that exposes one property per API resource.

The package also sets the required `User-Agent` header on every request for you (the API answers a request without one with a `403 validation_error`), automatically retries `429` and `503`, and throws a typed exception instead of handing back raw HTTP.

## Prerequisites

1. A **verified domain** for your `from` address. ([guide](https://www.mailblastr.com/docs/domains/managing))
2. An **API key** (`mb_...`), stored in your environment as `MAILBLASTR_API_KEY`. ([Authentication](https://www.mailblastr.com/docs/authentication))
3. PHP **8.1 or newer** with the `curl` and `json` extensions enabled — the package declares `"php": "^8.1"`, `ext-curl` and `ext-json`.
4. [Composer](https://getcomposer.org) to install the package.

## Install

```sh
composer require mailblastr/mailblastr
```

The current release is **5.0.0**. The package autoloads under the `Mailblastr\` PSR-4 namespace.

## Create the client

Call the static `Mailblastr::client()` factory with your key. Read the key from the environment so the secret never lands in source control.

**mailer.php**

```php
<?php

require __DIR__ . '/vendor/autoload.php';

use Mailblastr\Mailblastr;

$mailblastr = Mailblastr::client(getenv('MAILBLASTR_API_KEY'));
```

A second array argument tunes the client. `timeout` (per-request seconds, default `30`; `0` disables it) and `maxRetries` (default `2`, `0` disables) configure the default curl transport; `baseUrl` overrides the API host, and `transport` swaps in any `Mailblastr\Transport\TransportInterface` implementation — useful for tests. Only `429` and `503` are ever retried, so a retry can never duplicate a send.

```php
$mailblastr = Mailblastr::client(getenv('MAILBLASTR_API_KEY'), [
    'timeout' => 30,     // seconds per request (0 = no timeout)
    'maxRetries' => 2,   // automatic retries on 429/503 only (0 disables)
]);
```

## Send your first email

Every field of the send body is a plain array key — `from` is a reserved word in PHP, so the payload is an associative array rather than named arguments. `to` takes a list of up to 50 recipients.

**send.php**

```php
<?php

require __DIR__ . '/vendor/autoload.php';

use Mailblastr\Mailblastr;

$mailblastr = Mailblastr::client(getenv('MAILBLASTR_API_KEY'));

$sent = $mailblastr->emails->send([
    'from' => 'Acme <hello@yourdomain.com>',
    'to' => ['delivered@mailblastr.dev'],
    'subject' => 'Hello from PHP',
    'html' => '<p>Your first email 🐘</p>',
]);

echo "Sent email {$sent['id']}\n";
```

> **Note:** `delivered@mailblastr.dev` is the MailBlastr simulator: the send is accepted, recorded, and marked delivered without touching a real inbox. Swap it for your own address once the first call succeeds — and never use `@example.com`, which the API rejects with `422`.

## Handling the response

Every method returns the decoded JSON response as an associative array, so a successful send gives you `$sent['id']`. Any non-2xx answer throws `Mailblastr\Exceptions\MailblastrException`, which carries the `{ statusCode, name, message }` envelope. Branch on `getName()` together with `getStatusCode()`, never on the message — messages are scrubbed server-side and are not a stable contract.

```php
use Mailblastr\Exceptions\MailblastrException;

try {
    $sent = $mailblastr->emails->send($payload);
    echo "Sent email {$sent['id']}\n";
} catch (MailblastrException $e) {
    echo $e->getStatusCode();   // 422
    echo $e->getName();         // 'validation_error'
    echo $e->getMessage();      // human text — do not match on it

    // WHICH quota ran out, and what would clear it (null on other errors).
    if ($limit = $e->getLimit()) {
        echo "{$limit['kind']}: {$limit['used']}/{$limit['limit']} used";
    }

    // Reputation gates: whether waiting helps, and until when.
    if ($rep = $e->getReputation()) {
        echo $rep['retryable'] ? "retry at {$rep['retry_at']}" : 'not retryable';
    }

    print_r($e->getBody());      // the whole parsed error body
}
```

A transport-level failure throws the same exception with `getStatusCode() === 0` and a `getName()` of `network_error`, so one `catch` clause covers both.

## Retry safely with an idempotency key

Pass `['idempotencyKey' => '...']` as the second argument so replaying a request returns the original response instead of sending twice. Only `emails->send()` and `batch->send()` honour it — see [Idempotency keys](https://www.mailblastr.com/docs/emails/idempotency).

```php
$mailblastr->emails->send($payload, ['idempotencyKey' => 'order-123']);

// Up to 100 emails in one request (batch items reject attachments and scheduled_at).
$mailblastr->batch->send([
    ['from' => 'hello@yourdomain.com', 'to' => ['delivered@mailblastr.dev'], 'subject' => 'Hi A', 'html' => '<p>A</p>'],
    ['from' => 'hello@yourdomain.com', 'to' => ['delivered@mailblastr.dev'], 'subject' => 'Hi B', 'html' => '<p>B</p>'],
], ['idempotencyKey' => 'orders-2026-08-19']);
```

If a batch fails part way through, the exception names the emails that already went out — read `$e->getSent()` and `$e->getSentCount()` and do **not** resend those.

## Schedule instead of sending now

Add `scheduled_at` (ISO 8601, at most 30 days ahead) to hand the send to the scheduler. A scheduled send goes through the real delivery path, so it cannot target the simulator address — use one of your own recipients.

```php
$scheduled = $mailblastr->emails->send([
    'from' => 'Acme <hello@yourdomain.com>',
    'to' => ['you@yourdomain.com'],
    'subject' => 'Tomorrow morning',
    'html' => '<p>Queued ahead of time.</p>',
    'scheduled_at' => '2026-09-01T09:00:00Z',
]);

$mailblastr->emails->update($scheduled['id'], ['scheduled_at' => '2026-09-01T14:00:00Z']); // reschedule
$mailblastr->emails->cancel($scheduled['id']);                                             // or call it off
```

> **Warning:** Keep the `mb_` API key server-side — load it from an environment variable or secrets manager, never commit it, and never echo it into a Blade view or any browser response. Anyone holding the key can send email as you.

## Next steps

- See the full [Send Email API](https://www.mailblastr.com/docs/api/emails-send) reference for every body field, including [attachments](https://www.mailblastr.com/docs/emails/attachments).
- Using Laravel? See [Send emails with PHP & Laravel](https://www.mailblastr.com/docs/integrations/php-laravel).
- Explore [the SDKs reference](https://www.mailblastr.com/docs/resources/sdks) for the other resources the client exposes — `domains`, `contacts`, `campaigns`, `automations`, `webhooks`, `polls`, and more (plus inbound mail via `emails->receiving`).
- Want no dependency at all? [PHP without the SDK](https://www.mailblastr.com/docs/send-with/php) does the same send with the raw cURL extension.
