PHPackages                             nextmigrant/laravel-plunk - 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. [Mail &amp; Notifications](/categories/mail)
4. /
5. nextmigrant/laravel-plunk

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

nextmigrant/laravel-plunk
=========================

Integration for Plunk (useplunk.com)

v1.0.2(3w ago)038↓100%MITPHPPHP ^8.3CI passing

Since May 14Pushed 3w agoCompare

[ Source](https://github.com/NextMigrant/laravel-plunk)[ Packagist](https://packagist.org/packages/nextmigrant/laravel-plunk)[ Docs](https://github.com/nextmigrant/laravel-plunk)[ GitHub Sponsors](https://github.com/NextMigrant)[ RSS](/packages/nextmigrant-laravel-plunk/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (12)Versions (4)Used By (0)

Laravel Plunk
=============

[](#laravel-plunk)

 [![Latest Version on Packagist](https://camo.githubusercontent.com/d50d7f97d7bcdc31ce42e9c8d8f257a9ad68374f0ae4798fa746c7d52119448a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6578746d696772616e742f6c61726176656c2d706c756e6b2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nextmigrant/laravel-plunk) [![Tests](https://camo.githubusercontent.com/013d6ea4a175562c2e06960e554f9ed2e6614a4675959df3943b4857d384f38c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6578746d696772616e742f6c61726176656c2d706c756e6b2f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/nextmigrant/laravel-plunk/actions?query=workflow%3Arun-tests+branch%3Amain) [![Code Style](https://camo.githubusercontent.com/4fd4f2844fd82ab050f7a5a42d076b73bff286d6984cad5c1c4fc5fb115f656c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6578746d696772616e742f6c61726176656c2d706c756e6b2f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/nextmigrant/laravel-plunk/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain) [![Total Downloads](https://camo.githubusercontent.com/cb73dc76ece368486a15a74a05727423cb6211fd5f47a97eb4a43c01151af17d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e6578746d696772616e742f6c61726176656c2d706c756e6b2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nextmigrant/laravel-plunk)

A clean, expressive Laravel package for the [Plunk](https://useplunk.com) email platform. Send transactional emails, manage contacts, track events, and verify email addresses — all through a simple Facade.

**Works with Plunk SaaS and self-hosted instances.**

---

Features
--------

[](#features)

- 📧 **Transactional Emails** — Send with templates, attachments, custom headers, and reply-to
- 👥 **Contact Management** — Full CRUD with bulk ops, CSV import, and cursor pagination
- 📝 **Template Management** — Full CRUD with duplicate and usage tracking
- 📣 **Campaign Management** — Create, send, schedule, cancel, and track stats
- 🎯 **Segment Management** — Dynamic/static segments with member management
- 📡 **Event Tracking** — Track events with automatic contact upsert and workflow triggers
- ✅ **Email Verification** — Validate format, MX records, disposable domains, and typos
- 🔑 **Dual Key Support** — Secret key for admin APIs, public key for event tracking
- 🛡️ **Typed Exceptions** — `AuthenticationException`, `ValidationException`, `RateLimitException`, `BillingException`, `ConflictException`
- ⚡ **Built on Laravel HTTP Client** — Retries, timeouts, and `Http::fake()` for testing

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

[](#requirements)

- PHP 8.3+
- Laravel 11, 12, or 13

Installation
------------

[](#installation)

```
composer require nextmigrant/laravel-plunk
```

Publish the config file:

```
php artisan vendor:publish --tag="plunk-config"
```

Add your configuration to `.env`:

```
PLUNK_SECRET_KEY=sk_your_secret_key
PLUNK_PUBLIC_KEY=pk_your_public_key           # Required for event tracking
PLUNK_BASE_API_URL=https://next-api.useplunk.com  # Override for self-hosted instances
```

Quick Start
-----------

[](#quick-start)

```
use NextMigrant\Plunk\Plunk;

// Send a transactional email
Plunk::transactional()->send(
    to: 'user@example.com',
    subject: 'Welcome aboard!',
    body: 'Welcome to our platform',
);

// Track an event
Plunk::events()->track(
    email: 'user@example.com',
    event: 'signed_up',
    data: ['plan' => 'pro'],
);

// Verify an email
$result = Plunk::verifyEmail('user@example.com');
// $result->valid, $result->isDisposable, $result->hasMxRecords, etc.
```

Usage
-----

[](#usage)

### Transactional Emails

[](#transactional-emails)

Send with inline content or a template. Requires either `template`, or both `subject` and `body`:

```
// Inline content
Plunk::transactional()->send(
    to: 'user@example.com',                        // string, {name, email}, or array
    subject: 'Your Invoice',
    body: 'Invoice #1234',
    from: ['name' => 'Acme', 'email' => 'billing@acme.com'],  // verified domain
    reply: 'support@acme.com',
    subscribed: true,
    data: ['invoice_id' => '1234'],                // contact data + template vars
    headers: ['X-Priority' => '1'],
    attachments: [
        [
            'filename' => 'invoice.pdf',
            'content' => base64_encode($pdfContent),
            'contentType' => 'application/pdf',
        ],
    ],
);

// Using a template (subject/body come from the template)
Plunk::transactional()->send(
    to: 'user@example.com',
    template: 'tpl_welcome',
    data: ['firstName' => 'John', 'plan' => 'pro'],
);
```

### Event Tracking

[](#event-tracking)

Track events to trigger Plunk workflows. Contacts are created automatically if they don't exist:

```
Plunk::events()->track(
    email: 'user@example.com',
    event: 'plan_upgraded',
    data: ['plan' => 'enterprise', 'seats' => 50],
    subscribed: false,  // Subscription state for auto-created contacts
);
```

> **Note:** The `/v1/track` endpoint requires a public key (`pk_*`). Secret keys are not accepted for this endpoint. Set `PLUNK_PUBLIC_KEY` in your `.env`.

### Contact Management

[](#contact-management)

#### Basic CRUD

[](#basic-crud)

```
// List contacts (cursor-based pagination)
$result = Plunk::contacts()->list(
    search: 'john',    // Filter by email substring
    limit: 50,         // Items per page (max 100)
    cursor: $cursor,   // Cursor from previous response
);

foreach ($result['data'] as $contact) {
    echo $contact->email;       // Contact DTO
    echo $contact->subscribed;
}
// $result['cursor'], $result['hasMore'], $result['total']

// Get a single contact
$contact = Plunk::contacts()->get('contact_id');

// Create or upsert a contact
$result = Plunk::contacts()->create('new@example.com',
    subscribed: true,
    data: ['source' => 'api', 'plan' => 'free'],
);
// $result['_meta']['isNew'], $result['_meta']['isUpdate']

// Update a contact (PATCH)
$result = Plunk::contacts()->update('contact_id',
    subscribed: false,
    data: ['plan' => 'pro'],
);

// Delete a contact
Plunk::contacts()->delete('contact_id');

// Bulk email-existence check (max 500 emails)
$result = Plunk::contacts()->lookup(['a@example.com', 'b@example.com']);
```

#### Bulk Operations

[](#bulk-operations)

All bulk operations are async and return a `jobId` for status polling:

```
// Subscribe/unsubscribe/delete (up to 1,000 IDs)
$result = Plunk::contacts()->bulkSubscribe(['id_1', 'id_2', 'id_3']);
$result = Plunk::contacts()->bulkUnsubscribe(['id_1', 'id_2']);
$result = Plunk::contacts()->bulkDelete(['id_1']);

// Poll job status
$status = Plunk::contacts()->bulkStatus($result['jobId']);

// Import from CSV (max 5MB, queued)
$result = Plunk::contacts()->import('/path/to/contacts.csv');
$status = Plunk::contacts()->importStatus($result['jobId']);
```

### Templates

[](#templates)

```
// List templates (with pagination and filtering)
$result = Plunk::templates()->list(
    search: 'welcome',
    type: 'TRANSACTIONAL',  // or 'MARKETING'
    limit: 50,
);

// Get a single template
$template = Plunk::templates()->get('template_id');

// Create a template
$template = Plunk::templates()->create(
    name: 'Welcome Email',
    subject: 'Welcome to {{company}}!',
    body: 'Hello {{firstName}}',
    type: 'TRANSACTIONAL',  // or 'MARKETING'
);

// Update a template (PATCH)
$template = Plunk::templates()->update('template_id',
    subject: 'Updated Subject',
);

// Duplicate a template
$copy = Plunk::templates()->duplicate('template_id');

// Check what uses a template
$usage = Plunk::templates()->usage('template_id');

// Delete a template
Plunk::templates()->delete('template_id');
```

### Campaigns

[](#campaigns)

```
// List all campaigns
$campaigns = Plunk::campaigns()->list();

// Create a campaign (starts in DRAFT)
$result = Plunk::campaigns()->create(
    name: 'Product Launch',
    subject: 'Exciting news!',
    body: 'We launched!',
    from: 'hello@acme.com',
    audienceType: 'ALL',          // 'ALL', 'SEGMENT', or 'FILTERED'
    segmentId: 'seg_123',         // required if SEGMENT
    audienceFilter: [...],        // required if FILTERED
);

// Send immediately
Plunk::campaigns()->send('campaign_id');

// Schedule for later
Plunk::campaigns()->send('campaign_id', scheduledFor: '2026-06-01T10:00:00Z');

// Cancel a scheduled/sending campaign
Plunk::campaigns()->cancel('campaign_id');

// Send a test email
Plunk::campaigns()->test('campaign_id', 'tester@example.com');

// Get campaign stats
$stats = Plunk::campaigns()->stats('campaign_id');
// $stats['sent'], $stats['opened'], $stats['clicked'], $stats['bounced']

// Duplicate / Update / Delete
$copy = Plunk::campaigns()->duplicate('campaign_id');
Plunk::campaigns()->update('campaign_id', [...]);
Plunk::campaigns()->delete('campaign_id');
```

### Segments

[](#segments)

```
// List all segments
$segments = Plunk::segments()->list();

// Create a segment
$result = Plunk::segments()->create(
    name: 'Pro Users',
    filters: ['data.plan' => 'pro'],
    trackMembership: true,
);

// Get segment members (page-based pagination)
$result = Plunk::segments()->contacts('segment_id', page: 1, pageSize: 100);

// Add/remove members (static segments)
Plunk::segments()->addMembers('segment_id',
    emails: ['a@example.com', 'b@example.com'],
    createMissing: true,
);
Plunk::segments()->removeMembers('segment_id', ['a@example.com']);

// Recompute membership (fires entry/exit events)
Plunk::segments()->compute('segment_id');

// Cheap count refresh (no events)
Plunk::segments()->refresh('segment_id');

// Update / Delete
Plunk::segments()->update('segment_id', ['name' => 'Updated Name']);
Plunk::segments()->delete('segment_id');
```

### Email Verification

[](#email-verification)

```
$verification = Plunk::verifyEmail('user@example.com');

$verification->valid;            // bool — overall result
$verification->email;            // string — the email checked
$verification->isDisposable;     // bool — is a disposable domain
$verification->isAlias;          // bool — is an alias address
$verification->isTypo;           // bool — likely contains a typo
$verification->suggestedEmail;   // string|null — correction if isTypo is true
$verification->isPlusAddressed;  // bool — uses + addressing
$verification->isPersonalEmail;  // bool — personal vs business
$verification->domainExists;     // bool — domain resolves
$verification->hasWebsite;       // bool — domain has a website
$verification->hasMxRecords;     // bool — MX records exist
$verification->reasons;          // array — human-readable explanations
```

Configuration
-------------

[](#configuration)

The published config file (`config/plunk.php`):

```
return [
    'secret_key' => env('PLUNK_SECRET_KEY'),
    'public_key' => env('PLUNK_PUBLIC_KEY'),
    'base_api_url'   => env('PLUNK_BASE_API_URL', 'https://next-api.useplunk.com'),
    'timeout'    => env('PLUNK_TIMEOUT', 30),
    'retry'      => [
        'times' => 3,
        'sleep' => 100, // milliseconds
    ],
];
```

Error Handling
--------------

[](#error-handling)

The package throws typed exceptions mapped from HTTP status codes. All exceptions expose `errorCode`, `requestId`, and `suggestion` from the Plunk error response:

```
use NextMigrant\Plunk\Exceptions\AuthenticationException; // 401, 403
use NextMigrant\Plunk\Exceptions\BillingException;        // 402
use NextMigrant\Plunk\Exceptions\ConflictException;       // 409
use NextMigrant\Plunk\Exceptions\ValidationException;     // 422
use NextMigrant\Plunk\Exceptions\RateLimitException;      // 429
use NextMigrant\Plunk\Exceptions\PlunkException;          // All others

try {
    Plunk::transactional()->send(to: $email, subject: 'Hi', body: 'Hello');
} catch (AuthenticationException $e) {
    // 401/403 — Invalid or missing API key
} catch (BillingException $e) {
    // 402 — Billing limit exceeded or upgrade required
} catch (ConflictException $e) {
    // 409 — Resource conflict (e.g., duplicate email)
} catch (ValidationException $e) {
    // 422 — Invalid request payload
    $e->response->json()['error']['errors']; // Field-level validation errors
} catch (RateLimitException $e) {
    // 429 — Exceeded 1,000 requests/minute
} catch (PlunkException $e) {
    // Any other API error
    $e->errorCode;   // e.g., 'INTERNAL_SERVER_ERROR'
    $e->requestId;   // For debugging with Plunk support
    $e->suggestion;  // Helpful fix guidance
    $e->response;    // Underlying HTTP response
}
```

Testing
-------

[](#testing)

The package uses Laravel's HTTP client under the hood, so you can use `Http::fake()` in your application tests:

```
use Illuminate\Support\Facades\Http;
use NextMigrant\Plunk\Plunk;

Http::fake([
    '*/v1/send' => Http::response(['success' => true]),
]);

Plunk::transactional()->send(
    to: 'user@example.com',
    subject: 'Test',
    body: 'Hello',
);

Http::assertSent(fn ($request) =>
    str_contains($request->url(), '/v1/send')
    && $request['to'] === 'user@example.com'
);
```

Run the package test suite:

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [NextMigrant](https://github.com/nextmigrant)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance94

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

3

Last Release

26d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/141585205?v=4)[NextMigrant Inc.](/maintainers/nextmigrant)[@NextMigrant](https://github.com/NextMigrant)

---

Top Contributors

[![aditya2r](https://avatars.githubusercontent.com/u/31886760?v=4)](https://github.com/aditya2r "aditya2r (11 commits)")

---

Tags

api-clientemaillaravellaravel-packagemarketing-automationphpplunktransactional-emaillaravelnextmigrantlaravel-plunk

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/nextmigrant-laravel-plunk/health.svg)

```
[![Health](https://phpackages.com/badges/nextmigrant-laravel-plunk/health.svg)](https://phpackages.com/packages/nextmigrant-laravel-plunk)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.3M41](/packages/spatie-laravel-pdf)[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24655.3k](/packages/vormkracht10-laravel-mails)[xammie/mailbook

Laravel Mail Explorer

482519.8k1](/packages/xammie-mailbook)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3913.7k](/packages/rawilk-profile-filament-plugin)[backstage/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24675.5k8](/packages/backstage-laravel-mails)

PHPackages © 2026

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