ActiveCampaign’s public surface is one REST API — v3, JSON in and out, authenticated with an Api-Token header against an account-specific base URL — plus an eComm GraphQL endpoint sharing the same credential and quota. It is a marketing-automation and CRM API, not a sending API: there is no relay behind it. The 2026 headline is MCP. ActiveCampaign is one of the few ESPs to ship a first-party, hosted, account-scoped MCP server on every plan tier, while the SDK story went the other way — the only official platform SDK is a PHP wrapper frozen in 2017 against the legacy v1 API.
Model Context Protocol is how an LLM client — Claude, ChatGPT, Cursor — gets structured, permissioned access to a product instead of a raw API key. Most email platforms leave it to community wrappers. ActiveCampaign does not.
ActiveCampaign operates an official Remote MCP Server exposing roughly 50 tools over contacts, tags, lists, custom fields, campaigns, automations and deals — on Starter, Plus, Pro and Enterprise alike. The endpoint is unique to your account, copied from Settings › Developer, and connecting is a browser sign-in and approval rather than a token paste. It is a data server, not a sending server: no send-email tool, because there is no relay.
Four endpoints matter. Two come from ActiveCampaign itself — the platform server and the Postmark server, on different accounts and different billing — one is Zapier’s wrapper, one a community Python server for teams needing stdio.
Roughly 50 tools over contacts, tags, lists, custom fields, campaigns, automations and deals. Per-account URL, all plan tiers.
24 tools: sending, templates, message search, bounces, suppressions, stats. Local stdio, authenticated with POSTMARK_SERVER_TOKEN.
Hosted endpoint wrapping the Zapier actions: create or update contact, add or remove from automation, create campaign. Worth it only to keep many apps behind one endpoint.
MIT Python server for v3, v0.2.0 (April 4, 2026). Its tool list shows 33 tools while PyPI advertises 65 — verify before trusting it.
The split between the two official servers is the decision point. The ActiveCampaign server reads and writes CRM and marketing data — create a contact, apply a tag, enrol someone in an automation — but cannot send email, and its browser login is awkward headless. The Postmark server does the opposite: it sends, inspects bounces and suppressions and reads stats with a static token that works unattended. An agent that must both segment an audience and send to it needs both servers, two accounts, two invoices — unlike Klaviyo or Customer.io, where data and sending sit behind one credential.
Everything below is the v3 REST API, described in the docs as “structured around REST, HTTP, and JSON”. The two values you need before writing any code — base URL and API key — both live in Settings › Developer.
| Base URL | https://{your-account}.api-us1.com READ FROM ACCOUNTAccount- and region-specific. Never hardcode api-us1.com. |
|---|---|
| Resource path | /api/3/{resource}, e.g. /api/3/contacts |
| Current version | v3 REST. Legacy v1 remains supported with no announced sunset, but gets no new functionality. |
| GraphQL endpoint | /api/3/ecom/graphql — eComm data only: orders, customers, products, recurring payments |
| Auth header | Api-Token: {your-key} |
| Format | JSON over HTTPS — plain HTTP is refused |
| Pagination | limit (default 20, max 100) and zero-based offset |
| Result count | meta.total on collection responses — pages = ceil(meta.total / limit) |
| Sort and filter | orders[fieldName]=ASC|DESC (stackable) and filters[fieldName]=value |
| Bulk writes | POST /api/3/import/bulk_import instead of looping single-contact calls |
| Rate limit | 5 requests per second, per account (about 300/minute) |
The resource model is flatter than it looks. Contacts, deals, lists, tags and campaigns are top-level collections, and relationships between them are their own collections rather than nested sub-resources: you do not tag a contact by PUTting the contact, you POST a contactTag referencing both IDs. Same for list membership (contactLists), enrolment (contactAutomations) and field data (fieldValues). Once that clicks the API is predictable — create the join record, delete the join record, and remember that removal is a DELETE on the join record’s own ID, so you often GET it just to delete it. The cost is call volume: one logical operation is often four requests against 5 per second for the whole account. Cache tag and list IDs at startup instead of resolving names on every write.
This is the only documented authentication method for v3. The docs are explicit: “The API key should be provided as an HTTP header named Api-Token.” No query-string fallback, no HTTP Basic variant, no bearer form. GET /api/3/users/me is the canonical smoke test — if it returns your user object, key and base URL are both correct.
Keys are per-user, not per-application. Every user has their own key on the Settings › Developer tab, which also shows the account’s base URL. There is no scoping mechanism: a key carries the permissions of its owner, so the only way to constrain an integration is a dedicated user with limited permissions. Rotating a key rotates it for that human too — create a service-account user before five integrations end up sharing one admin’s token.
The GraphQL endpoint uses the same Api-Token header and the same key. No separate credential, no separate quota — it draws on the same 5 requests per second your REST sync is already spending.
OAuth-style authorization exists, but not where API developers look for it. The Remote MCP Server uses an interactive browser login; CX Apps built with App Studio and the Postmark connector have their own authorize flows. None produces a credential usable against the v3 REST API. For server-to-server work there is one path: the Api-Token header.
If you are building a product that connects to your customers’ ActiveCampaign accounts, there is no consent screen to send them through. Each customer pastes an API key and a base URL — two values, because the host differs per account and per region. Validate the pair against /api/3/users/me before storing it, and keep the base URL as configuration rather than deriving it from the account name.
ActiveCampaign publishes one API rate limit and no SMTP-shaped limits, because there is no SMTP surface. Sending volume is metered monthly against contact count rather than throttled per hour.
| Limit | Value | Notes |
|---|---|---|
| API requests | 5 per second | Per account, about 300/minute. Not per key, not per user. |
| GraphQL requests | Shared | eComm GraphQL is bound by “the same rate limit restriction as the existing v3 REST API of 5 requests per second per account”. |
| Over-limit response | 429 Too Many Requests | Carries Retry-After, RateLimit-Limit and RateLimit-Remaining — read them, do not guess. |
| Monthly send allowance | 10x to 15x contacts | Starter and Plus 10x, Professional 12x, Enterprise 15x, for plans bought on or after June 3, 2024. |
| Overage billing | $0.005 per extra send | If overage reaches 3x your limit, sending is switched off until the next billing cycle. |
| Trial cap | 100 sends | Total, not per day — enough to test an automation, not a campaign. |
| Per-hour throttle, message size, recipients, concurrency | Not published | SMTP metrics, and there is no relay — treat any figure found elsewhere as invented. |
The workaround is architectural, not clever. Stop looping single writes: POST /api/3/import/bulk_import exists so a 40,000-contact sync is not 40,000 requests. Treat the budget as shared and put every integration behind one queue with a token bucket, because the nightly warehouse sync, the checkout webhook handler and an MCP client someone connected last week all draw from the same five. Honour Retry-After on a 429 — blind retries turn a burst into a lockout. If you need high-frequency writes, this is the wrong shape of product; that is event-pipeline territory, closer to Customer.io.
This is the weakest part of the ActiveCampaign developer story. The GitHub organisation is busy in 2026, almost entirely with Postmark libraries. There is no maintained client for the platform API in any language.
| Language | Package | Install | Repo |
|---|---|---|---|
| PHP (platform API) | activecampaign/api-php | composer require activecampaign/api-php | activecampaign-api-php |
| Node (Postmark MCP, transactional) | @activecampaign/postmark-mcp | git clone then npm install | postmark-mcp |
Its README still calls it “the official PHP wrapper for the ActiveCampaign API”, but composer.json carries "time": "2017-04-26", it requires php >=5.3.0, and its new ActiveCampaign("API_URL", "API_KEY") constructor is the legacy v1 shape. It does not target v3. Use a plain HTTP client and handle pagination and 429 backoff yourself — roughly forty lines, and less risk than a dependency that stopped moving in 2017.
With no first-party v3 client, the community fills the gap and quality varies. Vet any wrapper on three checks: does it read the base URL from configuration rather than hardcoding api-us1.com, does it handle 429 with Retry-After, and does it expose meta.total so pagination is deterministic? The most visible community project in 2026 is the Python MCP server mcp-activecampaign (v0.2.0, MIT), a readable reference implementation of v3 calls even if you never run it as an MCP endpoint. On the Postmark side the picture inverts: postmark-python, postmark-php, postmark.js, postmark-dotnet and postmark-java all shipped releases in 2026.
The v3 API covers well over a hundred endpoints. These 15 carry almost every real integration. Full reference at developers.activecampaign.com.
| Resource | Methods | Description |
|---|---|---|
| Contacts /api/3/contacts | GET, POST | List, search and filter contacts, or create one. |
| Contact (single) /api/3/contacts/{id} | GET, PUT, DELETE | Retrieve, update or delete one contact by ID. |
| Contact sync /api/3/contact/sync | POST | Create-or-update keyed on email. Standardise on this. |
| Bulk import /api/3/import/bulk_import | POST | Many contacts per call instead of looping against 5 req/sec. |
| Lists /api/3/lists | GET, POST | Browse, search and create contact lists. |
| List membership /api/3/contactLists | POST | Subscribe or unsubscribe a contact via status. |
| Tags /api/3/tags | GET, POST | List and create tags; filter by name. |
| Contact tags /api/3/contactTags | POST, DELETE | Apply or remove a tag: the main automation trigger. |
| Field values /api/3/fieldValues | GET, POST, PUT | Per-contact field data; definitions at /api/3/fields. |
| Automations /api/3/automations | GET | List automation workflows and statuses to resolve IDs. |
| Contact automations /api/3/contactAutomations | GET, POST, DELETE | Enrol a contact, check progress, eject mid-run. |
| Deals /api/3/deals | GET, POST | List and create CRM deals; filter by stage, value, owner. |
| Campaigns /api/3/campaigns | GET, POST | Retrieve campaigns by status, type, date; create or duplicate. |
| Webhooks /api/3/webhooks | GET, POST, DELETE | Outbound subscriptions for contact, deal, campaign events. |
| Ecommerce orders /api/3/ecomOrders | GET, POST | Order data behind Deep Data revenue attribution. |
# Credential smoke test. Base URL comes from Settings > Developer.
curl -sS -H "Api-Token: $AC_API_TOKEN" "$AC_BASE_URL/api/3/users/me"
# Page 2 of contacts, newest first, 100 per page (the maximum).
curl -sS -H "Api-Token: $AC_API_TOKEN" "$AC_BASE_URL/api/3/contacts?limit=100&offset=100&orders[cdate]=DESC"
# Tagging is a join record, not a field on the contact object.
curl -sS -X POST -H "Api-Token: $AC_API_TOKEN" -H "Content-Type: application/json" -d '{"contactTag":{"contact":"12345","tag":"42"}}' "$AC_BASE_URL/api/3/contactTags"The pattern that matters is /api/3/contact/sync rather than POST /api/3/contacts: sync is keyed on email and safe to replay, which is what you want behind a webhook that can fire twice.
import os, time, requests
# Both values come from Settings > Developer. Never hardcode api-us1.com.
BASE = os.environ["AC_BASE_URL"].rstrip("/")
S = requests.Session()
S.headers.update({"Api-Token": os.environ["AC_API_TOKEN"],
"Content-Type": "application/json"})
def call(method, path, **kw):
while True:
r = S.request(method, BASE + path, timeout=30, **kw)
if r.status_code == 429: # 5 req/sec, per account
time.sleep(int(r.headers.get("Retry-After", 1)))
continue
r.raise_for_status()
return r.json()
def upsert(email, first=None): # idempotent, keyed on email
body = {"contact": {"email": email, "firstName": first}}
return call("POST", "/api/3/contact/sync", json=body)["contact"]
def all_contacts(page=100):
offset, total = 0, None
while total is None or offset < total:
body = call("GET", "/api/3/contacts",
params={"limit": page, "offset": offset})
total = int(body["meta"]["total"]) # authoritative count
yield from body["contacts"]
offset += pageAlmost every code sample online uses https://youraccount.api-us1.com, and ActiveCampaign’s own guidance pushes back: third-party developers should have users read the base URL from Settings › Developer, because that value is the source of truth for that user, particularly outside the United States. An integration with the US host baked in authenticates fine in testing and silently fails for a non-US customer.
The budget is not per API key and not per integration. A nightly warehouse sync, a Shopify webhook handler, a colleague’s Zap and any connected MCP client all draw from the same five per second, and eComm GraphQL shares the ceiling. The failure mode is confusing: it looks like your code broke when in fact somebody else’s job started. Centralise on one queue, use bulk_import for volume writes, and log RateLimit-Remaining so contention is visible rather than inferred.
Two constraints trip up agent builders. First, the Remote MCP Server has no send-email tool — its roughly 50 tools read and write contacts, tags, lists, fields, deals, campaigns and automations, consistent with a platform that has no relay. Transactional sending is the separate Postmark MCP server, with its own account and token. Second, the server URL is account-scoped and connection is an interactive browser login, so a config cannot be copy-pasted between accounts and does not fit CI. Headless work belongs on the v3 REST API or the Postmark stdio server.
ActiveCampaign acquired Postmark and DMARC Digests from Wildbit on May 6, 2022, and its transactional-email page advertises a globally distributed SMTP service under a “Powered by Postmark” banner — which reads like an ActiveCampaign feature. It is not bundled. Postmark’s acquisition FAQ says it “will remain available as a standalone product”, and ActiveCampaign’s setup doc requires “an ActiveCampaign account and an approved Postmark account”. No plan includes Postmark volume, and the in-app “Send a transactional email” action is a connector to your own Postmark account, not a provisioning step.
postmark-skills updated August 18, postmark-python August 17, postmark-mcp August 12, postmark-php August 11. No platform v3 SDK is maintained there.postmark-mcp v2.0.0 released under the ActiveCampaign org, expanding the official Postmark MCP server to 24 tools across sending, templates, message search, bounces, suppressions and stats.mcp-activecampaign v0.2.0 published to PyPI under MIT, with 33 tools against the v3 REST API.ActiveCampaign publishes a developer changelog at developers.activecampaign.com/changelog; its entries carry no machine-readable dates, so pinning an integration against it is manual work. On deprecation risk, v1 is not deprecated — the v3 overview promises “no plans to sunset version 1 of our API at this time” plus advance notice — but it receives no new functionality, so treat it as frozen rather than safe.
Both live under Settings › Developer. The base URL has the shape https://{your-account}.api-us1.com, resource paths hang off /api/3/, and the key goes in an HTTP header named Api-Token. Do not assume api-us1.com: the host is account-specific and differs by region. Keys are per-user.
v3 is the current REST API and where all new functionality ships. v1 is the legacy query-string API; its own docs say that while it is still fully supported, new functionality goes to v3. v1 is not deprecated — ActiveCampaign has “no plans to sunset version 1 of our API at this time” — but that makes it frozen rather than safe. Build on v3.
5 requests per second, per account. Exceeding it returns 429 Too Many Requests with Retry-After, RateLimit-Limit and RateLimit-Remaining headers. It is not per API key, so every integration competes for the same budget and eComm GraphQL shares the ceiling. For volume writes use POST /api/3/import/bulk_import.
Offset pagination. Pass limit (default 20, maximum 100) and a zero-based offset, then read meta.total from the first response — pages = ceil(meta.total / limit). Sort with orders[fieldName]=ASC|DESC and filter with filters[fieldName]=value. Order by a stable field such as cdate so records do not shuffle between pages.
Yes — genuinely first-party, which is unusual for an ESP in 2026. ActiveCampaign runs a hosted Remote MCP Server with roughly 50 tools across contacts, tags, lists, custom fields, campaigns, automations and deals, on all plan tiers including Starter. The endpoint URL is unique to your account and is copied from Settings › Developer; you connect by signing in through the browser and approving, not by pasting a token. Client guides cover Claude, ChatGPT and Cursor. It has no send-email tool — that is the separate Postmark MCP server.
Effectively no. The only official platform SDK is activecampaign/api-php, a v1-only wrapper last released 2017-04-26 that requires php >=5.3.0 and does not target v3. There is no official Python or Node client; the maintained libraries on that GitHub org are Postmark SDKs. Call v3 with a plain HTTP client and handle pagination and 429 backoff yourself, or vet a community wrapper for base-URL configurability and retry behaviour first.
This review follows our email infrastructure testing methodology. We disclose affiliate relationships in our editorial independence policy.