Send email from PHP with the Runearc SDK
Plain PHP 8.2 with ext-curl and ext-json, in any framework. This page installs the SDK, sends an email to a test inbox, reads it back and verifies a signed webhook, with the output of a real run.
composer require runearc/runearcNot published yet: this is the command that will work after the release. See the SDK page for the status of every package.


Install
PHP 8.2 or later, with ext-curl and ext-json. It is plain PHP and works in any framework. The commands on this page are the ones that will work once the package is released; none of the five Runearc SDK packages is on a registry yet, and until then the Resend SDK or plain HTTP calls with your API key do the same work. The SDK covers emails, senders, inboxes, messages, threads, conversations, domains, webhooks, keys, suppressions and events. Contacts, lists, campaigns, sequences, templates, forms, pages and analytics are not in it: use the REST API, the CLI or MCP for those.
composer require runearc/runearcCreate an API key in the dashboard and export it as RUNEARC_API_KEY. The quickstart below also reads RUNEARC_FROM, a sender address on a domain you have verified, and RUNEARC_BASE_URL to point at another API host. Tested on PHP 8.5.
Send an email
The whole quickstart: create an inbox, send an email to it, wait for it to arrive and check a webhook signature.
<?php
declare(strict_types=1);
use Runearc\Runearc;
use Runearc\WebhookSignature;
require __DIR__ . '/vendor/autoload.php';
$runearc = new Runearc(); // reads RUNEARC_API_KEY and RUNEARC_BASE_URL
$inbox = $runearc->inboxes->create(['display_name' => 'quickstart']);
echo "inbox {$inbox->address}\n";
$subject = 'Hello from the PHP SDK ' . bin2hex(random_bytes(3));
$sent = $runearc->emails->send([
'from' => getenv('RUNEARC_FROM'),
'to' => $inbox->address,
'subject' => $subject,
'text' => 'It works.',
]);
echo "sent {$sent->id}\n";
$waited = $runearc->messages->wait($inbox->id, ['subject' => $subject, 'after' => $inbox->created_at, 'timeout' => 30]);
if ($waited === null) {
fwrite(STDERR, "no message arrived within 30 seconds\n");
exit(1);
}
echo "received {$waited->message->subject}\n";
// Sign a sample event the way Runearc does (Standard Webhooks), then verify it.
$key = random_bytes(32);
$secret = 'whsec_' . base64_encode($key);
$payload = json_encode(['id' => 'evt_1', 'type' => 'message.delivered', 'data' => ['email_id' => $sent->id]]);
$timestamp = time();
$signature = base64_encode(hash_hmac('sha256', "evt_1.$timestamp.$payload", $key, true));
WebhookSignature::verify($payload, [
'webhook-id' => 'evt_1',
'webhook-timestamp' => (string) $timestamp,
'webhook-signature' => "v1,$signature",
], $secret);
echo "webhook verified\n";
$runearc->inboxes->delete($inbox->id);emails->send takes a plain array with the same fields as the REST API and returns a typed response object with the new email's id. It sends an Idempotency-Key for you, so a retry does not send twice. Passing 'after' => $inbox->created_at to wait is needed because wait only matches messages received after it starts, and the email can arrive before the wait begins.


The same output as selectable text
inbox inbox-e855bt@39vfan.runearc.email
sent eml_01m70s0rpgrxhkn6ebr2ryj9wv
received Hello from the PHP SDK c811d2
webhook verifiedIds and the inbox address are from that one run, on November 9, 2026.
Read an inbox
messages->wait blocks for up to timeout seconds and returns null when nothing arrived, so check for it. When a message matches, it comes back with the one time codes and links already extracted, which is what a sign-up test needs. The screenshot shows the same inbox in the dashboard. See wait for an email for every filter.
$waited = $runearc->messages->wait($inbox->id, ['subject' => $subject, 'after' => $inbox->created_at, 'timeout' => 30]);
if ($waited === null) {
fwrite(STDERR, "no message arrived within 30 seconds\n");
exit(1);
}
echo "received {$waited->message->subject}\n";
// $waited->codes and $waited->links hold the one time codes and links found in the message

