pufferpost / sdk
Framework-agnostic PHP SDK for the transactional email API.
Requires
- php: >=8.2
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Framework-agnostic PHP SDK for the transactional email API. MIT-licensed.
Full documentation: docs/ — sending, batch, messages, error handling, testing.
Install
composer require pufferpost/sdk
Send a templated email
use PufferPost\Sdk\Client; use PufferPost\Sdk\Email; $client = new Client('key_live_…'); $result = $client->send(Email::fromTemplateId( from: 'no-reply@acme.com', to: 'jane@example.com', templateId: 'tpl_…', data: ['name' => 'Jane'], )); echo $result->id; // msg_… echo $result->status; // accepted
Pass an idempotency key to make a retry safe:
$client->send($email, idempotencyKey: 'order-1234-welcome');
Send an inline email (no template)
Deliver a subject + HTML body as-is — the API renders nothing:
$client->send(Email::inline( from: 'no-reply@acme.com', to: 'jane@example.com', subject: 'Your receipt', html: '<h1>Thanks!</h1>', ));
Provide a template or inline subject + html, never both.
Attachments, cc/bcc, reply-to
Any send (templated or inline) can carry these via the Email constructor:
use PufferPost\Sdk\Attachment; $client->send(new Email( from: 'no-reply@acme.com', to: 'jane@example.com', subject: 'Your receipt', html: '<h1>Thanks!</h1>', replyTo: 'support@acme.com', cc: ['ops@acme.com'], bcc: ['audit@acme.com'], attachments: [Attachment::fromContents('receipt.pdf', 'application/pdf', $pdfBytes)], ));
Send a batch
Up to 100 messages in one call; each is accepted or rejected independently:
$result = $client->sendBatch([$email1, $email2]); foreach ($result->results as $item) { echo $item->accepted ? $item->id : $item->errorCode; }
Retrieve a message
$message = $client->getMessage('msg_…'); echo $message->status; // delivered
Error handling
Every non-2xx response raises a typed exception carrying the stable error code, the HTTP
status, and the request_id:
use PufferPost\Sdk\Exception\ApiException; use PufferPost\Sdk\Exception\RateLimitException; use PufferPost\Sdk\Exception\ValidationException; try { $client->send($email); } catch (RateLimitException $e) { // back off and retry } catch (ValidationException $e) { echo $e->errorCode; // e.g. recipient_suppressed } catch (ApiException $e) { error_log($e->getMessage().' ('.$e->requestId.')'); }
Network failures (DNS/TLS/timeout) raise PufferPost\Sdk\Exception\TransportException.
Framework integration
Symfony and Laravel users get a drop-in Mailer transport so $mailer->send($email) flows
through this API — see the companion mailer-symfony package.
Testing
Inject a Symfony\Component\HttpClient\MockHttpClient to test without network calls:
$client = new Client('key_test_…', 'https://pufferpost.com', $mockHttpClient);