PHPackages                             easymailing/sdk-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. easymailing/sdk-php

ActiveLibrary[API Development](/categories/api)

easymailing/sdk-php
===================

Official Easymailing SDK for PHP backends.

v0.6.1-alpha.0(1mo ago)013MITPHPPHP ^8.1

Since May 27Pushed 1mo agoCompare

[ Source](https://github.com/easymailing/sdk-php)[ Packagist](https://packagist.org/packages/easymailing/sdk-php)[ Docs](https://github.com/easymailing/easymailing-sdk)[ RSS](/packages/easymailing-sdk-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (7)Versions (8)Used By (0)

easymailing/sdk-php
===================

[](#easymailingsdk-php)

Official Easymailing SDK for PHP backends.

> **Alpha.** Public API may still change before `1.0.0`. Pin to a known version (e.g. `"easymailing/sdk-php": "v0.6.0-alpha.0"`) in production.

> **📦 This is a read-only mirror.** The SDK lives in the [`easymailing/easymailing-sdk`](https://github.com/easymailing/easymailing-sdk) monorepo under [`packages/php/`](https://github.com/easymailing/easymailing-sdk/tree/main/packages/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 read `composer.json` from monorepo subdirectories.

Backend only
------------

[](#backend-only)

This SDK authenticates with a secret API key or OAuth access token. **Do not use it in client-rendered code.**

Requirements
------------

[](#requirements)

- PHP **8.1+**
- `ext-json`
- `ext-curl` (default transport — drop in `Psr18Transport` or `WordPressTransport` if you can't have cURL)

Install
-------

[](#install)

```
composer require easymailing/sdk-php:^0.6.0-alpha
```

Quick start
-----------

[](#quick-start)

```
use Easymailing\Sdk\Easymailing;

$em = new Easymailing(apiKey: getenv('EASYMAILING_API_KEY'));

// Generated resources — methods return typed DTO objects
$page = $em->audiences->list(['page' => 1]);
foreach ($page->data as $audience) {
    echo $audience->iri, ' — ', $audience->title, "\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
-------------------

[](#generated-resources)

Most API endpoints are exposed as generated resources on the client. The full reference lives in [`docs/resources.md`](./docs/resources.md).

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`.

Auth
----

[](#auth)

Pass exactly one of `apiKey` or `accessToken`. The constructor throws on both/neither and on empty strings.

```
new Easymailing(apiKey: '...');        // sends X-Auth-Token
new Easymailing(accessToken: '...');   // sends Authorization: Bearer
```

Hydra identity: `iri` + `uuid`
------------------------------

[](#hydra-identity-iri--uuid)

Every generated DTO carries `?string $iri` (the Hydra `@id` verbatim) and `?string $uuid` (the trailing UUID segment) as first-class typed properties. The `@id`, `@type` and `@context` keys are stripped — they're transport metadata, not domain fields.

```
$audience = $em->audiences->get('AUD-UUID');
echo $audience->iri;   // "/audiences/AUD-UUID"
echo $audience->uuid;  // "AUD-UUID"
echo $audience->title; // domain field
```

If you only have an IRI (e.g. from a webhook payload), use `IriExtractor` to peel off the trailing segment:

```
use Easymailing\Sdk\Hydra\IriExtractor;
IriExtractor::extract('/audiences/AUD-UUID/members/MEM-UUID'); // "MEM-UUID"
```

Transports
----------

[](#transports)

Three transports ship in the box:

- `CurlTransport` (default) — zero deps, requires `ext-curl`.
- `Psr18Transport` — adapter for any PSR-18 client (Guzzle, Symfony HttpClient with `Psr18Client`, Buzz, etc.).
- `WordPressTransport` — wraps `wp_remote_request` so the SDK works inside a WP plugin without bundling Guzzle.

```
use Easymailing\Sdk\Transport\WordPressTransport;

$em = new Easymailing(
    apiKey: '...',
    transport: new WordPressTransport(timeoutSeconds: 30),
);
```

Bodies are JSON-encoded automatically and the SDK sets `Content-Type: application/json` on POST/PUT/PATCH/DELETE-with-body. If you pass your own `Content-Type` header (any casing) it wins — useful for `application/merge-patch+json` and similar.

Errors
------

[](#errors)

All API errors derive from `EasymailingException` and follow RFC 7807:

- `AuthException` (401/403)
- `NotFoundException` (404)
- `ValidationException` (422) — exposes `$violations` (Symfony format)
- `RateLimitException` (429) — exposes `$retryAfterSeconds`
- `ServerException` (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.

Telemetry events
----------------

[](#telemetry-events)

Plug in your own observability without the SDK depending on a specific logger. The client emits structured `SdkEvent` instances for every request lifecycle stage, batch poll, and webhook signature check through a single `onEvent`callable.

```
use Easymailing\Sdk\Easymailing;
use Easymailing\Sdk\Telemetry\SdkEvent;
use Easymailing\Sdk\Telemetry\EventTypes;

$em = new Easymailing(
    apiKey: '...',
    onEvent: function (SdkEvent $event): void {
        match ($event->type) {
            EventTypes::REQUEST_END => $myLogger->log([
                'requestId' => $event->requestId,
                'pathTemplate' => $event->pathTemplate, // "/audiences/{audienceUuid}/members/{uuid}" — metric-safe
                'status' => $event->payload['status'],
                'durationMs' => $event->payload['durationMs'],
                'error' => $event->payload['error'] ?? null, // 422 violations preserved
            ]),
            EventTypes::WEBHOOK_REJECTED => $siem->alert($event->payload),
            default => null,
        };
    },
);
```

### Event types

[](#event-types)

Type constantWhen`EventTypes::REQUEST_START`Before the request leaves the SDK.`EventTypes::REQUEST_RETRY`The SDK is about to retry (429, 5xx, network).`EventTypes::REQUEST_END`Request finished. `status` + optional `error` + `durationMs`.`EventTypes::BATCH_POLLING`Each poll iteration in `batch->wait()`. Carries snapshot progress.`EventTypes::BATCH_FINISHED`Batch reached `finished`.`EventTypes::BATCH_TIMEOUT``wait()` ran past `maxWaitMs` — `BatchTimeoutException` is thrown.`EventTypes::WEBHOOK_VERIFIED``webhooks->verify()` accepted the signature.`EventTypes::WEBHOOK_REJECTED``webhooks->verify()` rejected (`signature-mismatch`, `invalid-format`, `invalid-secret`).Each `SdkEvent` carries `type`, `v: 1` (schema version), `timestampMs`, optional `requestId` (16-char hex — same value across `start` / `retry` / `end`for one logical request), `method`, `path`, `pathTemplate`, and a `payload`array specific to the event type.

### PSR-3 adapter

[](#psr-3-adapter)

If you already use a PSR-3 logger (Monolog, Symfony, Laravel, etc.), the SDK ships a `Psr3EventListener` that maps event types to log levels with a sensible default (5xx → error, retry/4xx/timeout/webhook-rejected → warning, everything else → debug):

```
use Easymailing\Sdk\Easymailing;
use Easymailing\Sdk\Telemetry\Psr3EventListener;

$em = new Easymailing(
    apiKey: '...',
    onEvent: new Psr3EventListener($monolog),
);
```

The adapter requires `psr/log` — install it explicitly if your project doesn't already pull it in (`composer require psr/log`).

### Safety, correlation, cardinality

[](#safety-correlation-cardinality)

- **`requestId`** correlates one full request lifecycle across `start`, `retry`, `end`.
- **`pathTemplate`** (`/audiences/{audienceUuid}/members/{uuid}`) is metric-safe — no UUID cardinality blow-up in Prometheus / Datadog. `path` carries the real substituted URL.
- **Safety**: a throwing `onEvent` handler can **never** break the API call — the SDK wraps every invocation in try/catch and logs to `error_log` only.
- **No secrets**: the SDK never emits auth headers, request body, or webhook secrets. 422 `violations` are kept because UIs need them.

Batch
-----

[](#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

[](#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 plain `fetchResponses()`?**The API writes the results file *asynchronously* after the status flips to `finished`, and **auto-deletes it 1 hour later**. `fetchResponsesGuaranteed` calls 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 bare `fetchResponses($snapshot)` only when you already know the snapshot has a fresh `response_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

[](#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

[](#no-batchfinished-webhook)

The API does **not** currently emit a webhook event when a batch finishes. You must poll (or subscribe to the SDK's `batch.finished` telemetry event if your own process did the wait).

Webhooks
--------

[](#webhooks)

```
$em = new Easymailing(apiKey: '...');

if ($em->webhooks->verify($rawBody, $signature, $secret)) {
    $event = $em->webhooks->parse($rawBody);
    error_log($event['event_type']);
}
```

`verify()` uses `hash_hmac` + `hash_equals` (constant-time). Signature must start with `sha256=` followed by hex. Both verified and rejected outcomes are emitted as `webhook.verified` / `webhook.rejected` telemetry events.

`parse()` returns an `array{event_type: string, webhook_id?: string, data: mixed}`(a discriminated array shape, not an object — keeps the wire payload faithful).

### Typed event constants

[](#typed-event-constants)

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;

$event = $em->webhooks->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.

Changelog
---------

[](#changelog)

See [`CHANGELOG.md`](./CHANGELOG.md). Releases ship via [release-please](https://github.com/googleapis/release-please): a `feat:` or `fix:` commit in the monorepo opens a Version PR; merging it tags the monorepo, the split workflow propagates the tag to this mirror, and Packagist auto-publishes via webhook.

License
-------

[](#license)

MIT — see [LICENSE](./LICENSE).

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance89

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 85.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

7

Last Release

57d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3948223?v=4)[Sergio](/maintainers/h3llr4iser)[@h3llr4iser](https://github.com/h3llr4iser)

---

Top Contributors

[![h3llr4iser](https://avatars.githubusercontent.com/u/3948223?v=4)](https://github.com/h3llr4iser "h3llr4iser (6 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

sdkHydrawebhooksemail marketingjsonldapi clientnewsletterMarketing Automationtransactional emaileasymailing

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/easymailing-sdk-php/health.svg)

```
[![Health](https://phpackages.com/badges/easymailing-sdk-php/health.svg)](https://phpackages.com/packages/easymailing-sdk-php)
```

###  Alternatives

[activecampaign/api-php

Official PHP wrapper for the ActiveCampaign API.

1162.3M13](/packages/activecampaign-api-php)[crowdin/crowdin-api-client

PHP client library for Crowdin API v2

621.7M5](/packages/crowdin-crowdin-api-client)[immobiliarelabs/braze-sdk

An sdk to interact with Braze API

2286.2k1](/packages/immobiliarelabs-braze-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
