easymailing / sdk-php
Official Easymailing SDK for PHP backends.
Requires
- php: ^8.1
- ext-json: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.50
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
Suggests
- psr/http-client: Required to use Psr18Transport (Guzzle, Symfony HttpClient, etc.)
- psr/http-factory: Required to use Psr18Transport (PSR-17 request/stream factories)
This package is auto-updated.
Last update: 2026-05-27 18:02:21 UTC
README
Official Easymailing SDK for PHP backends.
Alpha. Public API may still change before
1.0.0.
📦 This is a read-only mirror. The SDK lives in the
easymailing/easymailing-sdkmonorepo underpackages/php/. Submit issues, pull requests, and discussions upstream — every commit and tag here is auto-generated by a subtree split workflow. Packagist watches this mirror because it does not readcomposer.jsonfrom monorepo subdirectories.
Backend only
This SDK authenticates with a secret API key or OAuth access token. Do not use it in client-rendered code.
Requirements
- PHP 8.1+
ext-jsonext-curl(default transport — drop inPsr18TransportorWordPressTransportif you can't have cURL)
Install
composer require easymailing/sdk-php:^0.1.0-alpha
Quick start
use Easymailing\Sdk\Easymailing; $em = new Easymailing(apiKey: getenv('EASYMAILING_API_KEY')); // Generated resources $page = $em->audiences->list(['page' => 1]); foreach ($page->data as $audience) { echo $audience['name'] ?? '', "\n"; } $campaign = $em->campaigns->createRevalidation($body); $members = $em->members->search(email: 'sergio@example.com'); $forms = $em->audiences('audience-uuid')->subscriptionForms->list(); $order = $em->stores('store-uuid')->orders->import($body); $variants = $em->stores('store-uuid')->products('product-uuid')->variants->list();
Generated resources
Most API endpoints are exposed as generated resources on the client. The complete list is generated here:
Common call shapes:
// Top-level collection $em->audiences->list(['page' => 1]); $em->audiences->create($body); $em->audiences->get('audience-uuid'); // Nested resources $em->audiences('audience-uuid')->members->list(['status' => 'suscriber.status.confirmed']); $em->audiences('audience-uuid')->subscriptionForms->list(); // Actions and custom operations $em->campaigns->createRevalidation($body); $em->members->search(email: 'sergio@example.com'); $em->stores('store-uuid')->orders->import($body); $em->stores('store-uuid')->orders->refund('order-resource-id', $body); // Deep nested resources $em->stores('store-uuid')->products('product-resource-id')->variants->list();
Request bodies can be plain arrays or generated DTOs. Entity methods return
generated DTOs; collection methods return Page<DTO>.
Auth
Pass exactly one of apiKey or accessToken. The constructor throws on both/neither and on empty strings.
new Easymailing(apiKey: 'em_live_...'); // sends X-Auth-Token new Easymailing(accessToken: 'oauth-...'); // sends Authorization: Bearer
Transports
Three transports ship in the box:
CurlTransport(default) — zero deps, requiresext-curl.Psr18Transport— adapter for any PSR-18 client (Guzzle, Symfony HttpClient withPsr18Client, Buzz, etc.).WordPressTransport— wrapswp_remote_requestso the SDK works inside a WP plugin without bundling Guzzle.
use Easymailing\Sdk\Transport\WordPressTransport; $em = new Easymailing( apiKey: '...', transport: new WordPressTransport(timeoutSeconds: 30), );
Errors
All API errors derive from EasymailingException and follow RFC 7807:
AuthException(401/403)NotFoundException(404)ValidationException(422) — exposesgetViolations()(Symfony format)RateLimitException(429) — exposes$retryAfterSecondsServerException(5xx)NetworkException(transport-level: DNS, timeout, etc.)MalformedResponseException(server returned non-JSON or wrong shape)
The client retries idempotent requests, 429s, and 503s with exponential backoff. Configurable via the maxRetries constructor arg.
Batch
The /batch_operations endpoint is asynchronous: you submit up to 500 operations, the server processes them in the background, and the SDK polls until done. The whole flow can take seconds to many minutes depending on size and rate limits.
Two flavours
run() — blocking, for CLI / workers / long-lived processes:
use Easymailing\Sdk\Batch\BatchTypes\BatchOperation; $result = $em->batch->run([ new BatchOperation( method: 'POST', path: '/audiences/AUD-UUID/members', body: ['email' => 'a@b.c'], // SDK serializes arrays automatically externalIdentifier: 'import-1', ), ]); echo $result->snapshot->status; // "finished" echo count($result->responses ?? []); // number of operations echo $result->errors?->totalErrors ?? 0; // null if no errors
Polling uses exponential backoff (1s → 2s → 4s → ... cap 30s) with jitter. Default maxWaitMs is 30 minutes; on timeout, BatchTimeoutException is thrown but the batch keeps running server-side and the exception carries the UUID so a worker can resume.
runAsync() — fire-and-forget, for HTTP / Symfony controllers / FPM:
// In your controller — returns in one round-trip: $snapshots = $em->batch->runAsync($operations); $this->db->saveJob(['batch_uuid' => $snapshots[0]->uuid, 'status' => 'pending']); return new JsonResponse(['job_id' => $snapshots[0]->uuid], 202); // Later, in a Symfony Messenger consumer or worker process: $em->batch->wait($uuid); $responses = $em->batch->fetchResponsesGuaranteed($uuid);
Why
fetchResponsesGuaranteed()and not plainfetchResponses()? The API writes the results file asynchronously after the status flips tofinished, and auto-deletes it 1 hour later.fetchResponsesGuaranteedcalls the regenerate endpoint when the file isn't there — covering both the post-finish race window and old batches whose file already expired. Use the barefetchResponses($snapshot)only when you already know the snapshot has a freshresponse_body_url.
Why two methods? PHP-FPM has max_execution_time (typically 30–60 s). Calling run() from a controller will deadlock against that timeout for any non-trivial batch. Use runAsync() there and wait() from a worker.
Low-level primitives
create, get, wait, fetchResponses, errors, regenerateResponseBodyUrl are all exposed for custom flows. The presigned response_body_url expires after 15 minutes — use regenerateResponseBodyUrl($uuid) to get a fresh one if you need to download the file again.
No batch.finished webhook
The API does not currently emit a webhook event when a batch finishes. You must poll. Watch for batch_operation.finished in future API releases — if it ships, switch your worker from wait() to a webhook handler.
Webhooks
$em = new Easymailing(apiKey: '...'); if ($em->webhooks->verify($rawBody, $signature, $secret)) { $event = $em->webhooks->parse($rawBody); error_log($event->eventType); }
verify() uses hash_hmac + hash_equals (constant-time). Signature must start with sha256= followed by hex.
Typed events
The WebhookEvents class exposes one public const per known event_type. Compare against it instead of hand-writing string literals:
use Easymailing\Sdk\Generated\Webhooks\WebhookEvents; use Easymailing\Sdk\Webhooks\EventParser; $event = EventParser::parse($rawBody); match ($event['event_type']) { WebhookEvents::MEMBER_SUBSCRIBED => handleSubscribed($event['data']), WebhookEvents::MEMBER_CAMPAIGN_BOUNCED => handleBounce($event['data']), default => null, // unknown event types still arrive };
WebhookEvents::all() returns the full list. The catalogue is generated from the upstream WebhookEventType PHP enum (composer generate:webhooks). $event['data'] stays loosely typed (mixed) for now; a follow-up plan will tighten it once the upstream payload DTOs are catalogued.
Status
Implementation tracked in:
docs/superpowers/specs/2026-05-25-easymailing-sdk-design.mddocs/superpowers/plans/
License
MIT — see LICENSE.