PHPackages                             artisanpack-ui/convertkit - 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. artisanpack-ui/convertkit

ActiveLibrary

artisanpack-ui/convertkit
=========================

ConvertKit (Kit) integration for ArtisanPack UI — API client, forms, tags, sequences, and broadcasts for Laravel.

1.1.0(1mo ago)00MITPHPPHP ^8.2CI passing

Since Jul 13Pushed 1w agoCompare

[ Source](https://github.com/ArtisanPack-UI/convertkit)[ Packagist](https://packagist.org/packages/artisanpack-ui/convertkit)[ RSS](/packages/artisanpack-ui-convertkit/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (11)Versions (4)Used By (0)

ArtisanPack UI ConvertKit
=========================

[](#artisanpack-ui-convertkit)

ConvertKit (Kit) integration for Laravel. Ships a Kit v4 API client, a feed-driven `artisanpack-ui/forms` bridge, public REST endpoints for subscribe forms in any front-end, Artisan commands for reference-data sync and feed management, and a `FakeConvertKit` test double for consumer apps.

- Laravel 10, 11, 12, and 13
- PHP 8.2+
- Kit v4 API

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

[](#installation)

```
composer require artisanpack-ui/convertkit
```

Publish the config and run migrations:

```
php artisan vendor:publish --tag=convertkit-config
php artisan vendor:publish --tag=convertkit-migrations
php artisan migrate
```

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

[](#configuration)

Set your Kit v4 API key in `.env`:

```
CONVERTKIT_API_KEY=your-kit-v4-api-key
```

Generate a key in your Kit account under **Advanced → API**. Verify the key can reach Kit:

```
php artisan convertkit:test
```

Full config options live in `config/convertkit.php` after publishing — retries, cache TTLs, forms-integration toggles, rate-limit windows, etc.

Basic Usage
-----------

[](#basic-usage)

### Subscribers

[](#subscribers)

```
use ArtisanPackUI\ConvertKit\Facades\ConvertKit;

// Create a subscriber
$subscriber = ConvertKit::subscribers()->create(
    email: 'jane@example.com',
    firstName: 'Jane',
    fields: [ 'company' => 'Acme' ],
);

// Find by email
$existing = ConvertKit::subscribers()->findByEmail( 'jane@example.com' );

// Apply / remove a tag
ConvertKit::subscribers()->tag( $subscriber->id, 12345 );
ConvertKit::subscribers()->untag( $subscriber->id, 12345 );

// Unsubscribe
ConvertKit::subscribers()->unsubscribe( $subscriber->id );
```

The `convertkit()` helper is equivalent to the facade:

```
convertkit()->subscribers()->create( 'jane@example.com' );
```

### Forms, Tags, Custom Fields

[](#forms-tags-custom-fields)

```
$forms  = ConvertKit::forms()->list();          // cached; use ->refresh() to force
$tags   = ConvertKit::tags()->list();
$fields = ConvertKit::customFields()->list();

// Subscribe to a specific Kit form (applies tags server-side)
ConvertKit::forms()->subscribe(
    formId: 12345,
    email: 'jane@example.com',
    fields: [ 'company' => 'Acme' ],
    tags: [ 10, 20 ],
);
```

Refresh the cached reference data:

```
php artisan convertkit:sync            # all
php artisan convertkit:sync forms
php artisan convertkit:sync tags
php artisan convertkit:sync fields
```

### Account stats

[](#account-stats)

Read the account's growth aggregate — the subscriber count plus the new, cancelled, and net movement over a window — as an immutable `GrowthStats` DTO. Both bounds are optional; omit them and Kit reports the last 90 days. Dates use `yyyy-mm-dd` and are interpreted in the account's sending time zone.

```
$stats = ConvertKit::account()->stats();                          // last 90 days; cached
$stats = ConvertKit::account()->stats( '2026-01-01', '2026-03-31' );

$stats->subscribers;       // 12_840  (count at the end of the window)
$stats->newSubscribers;    // 512
$stats->cancellations;     // 78
$stats->netNewSubscribers; // 434

ConvertKit::account()->refresh(); // force a re-fetch, bypassing the cache
```

Kit has no native time-series endpoint, so `growthSeries()` composes one from per-bucket reads. The interval is `day`, `week`, or `month`, and the range is capped at `AccountEndpoint::MAX_BUCKETS` (366) buckets to bound the fan-out.

```
$series = ConvertKit::account()->growthSeries( '2026-01-01', '2026-03-31', 'week' );

foreach ( $series as $point ) {
    $point->starting;    // '2026-01-01'
    $point->subscribers; // 12_010
}

ConvertKit::account()->refreshSeries( '2026-01-01', '2026-03-31', 'week' ); // re-fetch the series
```

The cache TTL defaults to 15 minutes (growth data moves faster than reference data); override it with `CONVERTKIT_STATS_TTL`.

### Broadcasts

[](#broadcasts)

Read-only access to recent broadcasts, each paired with its delivery and engagement stats (recipients, open/click rates, unsubscribes) — everything a recent-broadcasts dashboard widget needs, in a single cached call. Kit returns broadcasts newest-first, so `list( $limit )` gives the `$limit` most recent.

```
$broadcasts = ConvertKit::broadcasts()->list();     // 10 most recent; cached
$broadcasts = ConvertKit::broadcasts()->list( 25 ); // up to BroadcastsEndpoint::MAX_LIMIT (100)

foreach ( $broadcasts as $broadcast ) {
    $broadcast->subject;             // "This week in ..."
    $broadcast->stats->recipients;   // 1_284
    $broadcast->stats->openRate;     // 0.42  (a fraction, not a percentage)
    $broadcast->stats->clickRate;    // 0.08
}

ConvertKit::broadcasts()->refresh(); // force a re-fetch, bypassing the cache
```

The cache TTL follows the reference-data endpoints (1 hour); override it with `CONVERTKIT_BROADCASTS_TTL`.

Forms Integration
-----------------

[](#forms-integration)

Pairs with [`artisanpack-ui/forms`](https://gitlab.com/jacob-martella-web-design/artisanpack-ui/forms). Flip on the integration and every submission of a form the feed matches is evaluated, mapped to a Kit payload, and dispatched to a queue.

```
CONVERTKIT_FORMS_INTEGRATION=true
CONVERTKIT_QUEUE_CONNECTION=redis
CONVERTKIT_QUEUE=convertkit
```

### Creating a feed

[](#creating-a-feed)

Use the REST endpoints (see below) or the CLI wizard:

```
php artisan convertkit:feeds create
```

A feed has:

- `form_id` — the `artisanpack-ui/forms` form to listen on
- `kit_form_id` — the Kit form to subscribe to (optional; leave null for a raw `subscribers()->create()` subscribe)
- `kit_tag_ids` — Kit tag ids to apply on subscribe
- `field_map` — Kit destination → submission field slug
- `conditional_logic` — optional rule set that must pass before the feed fires

### Field mapping

[](#field-mapping)

Keys are Kit destinations, values are your submission's field slugs:

```
{
    "email_address": "email",
    "first_name": "name",
    "company": "company_name"
}
```

`email_address` and `first_name` land at the top of the Kit payload; anything else (like `company`) becomes a Kit custom-field entry.

### Conditional logic

[](#conditional-logic)

```
{
    "match": "all",
    "conditions": [
        { "field": "plan", "operator": "equals", "value": "pro" },
        { "field": "email", "operator": "contains", "value": "@" }
    ]
}
```

Supported operators: `equals`, `not_equals`, `contains`, `not_contains`, `is_empty`, `is_not_empty`. `match` is `all` (AND) or `any` (OR).

REST API
--------

[](#rest-api)

All feed-admin routes sit under `admin/convertkit` by default and are guarded by the `manage-convertkit-feeds` Gate ability. Define your own gate closure:

```
use ArtisanPackUI\ConvertKit\Models\KitFeed;

Gate::define(
    'manage-convertkit-feeds',
    fn ( User $user, ?KitFeed $feed = null ): bool => $user->isAdmin(),
);
```

### Feeds

[](#feeds)

MethodPathPurpose`GET``/admin/convertkit/feeds`List feeds. Filter with `?form_id=`.`POST``/admin/convertkit/feeds`Create a feed.`GET``/admin/convertkit/feeds/{id}`Show a feed.`PUT``/admin/convertkit/feeds/{id}`Update a feed.`DELETE``/admin/convertkit/feeds/{id}`Delete a feed.`POST``/admin/convertkit/feeds/{id}/test`Dry-run a feed against a sample submission.Example — dry-run:

```
curl -X POST https://example.test/admin/convertkit/feeds/1/test \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -d '{"values": {"email": "jane@example.com", "plan": "pro"}}'
```

Response:

```
{
    "would_send": true,
    "reason": null,
    "payload": {
        "email_address": "jane@example.com",
        "fields": { "plan": "pro" }
    }
}
```

When conditional logic blocks the feed, `would_send` is `false` and `reason` is `"conditional_logic"`. When the field map can't resolve an email, `reason` starts with `field_map:`.

### Public subscribe endpoint

[](#public-subscribe-endpoint)

The public endpoint is what front-end forms POST to. Actual Kit calls run on the queue, so it always returns `202 Accepted`.

`POST /convertkit/subscribers`

Either pass a `feed_id` (uses that feed's Kit form + tags) or a bare `kit_form_id`:

```
{
    "feed_id": 1,
    "email": "jane@example.com",
    "first_name": "Jane",
    "tags": [ 100 ]
}
```

```
{
    "kit_form_id": 12345,
    "email": "jane@example.com",
    "fields": { "company": "Acme" }
}
```

Rate limited to 10 attempts per IP per minute by default. Tune via `CONVERTKIT_SUBSCRIBE_MAX_ATTEMPTS` and `CONVERTKIT_SUBSCRIBE_DECAY_MINUTES`.

Recipes
-------

[](#recipes)

### Livewire subscribe form

[](#livewire-subscribe-form)

```
use Livewire\Attributes\Validate;
use Livewire\Component;

class SubscribeForm extends Component
{
    #[Validate( 'required|email' )]
    public string $email = '';

    public bool $done = false;

    public function submit(): void
    {
        $this->validate();

        convertkit()->forms()->subscribe(
            formId: 12345,
            email: $this->email,
            tags: [ 100 ],
        );

        $this->done = true;
    }

    public function render(): string
    {
        return Thanks — check your inbox.

        Subscribe

```

Artisan Commands
----------------

[](#artisan-commands)

CommandPurpose`convertkit:test`Verify the configured API key can reach Kit.`convertkit:sync [resource]`Refresh cached forms/tags/fields.`convertkit:feeds list [--form=]`Table of feeds, optionally filtered by form.`convertkit:feeds create`Interactive wizard for creating a feed.`convertkit:feeds delete {id}`Delete a feed (with confirmation).Testing
-------

[](#testing)

Swap the real ConvertKit binding for a recording fake:

```
use ArtisanPackUI\ConvertKit\Facades\ConvertKit;

it( 'subscribes users to the newsletter', function (): void {
    $fake = ConvertKit::fake();

    $this->post( '/signup', [ 'email' => 'jane@example.com' ] )
        ->assertRedirect();

    $fake->assertSubscribed( 'jane@example.com' );
    $fake->assertTagged( 'jane@example.com', 100 );
    $fake->assertSentCount( 1 );
} );
```

Available assertions:

- `assertSubscribed( string $email, ?int $formId = null )`
- `assertTagged( string $email, int $tagId )`
- `assertNothingSent()`
- `assertSentCount( int $count )`

The fake never touches the network, so tests stay hermetic.

Hooks
-----

[](#hooks)

The `SubscribeToKit` job fires three ArtisanPack UI hooks around every subscribe attempt — direct-form and raw-subscribers paths both flow through them. Register listeners with `addAction()` (from `artisanpack-ui/hooks`).

HookWhenSignature`ap.convertkit.subscribing`Immediately before the Kit API call.`(string $email, array $attributes)``ap.convertkit.subscribed`After a successful subscribe.`(string $email, array $response)``ap.convertkit.subscribeFailed`On any subscribe exception.`(string $email, Throwable $exception)``$attributes` is the payload map about to be sent to Kit: `first_name`, `fields`, `tag_ids` (already capped at `SubscribeToKit::MAX_TAGS_PER_JOB = 50`), and `kit_form_id`.

`$response` is the Kit subscriber decoded to an array: `id`, `email`, `state`, `first_name`, `created_at`, `fields`.

`subscribeFailed` fires per `handle()` invocation, so a retryable transient failure (rate limit, 5xx) that the queue will retry emits one hook per attempt. Inspect the exception type to distinguish transient from terminal.

```
addAction( 'ap.convertkit.subscribed', function ( string $email, array $response ): void {
    Log::info( 'Kit subscribed', [ 'email' => $email, 'subscriber_id' => $response['id'] ] );
} );
```

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

[](#contributing)

As an open source project, this package is open to contributions from anyone. Please [read through the contributing guidelines](CONTRIBUTING.md)to learn more about how you can contribute to this project.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance96

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

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 ~8 days

Total

2

Last Release

40d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ba2a2c40c9a93470595cd10701d2291434f3a7db61862d9700a9e69e31608c6c?d=identicon)[JacobMartellaWebDesign](/maintainers/JacobMartellaWebDesign)

---

Top Contributors

[![ViewFromTheBox](https://avatars.githubusercontent.com/u/8247489?v=4)](https://github.com/ViewFromTheBox "ViewFromTheBox (32 commits)")

---

Tags

laravelemail marketingnewsletterkitConvertKitartisanpack-ui

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/artisanpack-ui-convertkit/health.svg)

```
[![Health](https://phpackages.com/badges/artisanpack-ui-convertkit/health.svg)](https://phpackages.com/packages/artisanpack-ui-convertkit)
```

###  Alternatives

[laravel/ai

The official AI SDK for Laravel.

1.1k6.4M360](/packages/laravel-ai)[psalm/plugin-laravel

Psalm plugin for Laravel

3365.5M359](/packages/psalm-plugin-laravel)[erag/laravel-lang-sync-inertia

A powerful Laravel package for syncing and managing language translations across backend and Inertia.js (Vue/React/Svelte) frontends, offering effortless localization, auto-sync features, and smooth multi-language support for modern Laravel applications.

6036.2k](/packages/erag-laravel-lang-sync-inertia)[ublabs/blade-simple-icons

A package to easily make use of Simple Icons in your Laravel Blade views.

1868.9k](/packages/ublabs-blade-simple-icons)

PHPackages © 2026

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