Skip to content

feat: active campaign integration - #29

Merged
shavonn merged 2 commits into
mainfrom
feat/active-campaign-integration
Sep 9, 2026
Merged

shavonn merged 2 commits into
mainfrom
feat/active-campaign-integration

Conversation

@shavonn

@shavonn shavonn commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ActiveCampaign as a fifth first-party mailing list driver. Entries sync to an ActiveCampaign list on sign-up with the same config surface as the existing drivers — no new API on the package side.

ActiveCampaign's v3 API addresses everything by id — lists, tags and custom fields — so most of the driver is resolving names to ids before a call can be made.

Things to note worth attention:

  • Contacts are upserted, not created. contact/sync matches on email, so a returning sign-up updates the existing contact instead of erroring on a duplicate. List membership is a separate resource (/contactLists), so subscribe is two calls: sync the contact, then place them on the list with status 1. That status also re-activates somebody who had previously unsubscribed from this list, which is the right reading of them signing up again.
  • Tags are searched before they are created. ActiveCampaign refuses a duplicate tag and matches tag names case-insensitively, so a blind create would succeed on the first sign-up and fail on every one after it. Resolved ids are memoised per driver instance.
  • Unknown attributes are dropped, not sent. Custom fields are addressed by field id, resolved from /fields by title or perstag. An attribute with no matching field on the account is skipped — a rejected contact costs the sign-up, a missing annotation does not.

Unsubscribe sets the list membership to status 2 rather than deleting the contact: the unsubscribe is a fact about this list, and a delete would remove them from every other list on the account too.

Errors are unwrapped from ActiveCampaign's errors[].title array into the MailingListException message, so a failure says what was actually refused instead of Array to string conversion.

Configuration takes the account URL exactly as ActiveCampaign shows it under Settings → Developer; /api/3 is appended by the driver.

ACTIVECAMPAIGN_API_KEY=...
ACTIVECAMPAIGN_API_URL=https://youraccount.api-us1.com
ACTIVECAMPAIGN_LIST_ID=7

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation (marks existing API as deprecated)
  • Refactor / internal change (no functional impact)
  • Documentation update
  • CI / tooling change

No breaking changes — the driver is additive and inert until waitlist.mailing_list.default (or an explicit MailingList::driver() call) selects activecampaign. No new package dependencies; the driver uses Laravel's HTTP client.

How Has This Been Tested?

Nine Pest feature tests in tests/Feature/ActiveCampaignTest.php, all driving the real sign-up path (Waitlist::add()) against a faked HTTP client rather than calling the driver directly:

  • Contact upsert and list placement send the expected payloads, and the subscriber id is persisted
    back onto the entry
  • Requests authenticate with the Api-Token header
  • /api/3 is appended to the configured account URL
  • An existing tag is reused — including a case-mismatched one — and no duplicate POST /tags is sent
  • An unseen tag is created with tagType: contact
  • Attributes map onto field ids, and one with no matching field is dropped from fieldValues
  • Unsubscribe posts status 2 and sends no DELETE
  • A missing url (or key) throws before any request is made
  • A 422 with errors[].title surfaces that reason in the exception message

To reproduce:
vendor/bin/pest tests/Feature/ActiveCampaignTest.php
composer test && composer analyse && vendor/bin/pint --test

  • composer test passes — 110 passed (195 assertions)
  • composer analyse (PHPStan / Larastan) passes — no errors
  • composer pint shows no formatting changes
  • Added or updated tests covering the change

Test Configuration:

  • PHP version(s): 8.4 locally (package supports ^8.3)
  • Laravel version(s): 12.x locally (package supports 11.x / 12.x / 13.x)
  • Database driver: SQLite (in-memory, Testbench)

Not exercised against a live ActiveCampaign account — all HTTP is faked. A reviewer with a sandbox account confirming a real round-trip would be worth having before release.

Checklist

  • My code follows the style guidelines of this project (Pint / PSR-12)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes
  • I have updated the README / docs where relevant
  • My commit messages follow Conventional Commits
  • Any breaking changes are clearly called out above

@shavonn
shavonn merged commit f40ba42 into main Sep 9, 2026
20 checks passed
@shavonn
shavonn deleted the feat/active-campaign-integration branch September 9, 2026 23:07
@github-actions github-actions Bot mentioned this pull request Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant