Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Createsend Php Laravel Package

campaignmonitor/createsend-php

Official PHP client for Campaign Monitor’s email marketing and transactional API. Manage campaigns, lists, subscribers, templates, and webhooks, or send transactional email. Install via Composer and authenticate with your API key.

Deep Wiki
Context7

Getting Started

campaignmonitor/createsend-php is a plain PHP SDK, not a Laravel package. There is no service provider, facade, config file, or auto-discovery. You wire it up yourself, which is usually 15 minutes of work and then a thin service class you use everywhere.

1. Install

composer require campaignmonitor/createsend-php

Since the last release was October 2021, also add a platform constraint and verify it on your PHP version early:

"config": { "platform": { "php": "8.1" } }

2. Put credentials in config

// config/services.php
'campaign_monitor' => [
    'api_key'   => env('CAMPAIGN_MONITOR_API_KEY'),
    'client_id' => env('CAMPAIGN_MONITOR_CLIENT_ID'),
    'list_id'   => env('CAMPAIGN_MONITOR_LIST_ID'),
],

The API key is account-wide, so treat it like a database password: .env only, never in a frontend or a log line.

3. Wrap the library once

namespace App\Services\CampaignMonitor;

use CS_REST_Clients;
use CS_REST_General;
use CS_REST_Subscribers;

class Client
{
    public function __construct(private array $config) {}

    private function auth(): array
    {
        return ['api_key' => $this->config['api_key']];
    }

    public function general(): CS_REST_General
    {
        return new CS_REST_General($this->auth());
    }

    public function client(): CS_REST_Clients
    {
        return new CS_REST_Clients($this->config['client_id'], $this->auth());
    }

    public function subscribers(?string $listId = null): CS_REST_Subscribers
    {
        return new CS_REST_Subscribers(
            $listId ?? $this->config['list_id'],
            $this->auth()
        );
    }
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(\App\Services\CampaignMonitor\Client::class, fn () => new \App\Services\CampaignMonitor\Client(
    config('services.campaign_monitor')
));

4. Smoke test before building anything

$result = app(\App\Services\CampaignMonitor\Client::class)->general()->get_clients();

if (! $result->was_successful()) {
    throw new RuntimeException($result->response->Message ?? 'Auth failed');
}

dump($result->response); // confirms key + account access

5. First real use case: subscribe on registration

$result = app(\App\Services\CampaignMonitor\Client::class)->subscribers()->add([
    'EmailAddress'  => $user->email,
    'Name'          => $user->name,
    'CustomFields'  => [['Key' => 'signup_source', 'Value' => 'web']],
    'Resubscribe'   => true,
]);

if (! $result->was_successful()) {
    report(new \App\Exceptions\CampaignMonitorException(
        $result->response->Message ?? 'subscribe failed',
        $result->http_status_code
    ));
}

Where to look first: the package README (it contains the canonical examples), then the class/ directory. Every resource is a CS_REST_* class taking ($resourceId, $auth) and returning a CS_REST_Wrapper_Result.

Implementation Patterns

Always route through your own wrapper, never call CS_REST_* from controllers. The library has no DI, no events, and no logging hooks. Your wrapper is the seam for testing, retries, and exception mapping.

Convert result objects into exceptions once.

namespace App\Exceptions;

use CS_REST_Wrapper_Result;
use RuntimeException;

class CampaignMonitorException extends RuntimeException
{
    public static function fromResult(string $operation, CS_REST_Wrapper_Result $result): self
    {
        $message = $result->response->Message ?? 'Unknown Campaign Monitor error';

        return new self("Campaign Monitor [$operation] failed: {$message}", $result->http_status_code);
    }
}

Then every wrapper method ends with:

if (! $result->was_successful()) {
    throw CampaignMonitorException::fromResult('subscribe', $result);
}

return $result->response;

Push writes to the queue, not the request. Signup and profile-update flows should dispatch a job. Campaign Monitor rate-limits per account and a slow API call in a registration request is a bad time.

class SyncSubscriberToCampaignMonitor implements ShouldQueue
{
    use Queueable, SerializesModels;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function __construct(public string $email, public string $name) {}

