PHP SDK
Official PHP SDK for the Tratto Email API
The official SDK for PHP: tratto/email, on Packagist. Everything on this
page requires 1.0.0 or newer, except where a section names a later version.
Requirements
- PHP 8.1 or later
- the
curlextension (ext-curl)
There are no other runtime dependencies.
Installation
composer require tratto/emailInitialization
use Tratto\Client;
$tratto = new Client((string) getenv('TRATTO_API_KEY'));Keep the key out of your code: read it from the environment or your secret store. API keys are created in the dashboard, under API Keys.
The second argument takes options:
$tratto = new Tratto\Client($apiKey, [
'baseUrl' => 'https://api.tratto.email', // default
'timeout' => 30, // seconds, default 30 (connection: at most 10)
]);| Option | Type | Description |
|---|---|---|
baseUrl | string | http:// or https:// URL, without /v1 (the SDK adds it). With http:// the key travels in clear text: use it only for a local server. |
timeout | int|float | Total time per request, in seconds. |
transport | Tratto\Http\Transport | Replaces the built-in curl transport, for example with a fake in your tests. |
An empty key, an unknown option or an invalid value throws
\InvalidArgumentException. There is no option to pass curl settings: TLS
verification is always on and redirects are never followed.
Responses
Methods return the decoded JSON as associative arrays, so a field added by the API is available without updating the SDK.
- Single objects come without the
dataenvelope:$tratto->emails->get($id)['status']. - Paginated lists return the whole page,
['data' => [...], 'pagination' => ['hasMore' => bool, 'nextCursor' => ?string]]: passnextCursorasafterto read the next page. - Methods that answer 204 (
webhooks->delete,templates->delete) return nothing. - A
\DateTimeInterfaceyou pass is sent in UTC with milliseconds; a string is sent as it is.
Send Email
$email = $tratto->emails->send([
'from' => 'Acme <hello@yourdomain.com>',
'to' => 'user@example.com',
'subject' => 'Welcome!',
'html' => '<h1>Welcome!</h1>',
'text' => 'Welcome to Tratto',
]);
echo $email['id'];Send with Markdown
Send Markdown and let the server render
responsive email HTML. markdown and html are mutually exclusive.
$tratto->emails->send([
'from' => 'Acme <hello@yourdomain.com>',
'to' => 'user@example.com',
'subject' => 'Welcome!',
'markdown' => "# Welcome, {{firstName}}\n\nGlad to have you on board.",
'variables' => ['firstName' => 'Alice'],
]);Send with Template, Schedule and Idempotency Key
The second argument of emails->send is the
idempotency key: within 24 hours, retrying with the same key returns the
original response instead of sending again (best-effort). emails->send and emails->sendBatch take one, and a key containing a line break is
rejected before sending.
$tratto->emails->send([
'from' => 'hello@yourdomain.com',
'to' => ['alice@example.com', 'bob@example.com'],
'subject' => 'Your receipt',
'templateId' => 'tmpl_abc123',
'variables' => ['name' => 'Alice'],
'scheduledAt' => new DateTimeImmutable('+1 hour'),
], 'receipt-order-1234');In PHP [] is both an empty list and an empty object, and an empty nested
array goes out as a JSON array. For fields that are objects, such as
variables or headers, omit the field or pass new \stdClass().
The array is sent as it is, so keys must match the API's camelCase field
names. With API 0.24.0 or later, a key the API does not know, such as
reply_to or attachments, fails with 422 VALIDATION_ERROR instead of being
dropped. See Unknown fields.
Send a Batch
emails->sendBatch() sends 1 to 100 emails in one request and requires
1.1.0 or newer. Each email takes the fields of send() except
scheduledAt; the second argument is the idempotency key,
which covers the whole batch.
$emails = [
['from' => 'hello@yourdomain.com', 'to' => ['alice@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Alice.'],
['from' => 'hello@yourdomain.com', 'to' => ['bob@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Bob.'],
];
$batch = $tratto->emails->sendBatch($emails, 'receipts-2026-10-21');
// Resend only the failed emails, in a new batch with a new key.
$failed = [];
foreach ($batch['emails'] as $i => $result) {
if ($result['status'] === 'failed') {
$failed[] = $emails[$i];
}
}
if ($failed !== []) {
$tratto->emails->sendBatch($failed, 'receipts-2026-10-21-retry-1');
}The response is always the 202, even when some emails failed: check failed.
Results are in the order of the request, and a failed email has no id but an
error with code and message. After a 503 or a 500 nothing was sent:
retry the same batch with the same key. Quota, limits and errors are in
Send a Batch.
Get Email Status and Timeline
$email = $tratto->emails->get('email_abc123');
echo $email['status']; // 'delivered'
$events = $tratto->emails->listEvents('email_abc123');List Emails
$page = $tratto->emails->list(['status' => 'delivered', 'limit' => 20]);
foreach ($page['data'] as $email) {
echo "{$email['id']}: {$email['status']}\n";
}
if ($page['pagination']['hasMore']) {
$next = $tratto->emails->list(['limit' => 20, 'after' => $page['pagination']['nextCursor']]);
}Other Resources
The client exposes ten resources: emails, contacts, audiences,
campaigns, templates, webhooks, domains, analytics, flows and
workspace. The
README shows every
method, and examples/
has one runnable file per resource plus Laravel and Symfony integrations.
Flows need an API key with the * permission: there are no
flows:read or flows:write scopes.
Error Handling
Every failed API call throws Tratto\TrattoException: HTTP errors, responses
that are not JSON, and network errors. Configuration mistakes throw
\InvalidArgumentException.
use Tratto\TrattoException;
try {
$tratto->emails->send($params);
} catch (TrattoException $e) {
error_log(sprintf('[%d] %s: %s', $e->statusCode, $e->errorCode, $e->getMessage()));
// $e->suggestion: what to do about it, when the API says
// $e->docs: a link to the documentation of this error, when available
}| Property | Type | |
|---|---|---|
getMessage() | string | The API message, or HTTP <status> when the response has none |
errorCode | string | The API error code, or unknown_error, invalid_response, invalid_header, connection_error |
statusCode | int | The HTTP status (also getCode()), 0 when no response was received |
suggestion | ?string | The API's own words on what to do, null when the response has none |
docs | ?string | A link to the page that explains the code, null when the response has none |
details | ?array | The API's details as they are, for example a 422's list of ['path' => '/emails/3/to', 'message' => ...]; null when absent. Requires 1.1.0 or newer |
errorCode is the same code listed in Error Codes.
The SDK does not retry. To retry a send safely, pass the same idempotency key.
PHP 8.1: Do Not Log Full Exception Traces
On PHP 8.2 and later the API key and the webhook secret are marked
#[\SensitiveParameter] and never appear in the SDK's own traces. PHP 8.1
ignores that attribute: the SDK moves them out of its parameters before doing
anything else, but an argument of the wrong type (for example an array where a
string is expected) raises a TypeError before that, and the trace of that
error holds the arguments.
On PHP 8.1, log the message, errorCode and statusCode of SDK exceptions,
not getTraceAsString() or a dump of the exception, or set
zend.exception_ignore_args=On.
Verify Webhooks
Tratto\Webhook::verify() checks the X-Tratto-Signature header of a
delivery: the HMAC-SHA256 of {t}.{raw body} with your secret (whsec_...,
used as it is). It accepts several v1 values (secret rotation) and rejects
deliveries older or newer than the tolerance, 5 minutes by default. It returns
bool and never throws for a bad signature. See
Webhook Verification for the scheme.
Always pass the raw body: a decoded and re-encoded JSON will not match.
use Tratto\Webhook;
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_TRATTO_SIGNATURE'] ?? null;
if (!Webhook::verify($payload, $header, (string) getenv('TRATTO_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);
// Deliveries can repeat: use $event['id'] to process each event once.The tolerance is the fourth argument, in seconds:
Webhook::verify($payload, $header, $secret, 600).
Not Included
Members and the workspace itself are managed from the dashboard: the SDK has no
workspace->delete(), inviteMember(), removeMember() or updateMember().
Links
- Packagist: https://packagist.org/packages/tratto/email
- GitHub: https://github.com/tratto-email/tratto-php
- API Docs: API Reference
Edit this page on GitHub
Last updated on