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

Laravel Activecampaign Laravel Package

jeffersongoncalves/laravel-activecampaign

Laravel ActiveCampaign is a Laravel integration for the ActiveCampaign API, providing a configured client to manage contacts, lists, tags, and automations. It simplifies setup with Laravel-friendly configuration and facades for building email marketing features.

View on GitHub
Deep Wiki
Context7

Getting Started

laravel-activecampaign is a thin Laravel wrapper around the official ActiveCampaign PHP SDK: it registers a singleton client in the container, wires credentials from config/env, and (in most builds of this pattern) exposes a facade for terse calls.

Minimal setup:

composer require jeffersongoncalves/laravel-activecampaign
php artisan vendor:publish --tag=activecampaign-config

Then set credentials in .env:

ACTIVECAMPAIGN_URL=https://youraccount.api-us1.com
ACTIVECAMPAIGN_API_KEY=your-api-key

The URL is not https://api.activecampaign.com — it is the account-specific API URL from Settings → Developer in ActiveCampaign. Copy it verbatim or every request 403s.

Where to look first, in order:

  1. config/activecampaign.php — the only file you actually need to understand; it defines the URL/key keys and any default timeout or retry settings.
  2. The package's service provider — confirms the binding name (e.g. activecampaign) and whether it is a singleton or a factory. This matters for long-running workers.
  3. The facade class — lists the available entry points. In a wrapper like this it is usually just __callStatic proxying to the SDK, so the facade docblock is thin; the real API surface is the SDK's own classes.

First use case: tag a user on registration. In an event listener, resolve the client and call the contact sync endpoint, then add a tag. Get that working synchronously first, then move it behind a queue (see below) before it ever touches production traffic.

Because this is the 1.0.0 release, treat it as the first stable API — pin it ("^1.0") rather than dev-main, and skim the changelog link in the release notes for the pre-1.0 iterations, which is where the naming churn will have happened.

Implementation Patterns

Prefer constructor injection over the facade. The facade is convenient in tinker and one-off commands, but injected dependencies make your sync logic testable and make the coupling explicit:

use Illuminate\Contracts\Container\Container;

public function __construct(private Container $app) {}

public function sync(User $user)
{
    $client = $this->app->make('activecampaign');
    // ...
}

In practice, wrap the client in your own ActiveCampaignService so your application code never depends on SDK object shapes. The SDK's fluent response objects ($response->contact->id) change between major SDK versions; your wrapper absorbs that.

Always queue the outbound call. ActiveCampaign calls are network-bound and rate-limited. The canonical pattern is an event + queued listener:

// app/Listeners/SyncContactToActiveCampaign.php
class SyncContactToActiveCampaign implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 60, 300];

    public function handle(ActiveCampaignService $ac): void
    {
        $contactId = $ac->syncContact($this->user);
        $ac->addTag($contactId, config('marketing.tags.registered'));
    }
}

Note that ShouldQueue listeners get a fresh container per job, so a singleton client is fine — but if you use queue:work with a long --timeout, be aware the underlying HTTP client may hold a stale connection pool.

Map Eloquent models to contacts in one place. Contact "sync" is upsert-by-email. Keep a single method that builds the payload from your User so field names and custom field IDs do not drift between registration, profile update, and nightly backfill:

public function contactPayload(User $user): array
{
    return [
        'email' => $user->email,
        'firstName' => $user->first_name,
        'lastName' => $user->last_name,
        'fieldValues' => [
            ['field' => config('activecampaign.fields.plan'), 'value' => $user->plan],
        ],
    ];
}

Store the remote contact ID. After a successful sync, persist activecampaign_contact_id on the user. Subsequent tag/list operations take the ID, not the email, and you avoid a lookup round-trip (and a rate-limit slot) on every operation.

Use custom field IDs from config, never literals. Custom field IDs are per-account. Hardcoding 'field' => '12' works on your dev account and silently writes to the wrong field in production. Resolve them once via the fields endpoint, put them in config/activecampaign.php, and commit that.

Testing. Do not hit the real API in tests. Because the SDK ultimately uses Guzzle/Laravel's HTTP client, Http::fake() on the ActiveCampaign host is the cheapest route:

Http::fake([
    '*.api-us1.com/api/3/contact/sync' => Http::response(['contact' => ['id' => '42']]),
]);

For unit tests of your wrapper, bind a mock into the container rather than faking HTTP — that keeps the assertion at the behaviour level (addTag was called with these args) instead of the wire format.

Gotchas and Tips

  • The URL trips up almost everyone. It must be your account subdomain (https://mycompany.api-us1.com), not the generic API host. A 403 with a valid key is nearly always this.
  • Rate limits are per-account and shared across every integration. ActiveCampaign allows roughly 5 requests/second; bursty backfills will 429. Chunk your sync jobs, add a small sleep or a rate limiter (RateLimiter::for('activecampaign')), and let your queue backoff handle the rest.
  • contact/sync vs contact/add. Sync upserts by email and is the safe default; add will fail on duplicates. Deletions are contact/delete and are permanent at the API level — soft-delete locally and reconcile before you call it.
  • Pagination is offset-based with a hard cap (100 per page in the v3 API). Loops over large lists need while (count($batch) === 100) with an incrementing offset, plus a guard against the total shifting mid-iteration.
  • Config caching. php artisan config:cache will bake in whatever is in config/activecampaign.php at cache time. If you resolve custom field IDs at runtime and write them back to config, that write is lost on the next deploy — persist to the database instead.
  • Timezones and date fields. ActiveCampaign stores datetimes in UTC; fieldValues date fields sent with a local-time string will look shifted. Use ->utc()->toIso8601String().
  • The singleton is a footgun in Octane. If you run Laravel Octane, a singleton client constructed with a per-request key (multi-tenant, per-user API keys) will leak across requests. Bind it as scoped rather than singleton if credentials can vary.
  • Logging. Turn on SDK debug/HTTP logging only in local. The request bodies contain full contact data, which is PII — do not let them land in production logs by default.
  • Extension point. The package's singleton binding is the natural seam: rebind activecampaign in a service provider to a subclass that adds retry, logging middleware, or metrics, rather than forking the package.
  • 1.0.0 signal. This release is the first stable tag, so the package is settling rather than mature. Pin the constraint, and if you need a client feature the wrapper does not expose, drop to the underlying SDK object rather than waiting on a wrapper update — but isolate that escape hatch in your own service class.
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/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
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