    public function handle(Client $cm): void
    {
        $result = $cm->subscribers()->add([
            'EmailAddress' => $this->email,
            'Name'         => $this->name,
            'Resubscribe'  => true,
        ]);

        // Treat "already subscribed" as success; it's the normal idempotent path.
        if (! $result->was_successful() && $result->http_status_code !== 400) {
            throw CampaignMonitorException::fromResult('subscribe', $result);
        }
    }
}

Use model observers for lifecycle sync, jobs for delivery.

User::created(fn (User $user) => SyncSubscriberToCampaignMonitor::dispatch($user->email, $user->name));
User::updated(fn (User $user) => $user->wasChanged('email')
    ? SyncSubscriberToCampaignMonitor::dispatch($user->email, $user->name)
    : null);

Transactional email: use Smart Email, not Mail::send. If your marketing team builds templates in Campaign Monitor, send them from the app via the Transactional API so the template stays in one place.

use CS_REST_Transactional_SmartEmail;

$smart = new CS_REST_Transactional_SmartEmail($smartEmailId, ['api_key' => $apiKey]);

$result = $smart->send([
    'To'              => [$user->email],
    'ConsentToTrack'  => 'Yes',
    'Data'            => [
        'first_name' => $user->name,
        'reset_link' => $signedUrl,
    ],
]);

if (! $result->was_successful()) {
    throw CampaignMonitorException::fromResult('transactional.send', $result);
}

Smart Email IDs are per-client and per-environment, so keep them in config keyed by template name, not hardcoded in services.

Lists and segments. There is no "get all lists" on CS_REST_Lists; use clients()->get_lists(). Fetching subscribers is paginated, so loop until you get a short page and be explicit about page size to avoid hammering the API.

$page = 1;
do {
    $result = $cm->subscribers($listId)->get([], 1000, $page);
    if (! $result->was_successful()) { throw CampaignMonitorException::fromResult('subscribers.get', $result); }

    $batch = $result->response->Results ?? [];
    // process $batch
    $page++;
} while (count($batch) === 1000);

Webhooks. Configure them in the Campaign Monitor UI and handle them in a queued controller. Campaign Monitor does not sign payloads (historically), so treat the endpoint as untrusted: use an unguessable path, verify by re-fetching the subscriber, and dedupe on the event's timestamp/email pair so retries don't double-process.

Multi-tenant/OAuth. For a SaaS where each tenant has their own Campaign Monitor account, store the OAuth access/refresh token per tenant and pass them in the auth array instead of api_key. Do not use one shared key with client_id switching unless you are intentionally centralizing billing and access.

Testing. Because the SDK uses raw cURL, Http::fake() will not intercept it. That is the single biggest testing gotcha. Extract an interface (CampaignMonitorClient) with your typed methods, bind the real implementation in production, and bind a fake in tests:

$this->mock(CampaignMonitorClient::class, function ($mock) {
    $mock->shouldReceive('subscribe')->once()->with('[email protected]', 'Jane');
});

For integration tests, use a separate Campaign Monitor account with test lists and clean up afterwards; do not point tests at the production key.

Scheduled campaigns. CS_REST_Campaigns supports creating and sending campaigns programmatically, but for app-driven mail (password resets, receipts, alerts) you want Transactional, not the campaign send endpoint. Campaigns are for segmentation and broadcasts owned by marketing.

Gotchas and Tips

  • No exceptions from the SDK. Every method returns CS_REST_Wrapper_Result. If you forget was_successful(), failures disappear silently. Enforce the exception-mapping pattern above in code review.
  • Http::fake() does nothing here. The transport is internal cURL. Wrap the library or you cannot test your integration logic.
  • Responses are stdClass, not arrays. JSON is decoded as objects, so use $result->response->Results, not $result->response['Results']. This bites when you copy examples from an associative-array mindset.
  • Global class names, no PSR-4. Classes are CS_REST_Subscribers, CS_REST_Lists, etc., loaded through the package's own autoloader. IDE completion is poor and class name collisions are possible; import explicitly and run composer dump-autoload -o on deploy.
  • Maintenance is thin. Last release was 2021. Expect PHP 8.2/8.3 deprecation noise (implicit nullable parameters, dynamic properties) and check that the package is installable against your platform before you commit to it. If it breaks, you own the fork.
  • Account-wide API key. One key can access every client and list. Never ship it to the browser, redact it in exception handlers, and rotate it immediately if it appears in logs or Sentry payloads.
  • Rate limits are real and per-account. Bulk imports and backfills should chunk with a sleep or use a low-rate queue. Handle HTTP 429 with exponential backoff ($backoff = [10, 60, 300]) rather than retrying immediately.
  • Custom fields must already exist in the list. Sending an unknown Key returns an error for the whole subscribe call, not just that field. Validate field keys against lists()->get_custom_fields() at deploy time or in a console command.
  • Date custom fields use YYYY-MM-DD. Multi-select fields accept arrays, and clearing a value has different semantics than setting an empty string. Look at the field's DataType before mapping.
  • Timezone and date strings. Response timestamps come back as strings in the account timezone. Wrap them in Carbon::parse(...)->setTimezone(config('app.timezone')); don't strtotime and assume UTC.
  • Unsubscribe vs delete. unsubscribe marks the subscriber inactive (what compliance wants); delete is for GDPR erasure requests. Pick deliberately per flow.
  • Don't print_r($result) in production. Responses contain subscriber PII (emails, names, custom fields). Log the HTTP status and Campaign Monitor Message only.
  • Extension points are minimal. There is no official event system or transport injection. If you need custom retry, metrics, or logging, do it in your wrapper, not by subclassing CS_REST_* internals.
  • Verify credentials in a health check. A small artisan command that calls general()->get_clients() and asserts success will catch rotated or expired keys before your signup flow does.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core