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

Sendyphp Laravel Package

jacobbennett/sendyphp

SendyPHP is a simple PHP client for the Sendy email newsletter API, making it easy to manage subscribers, lists, and campaigns from your Laravel or PHP application with clean, readable methods.

Deep Wiki
Context7

Getting Started

This is a thin, framework-agnostic wrapper around Sendy's form-POST API — one class, no service provider, no facades. Treat it as a convenience layer over curl, not as an SDK.

Install and inspect

composer require jacobbennett/sendyphp

Before writing any code, open vendor/jacobbennett/sendyphp/src/ and read the single class file. The public surface is small (constructor plus a handful of methods), and confirming the exact method names and argument order there will save you more time than any docs will. Check composer.json for the declared PHP constraint — the last release was January 2021, so verify it installs cleanly on your PHP version before committing to it.

First use case: subscribe a user to a list

The canonical day-one call is adding an email to a list from a signup form or a post-purchase hook:

use JacobBennett\SendyPhp\Sendy;

$sendy = new Sendy(
    config('services.sendy.url'),   // e.g. https://newsletter.example.com
    config('services.sendy.api_key')
);

$response = $sendy->subscribe(
    config('services.sendy.list_id'),
    $request->email,
    $request->name ?? '',
    ['plan' => 'pro']               // custom fields, flat key => value
);

In Laravel, register it as a singleton so the client is constructed once per request:

// AppServiceProvider::register()
$this->app->singleton(Sendy::class, fn () => new Sendy(
    config('services.sendy.url'),
    config('services.sendy.api_key'),
));

Put SENDY_URL, SENDY_API_KEY, and SENDY_LIST_ID in config/services.php and .env. The list ID and API key are per-brand in Sendy, not per-installation — if you run multiple brands off one Sendy install, you need multiple client instances, each with its own key.

Implementation Patterns

Wrap it, don't sprinkle it. The raw class returns Sendy's plain-text responses, which are hostile to application logic. Add one domain-level service that owns the client, normalizes responses into booleans/enums, and logs failures. Everything else in your app talks to that service.

class Newsletter
{
    public function __construct(private Sendy $sendy) {}

    public function subscribe(string $email, string $name = '', array $fields = []): bool
    {
        $result = trim((string) $this->sendy->subscribe(
            config('services.sendy.list_id'), $email, $name, $fields
        ));

        return $result === '1' || $result === 'true';
    }
}

Always call it from a queued job. Sendy is a remote HTTP call to a self-hosted box that may be slow, rate-limited, or briefly down. User-facing requests should dispatch a job:

dispatch(new SyncSubscriberToSendy($user))->onQueue('marketing');

This also lets you retry with backoff, which the package does not do for you.

Use subscriptionStatus for reconciliation, not for gating. Sendy's status endpoint is the right tool for a nightly sync job or a webhook backfill, but do not call it inline on page loads — it's an extra round trip to your own server for every request. Cache the result with a short TTL if you must expose it.

Custom fields require pre-provisioning. Sendy silently drops custom field values that don't match a custom field defined in the list's settings. Decide the field schema in the Sendy UI first, then pass a flat associative array. Nested arrays are not serialized — flatten them yourself (['utm_source' => $utm], not ['utm' => ['source' => ...]]).

Testing. There's no HTTP abstraction to swap, so don't try to mock the class internals. Instead, bind a fake Sendy subclass (or a small interface you own that the real client implements) in the container during tests, and assert your job/service called it with the right arguments. Use Http::fake() only if you fork the package to use Laravel's HTTP client — see below.

Fork-friendly extension point. Because the class is small and unmaintained, the pragmatic move for a long-lived project is to copy it into app/Support/Sendy.php and replace the transport with Illuminate\Support\Facades\Http. You keep the method signatures, gain Http::fake(), timeouts, retries, and better exceptions, and you stop depending on a package whose last release predates PHP 8.1.

Gotchas and Tips

  • Responses are plain text, not JSON. Sendy returns 1, true, false, Already subscribed., Invalid email address., Some fields are missing., etc. (bool) "false" is true in PHP — this is the single most common bug with wrappers like this. Always compare trimmed strings explicitly.
  • HTTP failures may not throw. Depending on the version, a 500 or a connection timeout can surface as a falsy or empty string rather than an exception. Wrap calls in try/catch and check the return value; log the raw body on anything unexpected.
  • Double opt-in changes the meaning of success. If the list is set to double opt-in, a 1 response means "confirmation email sent," not "subscribed." Make sure your UI copy matches the list setting.
  • API key scope. The key belongs to a brand. Using a key from brand A with a list ID from brand B returns an error that looks like an auth failure. Verify both together.
  • URL formatting. Pass the base installation URL without a trailing slash. The class appends /subscribe, /unsubscribe, etc. A trailing slash produces //subscribe, which some reverse proxies reject.
  • Timeouts. Set them. A self-hosted Sendy behind a misbehaving proxy can hang a worker indefinitely. If the package exposes no timeout option (likely), wrap the call in your own cURL layer or fork.
  • No retry, no rate limiting. Sendy has no official rate limit document, but hammering it during a bulk import will get you throttled or blocked by Cloudflare. Batch imports with Bus::batch() and a small delay between chunks.
  • Debugging recipe: reproduce the failing call with a raw curl -X POST against the same endpoint and payload. If curl works and the package doesn't, the difference is almost always a missing boolean=true parameter, a trailing slash, or a custom field name mismatch. Log the full outgoing payload before the call.
  • Version pinning. With no release since 2021, pin an exact version ("jacobbennett/sendyphp": "1.0.1") rather than a caret constraint, and add a test that asserts your wrapper's expected response mapping — that test is your real upgrade safety net if the package is ever abandoned or replaced.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route