Download the PHP package datomatic/laravel-active-campaign without Composer
On this page you can find all versions of the php package datomatic/laravel-active-campaign. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download datomatic/laravel-active-campaign
More information about datomatic/laravel-active-campaign
Files in datomatic/laravel-active-campaign
Package laravel-active-campaign
Short Description Laravel wrapper for Active Campaign API v3
License MIT
Homepage https://github.com/datomatic/laravel-active-campaign
Informations about the package laravel-active-campaign
Laravel wrapper for ActiveCampaign API v3
A small, explicit Laravel wrapper around the ActiveCampaign API v3.
It builds on Laravel's HTTP client, so you keep timeouts, retries, Http::fake() and the rest of the
framework's tooling, while the package takes care of authentication, the /api/3 base path, the request/response
envelopes ActiveCampaign uses (contact, tag, field, fieldValue, …) and error handling.
Requirements
- PHP 8.3+
- Laravel 12 or 13
Installation
Publish the config file:
Then add your credentials to .env:
The base URL is your account URL without the
/api/3suffix — the package appends it. You can find both values in your ActiveCampaign account under Settings → Developer.
Configuration
Usage
Every resource is reachable from the ActiveCampaign facade (or by injecting
Datomatic\ActiveCampaign\ActiveCampaign):
| Method | Resource | ActiveCampaign endpoints |
|---|---|---|
ActiveCampaign::contacts() |
contacts, contact tags, list subscriptions | /contacts, /contact/sync, /contactTags, /contactLists |
ActiveCampaign::import() |
bulk contact importer | /import/bulk_import, /import/info |
ActiveCampaign::lists() |
contact lists | /lists |
ActiveCampaign::automations() |
automations (read only) | /automations |
ActiveCampaign::contactAutomations() |
contact ↔ automation enrolments | /contactAutomations |
ActiveCampaign::deals() |
deals | /deals |
ActiveCampaign::dealStages() |
stages inside a pipeline | /dealStages |
ActiveCampaign::pipelines() |
pipelines | /dealGroups |
ActiveCampaign::accounts() |
CRM accounts | /accounts |
ActiveCampaign::accountContacts() |
account ↔ contact associations | /accountContacts |
ActiveCampaign::notes() |
notes on any record | /notes |
ActiveCampaign::tags() |
tags | /tags |
ActiveCampaign::fields() |
custom field definitions, their options and list relations | /fields, /fieldOption/bulk, /fieldRels |
ActiveCampaign::fieldOptions() |
selectable values of a field | /fieldOptions, /fieldOption/bulk |
ActiveCampaign::fieldRels() |
field ↔ list relations | /fieldRels |
ActiveCampaign::fieldValues() |
custom field values of a contact | /fieldValues |
All of them share the same CRUD surface:
Responses are returned as plain arrays, already unwrapped from the ActiveCampaign envelope and
stripped of the links key.
Pagination
The API returns 20 records per page by default and 100 at most, so list() alone will quietly
give you a slice of a large collection. Four methods cover the rest:
lazy() fetches one page at a time and only when you consume it, so it is the safe way to walk a
large account:
paginate() returns Laravel's LengthAwarePaginator, so ->links() works in a Blade view:
A perPage above 100 is clamped to 100, and count() reads the meta.total the API sends back
(it returns 0 for the few endpoints that do not report one).
All of them accept the same query string as list(), and it is applied to every page:
Note on the query string. It is parsed and re-encoded so that pagination params can be merged into it, so
[email protected]goes out asemail=john%40example.com. When the raw query and an explicitlimit/offsetargument set the same key, the argument wins.
Building queries
Anywhere a query string is accepted you can pass a Query instead, which spares you the API's
filters[...] / orders[...] syntax:
| Method | Produces |
|---|---|
filter('email', '[email protected]') |
filters[email][email protected] |
filter('cdate', $date, FilterOperator::GreaterThan) |
filters[cdate][gt]=... |
filters(['a' => 1, 'b' => 2]) |
several equality filters at once |
orderBy('cdate') / orderByDesc('cdate') |
orders[cdate]=ASC / DESC |
include('contactTags', 'contactLists') |
include=contactTags,contactLists |
limit(50) / offset(100) |
limit=50 / offset=100 |
where('search', 'john') |
a top-level param the API defines outside filters, such as contacts' email, search, listid or id_greater |
Values are normalised for you: booleans become 1/0, backed enums become their value, arrays are
joined with commas, and DateTimeInterface becomes an ISO-8601 string. FilterOperator covers the
operators the API supports (eq, neq, lt, lte, gt, gte, contains, starts_with).
A Query is a Stringable, so (string) $query still gives you the raw query string.
Contacts are walked by id
ActiveCampaign recommends
paginating contacts with id_greater rather than offset, because a deep offset on a large account
is slow and can skip records while the list shifts underneath the walk. contacts()->lazy() and
contacts()->all() do that for you, adding orders[id]=ASC&id_greater=<last id> to each page.
If your query already sets orders[...], id_greater or id_less, the generic offset walk is used
instead so your ordering is preserved. paginate() is always offset-based, since it needs
addressable page numbers.
Contacts
create(), update() and sync() accept only email, firstName, lastName, phone and the
custom field names you declared in the config. Everything else is ignored, so you can hand them a
model's toArray() without filtering it first. All three require email and throw an
ActiveCampaignException without it.
Custom fields
ActiveCampaign identifies custom fields by numeric id. Map them once in the config file:
and then use their names on both sides of the call:
Empty values are skipped, so a field you do not pass is never overwritten with an empty string.
Tags on a contact
Removing a tag needs the id of the association, not of the tag, so untag() and tryUntag()
resolve it for you with an extra GET before the DELETE.
List subscriptions
The array is keyed by list id. Plain integers (1 / 2) are accepted as well. One request per
list is sent, because the API only accepts a single contactList object per call.
Automations
As with tags, removing needs the id of the enrolment rather than of the automation, so the
remove methods resolve it with an extra GET before the DELETE.
Bulk import
Writing contacts one at a time means one request each, against a limit of 5 requests per second. The importer queues up to 250 contacts per request instead:
Contacts are accepted in the same shape as contacts()->sync() — firstName, lastName and the
custom field names from your config — and translated to the different one this endpoint expects
(first_name, fields: [{id, value}]). Its own keys are passed through untouched if you prefer
writing them directly, and subscribe/unsubscribe accept plain list ids as well as
[['listid' => 1]].
bulkAll() splits anything larger into batches the API accepts, and takes a LazyCollection so a
large import never has to sit in memory:
The import is asynchronous, so poll for the outcome:
statusOf() returns null while the API has not set a status yet, which is the case for the first
moment after queueing — leave a short delay before polling.
Lists
The four arguments of createList() are the ones the API requires; a fifth array is merged into
the list object for anything else (channel, user, send_last_broadcast, …).
ActiveCampaign does not document an update endpoint for lists, so
lists()->update()is inherited but unverified. See API-COVERAGE.md.
Automations
Automations themselves are read only in the API — you build them in the ActiveCampaign UI and enrol contacts through the API:
Deals, pipelines and accounts
Pipelines are called dealGroups in the API; this package calls them pipelines.
value is in cents and currency is lower-cased for you. A deal needs a primary contact or an
account — passing neither throws rather than letting the API reject it.
Notes attach to any of the record types the API supports:
Tags
tagType defaults to contact when you do not pass one.
Fields
The third argument is merged into the field object, so any attribute the API supports
(descript, perstag, defval, visible, ordernum, isrequired, …) can be passed through.
A field is not usable on its own
Two things are easy to miss, and the API will not warn you about either:
- A custom field stays invisible until it is related to a list. A contact only sees a field if one of the lists it belongs to has a relation to that field.
- Dropdown, listbox, radio, checkbox and multiselect fields need their options created
separately.
FieldType::requiresOptions()tells you which types those are.
createField() can do all three steps in one call:
That creates the field, bulk-creates its options in the order given, and relates it to lists 1 and 2.
Omit options/lists and nothing extra is sent.
Each step is also available on its own:
Options accept plain strings, or full arrays when you need more control. A string becomes an option
whose label and value match, and orderid follows the array order unless you set it yourself:
Options and relations have their own resources too, if you want to work with them directly:
ActiveCampaign creates options only through its bulk endpoint, so
fieldOptions()->create()sends a one-element bulk request and hands you back the created option.
Field values
For contacts you own, contacts()->sync() with the custom_fields mapping is usually the shorter
path — this resource is there for the cases where you need to address a field value directly.
Error handling
Every failing response raises a Datomatic\ActiveCampaign\Exceptions\ActiveCampaignException
carrying the endpoint and the error body returned by ActiveCampaign:
A misconfigured package raises Datomatic\ActiveCampaign\Exceptions\InvalidConfig instead, on the
first call that needs the missing value.
Connection errors, 429 and 5xx responses are retried according to retry_times / retry_sleep
before the exception is raised. 4xx responses are not retried.
Escape hatches
request() is public on every resource, so an endpoint the package does not wrap yet is still one
line away:
You can also resolve the client directly and skip the resource layer entirely:
Testing your own code
The package uses Laravel's HTTP client, so Http::fake() works as usual. ActiveCampaignFake
saves you from writing base urls and response envelopes by hand:
Paths are relative to /api/3 and may contain a * wildcard. A path you do not list answers with
an empty 200, so a test only describes the calls it cares about, and query strings are ignored
when matching so a paginated call still matches its bare path.
Testing
Roadmap
API-COVERAGE.md lists exactly which ActiveCampaign endpoints this package wraps and which it doesn't. ROADMAP.md is what is still to be built, in the order it is worth doing.
An endpoint the package does not wrap is still one line away — see Escape hatches.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
- Alberto Peripolli
- All Contributors
License
The MIT License (MIT). Please see License File for more information.
All versions of laravel-active-campaign with dependencies
spatie/laravel-package-tools Version ^1.16
illuminate/contracts Version ^12.0|^13.0
illuminate/http Version ^12.0|^13.0
illuminate/pagination Version ^12.0|^13.0
illuminate/support Version ^12.0|^13.0