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

Filament Activecampaign Laravel Package

xavicabot/filament-activecampaign

Connect ActiveCampaign to your Filament admin panel. Manage contacts, lists, tags, and automations directly from Laravel—no separate dashboard required. Simple setup, native Filament UI, and a smooth workflow for your team.

View on GitHub
Deep Wiki
Context7

Getting Started

Match the branch to your Filament version before anything else. This package ships two maintenance lines and they are not interchangeable:

Your Filament Package line Latest
^3.0 1.x v1.4.0 (this release)
^4.0 | ^5.0 2.x (main) v2.1.0
composer require xavicabot/filament-activecampaign:^1.4

Then publish and configure:

php artisan vendor:publish --tag="activecampaign-config"
ACTIVECAMPAIGN_API_URL=https://youraccount.api-us1.com
ACTIVECAMPAIGN_API_KEY=your-key

Register the plugin on your panel (only if you want the Filament-side UI/settings it exposes):

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(ActiveCampaignPlugin::make());
}

Where to look first: the published config/activecampaign.php. Everything you will touch day-to-day — API credentials, the async flag, async_tries, async_backoff, and any field mappings — lives there. Read it before writing code; most "why isn't this firing" questions are answered in that file.

First use case: trigger an automation when a user signs up. The most direct path is the service/facade:

use Xavicabot\FilamentActiveCampaign\Facades\ActiveCampaign;

ActiveCampaign::trigger(
    user: $user,
    automationId: config('activecampaign.automations.welcome'),
);

Or with an email attached, which is the common case for contact-form style flows:

ActiveCampaign::triggerWithEmail($user, $automationId);

Verify it worked by checking the automation's contact activity in ActiveCampaign, not just your logs — a 200 from /contact/sync does not guarantee an automation enrolled the contact.

Implementation Patterns

Trigger from Filament lifecycle hooks

The natural integration point is right after a record is persisted. Prefer model events or Filament's afterCreate, not the form's save() return value:

TextInput::make('email')->required(),

// in the resource's CreateRecord / EditRecord
protected function afterCreate(): void
{
    ActiveCampaign::triggerWithEmail($this->record, $this->automationId);
}

For anything broader than a single resource, use an observer so the automation fires regardless of whether the change came from Filament, an artisan command, or an importer.

Make async the default for user-facing writes

Since v1.4.0, trigger() and triggerWithEmail() honour the activecampaign.async config flag. Set it to true in production and every call is pushed to the queue — you get the same public API with no code changes. Use the explicit triggerAsync / triggerWithEmailAsync methods when you want a specific call queued regardless of the global flag (e.g. a bulk import that must never block the request).

// config/activecampaign.php
'async' => env('ACTIVECAMPAIGN_ASYNC', true),
'async_tries' => 3,
'async_backoff' => [10, 60, 300],

Backoff as an array gives you exponential-ish retries for the transient 429/5xx cases ActiveCampaign is prone to. Keep async_tries low — a retried job re-runs the whole automation trigger, so a long retry chain can mean duplicate enrolments if the failure happened after the API accepted the write.

Rely on the batched field sync

You no longer need to sequence field writes yourself. A single automation execution now issues one POST /contact/sync carrying every custom field (fieldValues[]) and every system field (firstName, phone, …). The release notes' example — 4 custom fields + 3 system fields — drops from ~5 requests to 2.

Day-to-day this means: do not hand-roll loops of updateContact calls around a trigger. Just map your fields in config and let the runner batch them. It also means one failing field fails the whole sync, so validate your mappings once rather than assuming partial success.

Tag management as a first-class workflow

v1.4.0 fills the gap that used to force people into raw HTTP calls. The service layer is what you want in application code:

// Read
$contact = ActiveCampaign::getContactByEmail($user->email);   // ?array
$tags    = ActiveCampaign::getContactTags($contact['id']);     // array