Verify a webhook
The lines in the quickstart verify a sample event the script signs itself with a random secret. It is not a live delivery from Runearc; it shows that WebhookSignature::verify accepts a correctly signed body and checks the timestamp, which must be within five minutes. For a real delivery, pass the raw request body and the headers ($_SERVER works), as in the receiver tab.
WebhookSignature::verify($payload, [
'webhook-id' => 'evt_1',
'webhook-timestamp' => (string) $timestamp,
'webhook-signature' => "v1,$signature",
], $secret);
echo "webhook verified\n";use Runearc\Exception\WebhookVerificationException;
use Runearc\WebhookSignature;
$payload = file_get_contents('php://input'); // the raw body, not re-encoded JSON
try {
$event = WebhookSignature::verify($payload, $_SERVER, getenv('RUNEARC_WEBHOOK_SECRET'));
} catch (WebhookVerificationException $e) {
http_response_code(400);
exit($e->getMessage());
}
if ($event['type'] === 'message.received') {
// $event['data']['message_id'], $event['data']['inbox_id'], ...
}
http_response_code(204);The first tab is from the quickstart we ran. The receiver is from the package README and was not part of that run. The signature headers and the secret format are on the webhooks page.
Errors and retries
Methods return typed response objects and throw a RunearcException. It carries status, a stable errorCode to branch on, a kind such as forbidden, a message, and a fix when there is something to do. Subclasses such as RateLimitException and ConnectionException let you catch one case.
use Runearc\Exception\RunearcException;
use Runearc\RequestOptions;
try {
$runearc->emails->send(['from' => 'noreply@acme.com', 'to' => 'bob@example.com', 'subject' => 'Hi', 'text' => 'Hi']);
} catch (RunearcException $e) {
echo $e->status; // 403
echo $e->errorCode; // "sender_not_verified", a stable string to branch on
echo $e->fix; // what to do about it, when there is something to do
}
// A stable key makes your own retries safe too, for example from a job queue.
$runearc->emails->send($email, new RequestOptions(idempotencyKey: "welcome/{$user->id}"));A request is retried up to two times by default, with exponential backoff and jitter, and the wait honours Retry-After. A 429 is always retried; a 5xx, a network error or a timeout is retried only when repeating the request is safe. Other 4xx errors are never retried, and creating an inbox is not repeated after a 5xx. Mail sending gets an Idempotency-Key automatically and every retry reuses it; Runearc remembers keys for 24 hours.
This code is from the package README, not from the quickstart run. Every error code and its fix is listed on the errors page.
Where other SDKs are better
Read on November 9, 2026. Runearc's SDK is new and covers eleven resources; the others have longer histories and, in several cases, more features. We list them in alphabetical order.
Amazon SES
SES covers PHP through the AWS SDK for PHP. Its sample says PHP 5.5 or later and was tested on PHP 7.2.7, which matters if you must support old PHP: Runearc needs 8.2.
Mailgun
Mailgun lists a PHP SDK among its SDKs for Go, Node.js, Java, Ruby and Python.
PHPMailer and symfony/mailer
Most PHP tutorials, including Maileroo's, which we read, are built on mail() or PHPMailer. For a plain SMTP relay they remain the simpler choice.
Postmark
Postmark lists an official PHP library that Postmark maintains itself.
Resend
Resend's PHP package installs with Composer, and its docs include an example project built on Symfony.
Maturity and deliverability track record are where Runearc is behind: the providers above have sent email for longer than we have. For the same comparison across the whole product, see Runearc compared with Resend.
Questions
Is the Composer package on Packagist yet?
Not yet. The package is written and tested, and composer require runearc/runearc is the command that will work after the release. Until then, curl with your API key does the same work.
Which PHP version does it need?
PHP 8.2 or later, with ext-curl and ext-json. We ran the quickstart on PHP 8.5. Responses are typed readonly objects; requests are plain arrays.
Does it work with my framework?
It is plain PHP with no framework code, so it can be called from any framework. We ran the quickstart as a plain script and have not tested it inside a framework.
Is the webhook line in the quickstart a real delivery?
No. The script signs a sample event with a random secret, the way Runearc signs deliveries, and then verifies it. It shows that verification works; it does not show a delivery from Runearc.
Does the SDK cover contacts, campaigns or sequences?
No. It covers emails, senders, inboxes, messages, threads, conversations, domains, webhooks, keys, suppressions and events. Everything else is in the REST API, the CLI and MCP.
Read next
Put Runearc to work
Get inboxes, sending, your own domains and MCP in one workspace. The free plan includes 3 inboxes, 3,000 emails a month and 3 custom domains, with no card.