// Write — signature widened, string or array both work
ActiveCampaign::addTagToContact($contact['id'], ['trial', 'newsletter']);
ActiveCampaign::removeTagFromContact($contact['id'], 'trial');

removeTagFromContact() is the ergonomic one: it takes a tag name, resolves the association for you, and calls the underlying detachContactTag($associationId). The existing addTagToContact($contactId, string $tagName) calls still work unchanged.

Drop to ActiveCampaignClient only when you need the raw shape:

$client = app(ActiveCampaignClient::class);
$client->listContactTags($contactId);   // GET /contacts/{id}/contactTags
$client->detachContactTag($assocId);    // DELETE /contactTags/{id}

A good pattern is tag-as-state-machine: trial → onboarding → customer, driven from your own model events, with removeTagFromContact in the transition. Keeps segmentation in ActiveCampaign while the source of truth stays in your app.

Idempotency wrapper

Because triggers are cheap now, wrap them in a small helper so a given automation only fires once per meaningful event:

if (! $user->hasBeenTriggered('welcome')) {
    ActiveCampaign::triggerWithEmailAsync($user, $automationId);
    $user->markTriggered('welcome');
}

This is the single most effective guard against the duplicate-enrolment problem, more so than queue configuration.

Gotchas and Tips

  • Wrong branch is the #1 failure mode. Installing v2.x on Filament 3 (or v1.4 on Filament 4/5) will break in ways that look like the plugin is broken. Pin explicitly: ^1.4 for Filament 3, ^2.1 otherwise. This release is only the v3 maintenance line — nothing here is a feature you get on 2.x that you didn't already have.
  • The one observable behaviour change: custom fields whose template references {user.*} / {ctx.*} and resolves to an empty string are now skipped instead of being sent. This mirrors existing system-field behaviour and prevents silently blanking values in ActiveCampaign — but it is a real change. If you relied on a null-then-blank reset, you must now clear the field explicitly (a direct client call), not via a template that happens to resolve empty.
  • Async jobs serialise primitives only. TriggerActiveCampaignAutomationJob stores primitive data and re-resolves the user from the database inside handle(). Consequences:
    • Pass the user by identifier; unsaved model state (e.g. a field you set in memory but never persisted) will not be visible to the job.
    • If the user is deleted between dispatch and execution, the job exits gracefully rather than throwing. That's good for queue health, but it means "no error" is not "it ran" — log the skip if the automation matters.
    • A queue worker must actually be running. With async => true and no worker, everything silently queues forever. This is the most common "the plugin does nothing" report.
  • Removing a tag needs the contact ID, not the email. The service API takes $contactId. If all you have is an email, getContactByEmail() first — and handle null, since a contact that has never synced will not exist. Same for getContactTags(), which returns [] for an unknown contact rather than throwing.
  • Empty-string collapse also applies to system fields. When debugging "why didn't the phone number update", check the template resolution before blaming the API.
  • Debug at the HTTP layer. Log the outgoing /contact/sync payload (with the API key redacted) to see the batched fieldValues[] array. Since the N+1 calls are gone, one log line now shows you every field in the execution — much easier to reason about than the old sequence.
  • Watch your retry budget against automation semantics. async_tries/async_backoff control the queue, not ActiveCampaign's idempotency. A job that partially succeeded before failing will re-trigger on retry.
  • Extension points: bind your own implementation of the client/service in a service provider if you need to intercept calls (e.g. to add metrics or a circuit breaker) rather than editing vendor code; the facade resolves from the container. Config-driven field mappings mean most customisation needs no code at all.
  • Test coverage is solid but scoped: 75 tests / 191 assertions green on Filament 3. There is no equivalent guarantee for your panel's customisations — run your own smoke test against the ActiveCampaign sandbox after upgrading, particularly around the empty-string skip.

Upgrade verdict: no breaking changes vs v1.3.0, safe drop-in. Update, then immediately audit any template-based field mappings that can resolve to empty, and confirm a queue worker is running before flipping activecampaign.async to true.

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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk