EmailOctopus exposes one programmatic surface: a REST API whose current generation is v2, launched October 7, 2024 at https://api.emailoctopus.com with Bearer auth, cursor pagination and RFC 7807 errors. It is a list-and-contact API, not a sending API — no message endpoint, no attachment upload, no SMTP relay — so anything transactional has to leave through a second provider. The agent verdict is equally blunt: no official MCP server in 2026, and every EmailOctopus MCP endpoint you can connect to is a third-party wrapper over that same v2 API.
The Model Context Protocol gives an LLM client typed, permissioned access to a SaaS product instead of a scraped dashboard. For an ESP the question is simple: can an agent read your audience, tag contacts and trigger a flow with no glue code? For EmailOctopus in 2026 the answer is yes — but only through someone else’s infrastructure.
As of August 2026 nothing on emailoctopus.com, in the knowledge base or in the v2 API documentation announces an MCP server, and there is no public EmailOctopus GitHub organisation to host one. The 4 working endpoints below are all built by automation vendors on top of the public REST API — their tool surface is contact CRUD and tagging, not the full v2 spec. Because EmailOctopus has no send endpoint at all, no MCP server can make it deliver an arbitrary email.
Four hosted options expose EmailOctopus over MCP. All authenticate with your own API key or an account link, and all inherit the same ceiling: whatever that vendor’s app already supports.
Hosted endpoint exposing 6 actions to any MCP client: add and remove tags, unsubscribe, change an email address, add or update a contact, find a contact. No hosting or code required.
Connect server served from https://mcp.pipedream.net/v2 after account linking. Wraps Pipedream’s EmailOctopus actions and triggers, so event-driven workflows come with the agent tools.
Managed toolkit with per-framework setup guides for Claude, Codex and others. Handles auth storage and tool schemas — the main reason teams pick it over a hand-rolled wrapper.
viaSocket-hosted wrapper around the API for AI agents. Same caveat: the exposed tools follow viaSocket’s connector, not the v2 endpoint list.
A missing first-party MCP server is no dealbreaker for a newsletter tool, but it changes who owns the integration: a vendor wrapper puts a second SaaS in the credential path with a tool list you do not control. If your agent needs full v2 coverage — batch upserts, campaign reports, automation queueing — wrapping the REST API with a Bearer key is about a hundred lines of code. Check Mailchimp, MailerLite and Sender before assuming an official server exists anywhere in this category.
Everything below applies to v2. v1.6 lived on a different host with a different auth mechanism, is documented as legacy and unmaintained, and its keys are not portable to v2.
| Base URL | https://api.emailoctopus.com RECOMMENDED |
| Legacy base URL | https://emailoctopus.com/api/1.6/ (v1.6, legacy) |
| Current version | v2, live since October 7, 2024 |
| Response format | JSON, shaped as { data, paging } on collections |
| Authentication | Authorization: Bearer eo_xxxxx |
| Pagination | Cursor-based, max 100 results per page |
| Cursor field | paging.next.url or paging.next.starting_after |
| Error format | RFC 7807 problem+json with type, title, detail |
| Send endpoint | Does not exist — no transactional API, no SMTP relay |
The resource model is deliberately small: lists, the contacts inside them, the fields and tags on those contacts, read-only campaign reports, and an automation queue you can push a contact into. Campaigns are composed in the dashboard and cannot be created over the API. Internalise that before designing an integration: EmailOctopus is an audience system of record with reporting attached, and dispatch stays inside the product. Teams needing a programmatic send path pair it with Amazon SES or a turnkey relay, exactly as the knowledge base recommends.
v2 accepts one credential type: an API key created in account settings and sent as Authorization: Bearer eo_xxxxx. Keys are account-scoped, not list-scoped and not permission-scoped, so a key that can read a list can also delete contacts from it. Rotate by generating a new key and revoking the old one; there is no OAuth flow and no scope selector.
curl -sS -H "Authorization: Bearer eo_xxxxxxxxxxxxxxxx" https://api.emailoctopus.com/lists
v1.6 passed the credential as an api_key query-string or body parameter against https://emailoctopus.com/api/1.6/. It still answers for existing keys — there has been no forced migration — but it is unmaintained and it leaks the credential into access logs and proxy caches. Treat any code path still doing this as debt with a deadline you set yourself, because EmailOctopus has published no sunset date.
The commonest migration failure. The two generations do not share a key namespace: generate a fresh v2 key, then swap host and auth mechanism together. Reusing the old key against the new base URL returns an auth error that reads like a permissions problem and is not one. Keys carry no scopes, so treat each as a full-account credential — secret manager only, never client-side.
EmailOctopus documents a token-bucket limiter: generous for list maintenance, tight for bulk backfills. Read it as a burst allowance plus a refill rate, not a flat per-minute quota.
| Limit | Value | Notes |
|---|---|---|
| Sustained request rate | 600 requests/minute | Equivalent to 10 requests per second |
| Burst allowance | 100 requests | Token bucket of 100 tokens |
| Refill rate | 10 tokens/second | Bucket refills continuously, not on a fixed window |
| Over-limit behaviour | HTTP 429 | Connection blocked for up to 60 seconds |
| Remaining-quota header | X-RateLimiting-Remaining | Present on every response — read it, do not guess |
| Collection page size | 100 results | Hard maximum per page on every collection |
The workaround for large imports is batching, not parallelism. PUT /lists/{list_id}/contacts/batch upserts many contacts per request, turning a 50,000-contact backfill from a rate-limit fight into a few hundred calls. If you must iterate per contact, cap concurrency at 10 in-flight requests and back off on X-RateLimiting-Remaining before the bucket empties — a 429 is expensive because it blocks the connection for up to a full minute rather than rejecting one call. Note what is not in the table: no hourly send ceiling, no recipients-per-message cap, no maximum message size — sending is not an API operation at all.
EmailOctopus maintains no official SDKs in any language. Its knowledge base lists 6 community projects and states they are unofficial and untested by the team. The stated position is that the API “plays nicely with any REST client in your chosen language”; EmailOctopus offers developer discounts to people who open-source a library instead of shipping one, and no public EmailOctopus GitHub organisation exists.
| Language | Package | Install | Repo |
|---|---|---|---|
| Node.js | email-octopus | npm install email-octopus | wthomsen |
| Python | emailoctopusAPI | git clone the repo | vivekfantain |
| Ruby | email_octopus | gem install email_octopus | tubbo |
| Swift | EmailOctopusKit | Swift Package Manager | caloon |
| PHP | goran-popovic/email-octopus-php | composer require | goran-popovic |
| PHP (Laravel) | goran-popovic/email-octopus-laravel | composer require | goran-popovic |
Several listed libraries predate the October 2024 v2 launch — the Python project is documented as a v1 client. v2 changed host, auth header and pagination shape at once, so a v1-era wrapper does not degrade gracefully; it simply fails to authenticate. Confirm the repo targets api.emailoctopus.com with a Bearer header before taking the dependency.
The two PHP packages from goran-popovic are the most actively maintained and the only ones with a first-class Laravel path. The Ruby gem is the oldest and best read as reference code. On Node and Python, skip the wrapper: two fetch or requests calls cover contact upsert and pagination, which is most of what the API does.
The full v2 surface reads in one sitting. The fifteen entries below cover every documented resource plus the two behaviours common to all of them. Canonical reference: emailoctopus.com/api-documentation/v2.
| Resource | Methods | Description |
|---|---|---|
| Lists /lists | GET, POST | List all lists (paginated, 100/page) or create a new list. |
| List /lists/{list_id} | GET, PUT, DELETE | Retrieve, rename or delete a single list. |
| List contacts /lists/{list_id}/contacts | GET, POST, PUT | Page through contacts, create one, or upsert one by email address. |
| Contacts batch /lists/{list_id}/contacts/batch | PUT | Batch create-or-update many contacts in a single request. |
| Contact /lists/{list_id}/contacts/{contact_id} | GET, PUT, DELETE | Read, update (fields, tags, status) or delete one contact. Id = MD5 of the lowercased email. |
| Fields /lists/{list_id}/fields | POST | Create a custom field; existing fields are returned inside the list object. |
| Field /lists/{list_id}/fields/{tag} | PUT, DELETE | Update or delete a custom field, addressed by its merge tag. |
| Campaigns /campaigns | GET | List campaigns with status and metadata. Read-only — composition is dashboard-only. |
| Campaign /campaigns/{campaign_id} | GET | Retrieve a single campaign’s details. |
| Summary report /campaigns/{campaign_id}/reports/summary | GET | Aggregate sent, opened, clicked, bounced, complained and unsubscribed counts. |
| Contact reports /campaigns/{campaign_id}/reports/{event} | GET | Per-contact event collections, one per event type. |
| Links report /campaigns/{campaign_id}/reports/links | GET | Click performance broken down per link URL in the campaign. |
| Automation queue /automations/{automation_id}/queue | POST | Push a contact into an automation queue — the only programmatic flow trigger. |
| Rate-limit headers (all endpoints) | All | Every response carries X-RateLimiting-Remaining; 429 means the bucket is empty. |
| Errors (all endpoints) | All | RFC 7807 problem+json with type/title/detail. Treat non-2xx as failure. |
The cheapest way to confirm a key is a v2 key and not a stale v1.6 one: hit the lists collection and read the status line before debugging anything else.
curl -i -H "Authorization: Bearer eo_xxxxxxxxxxxxxxxx" "https://api.emailoctopus.com/lists?limit=1" # 200 OK -> key is valid for v2 # 401/403 -> key is a legacy v1.6 key, regenerate it in Account settings # # Useful response header: # X-RateLimiting-Remaining: 99
import hashlib
import requests
API_KEY = "eo_xxxxxxxxxxxxxxxx" # v2 key from EmailOctopus > Account settings > API
BASE = "https://api.emailoctopus.com"
LIST_ID = "00000000-0000-0000-0000-000000000000"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
# 1. Upsert a contact (PUT on the collection = create or update by email)
resp = requests.put(
f"{BASE}/lists/{LIST_ID}/contacts",
headers=HEADERS,
json={
"email_address": "ada@example.com",
"fields": {"FirstName": "Ada"},
"tags": {"vip": True},
"status": "subscribed",
},
timeout=30,
)
resp.raise_for_status()
contact = resp.json()
# contact_id is the MD5 of the LOWERCASED email address
print(contact["id"] == hashlib.md5(b"ada@example.com").hexdigest())
# 2. Page through every contact using the cursor in `paging.next`
url = f"{BASE}/lists/{LIST_ID}/contacts?limit=100"
while url:
page = requests.get(url, headers=HEADERS, timeout=30).json()
for c in page["data"]:
print(c["email_address"], c["status"])
url = (page.get("paging") or {}).get("next", {}).get("url")
# NOTE: no send/message endpoint exists - campaigns are sent from the dashboard.// npm i node-fetch@3 (or use global fetch on Node 18+)
const API_KEY = process.env.EMAILOCTOPUS_API_KEY; // v2 key
const BASE = 'https://api.emailoctopus.com';
const LIST_ID = '00000000-0000-0000-0000-000000000000';
const headers = {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
};
// 1. Upsert a contact
const res = await fetch(`${BASE}/lists/${LIST_ID}/contacts`, {
method: 'PUT',
headers,
body: JSON.stringify({
email_address: 'ada@example.com',
fields: { FirstName: 'Ada' },
tags: { vip: true },
status: 'subscribed',
}),
});
if (!res.ok) throw new Error(`EmailOctopus ${res.status}: ${await res.text()}`);
// 2. Trigger an automation for that contact
await fetch(`${BASE}/automations/AUTOMATION_ID/queue`, {
method: 'POST',
headers,
body: JSON.stringify({ contacts: [{ email_address: 'ada@example.com' }] }),
});
// 3. Cursor pagination, with 429 back-off
let url = `${BASE}/lists/${LIST_ID}/contacts?limit=100`;
while (url) {
const r = await fetch(url, { headers });
if (r.status === 429) { await new Promise(s => setTimeout(s, 60000)); continue; }
const page = await r.json();
url = page.paging?.next?.url ?? null;
}The knowledge base is explicit that all emails must be created and sent through the dashboard, and v2 has no message resource to compensate. EmailOctopus Connect looks like the exception and is not: its setup guide asks for an AWS IAM access key and secret with the AmazonSESFullAccess policy, and EmailOctopus calls the SES API on your behalf. “EmailOctopus SMTP credentials” are always Amazon SES credentials from someone’s own AWS account. Plan transactional mail on Amazon SES or a dedicated relay from day one.
v2 launched on a new host (api.emailoctopus.com, Bearer header) while v1.6 lived at emailoctopus.com/api/1.6/ with an api_key parameter. Legacy keys keep working there but will not authenticate against v2, so migrating means regenerating the key, changing the host, moving the credential into a header and rewriting pagination — four changes that ship together. No v1 sunset date is published, which makes it easy to defer indefinitely and get caught during an unrelated incident.
Collections cap at 100 results and return a paging.next holding both a ready-made url and a starting_after cursor. Follow the URL instead of incrementing a page number: there is no total count to loop against, and an invented offset returns page one repeatedly. The loop ends when paging.next is absent, not when a page comes back short.
A contact’s id is the MD5 hash of its lowercased email address. Hashing the address as typed produces a valid-looking id that resolves to nothing, and the 404 reads like a missing contact rather than a normalisation bug. Lowercase before hashing everywhere, including in the job that reconciles your warehouse against the list. Mailchimp uses the same convention, so ported code usually survives.
EmailOctopus does not support file attachments at all; the knowledge base tells you to host the file elsewhere and link to it. So there is no attachment API, no MIME upload path and no published maximum message size. A migration plan that assumed you could attach a PDF invoice fails at the product level, not the API level.
POST /automations/{automation_id}/queue is the only programmatic trigger./lists.api.emailoctopus.com with Bearer auth, cursor pagination and RFC 7807 errors. v1 keeps working but is documented as legacy; keys must be regenerated.Product updates: emailoctopus.com/blog/category/product-updates. API changes are announced in the v2 documentation.
v2, live since October 7, 2024 at https://api.emailoctopus.com. v1.6 at https://emailoctopus.com/api/1.6/ is documented as legacy and no longer actively maintained. Existing v1 keys still work there — no forced migration, no announced sunset date — but nothing new is added, and the credential travels in the query string.
Generate a key in Account settings and send it as Authorization: Bearer eo_xxxxx. There is no OAuth flow and no per-key scoping, so every key is a full-account credential — keep it server-side. Critically, a v1.6 key will not authenticate against v2. A 401 or 403 straight after changing the base URL is almost always this.
A token bucket of 100 tokens refilling at 10 per second — 600 requests per minute sustained with a 100-request burst. Exceeding it returns HTTP 429 and blocks the connection for up to a minute, so it is worth avoiding rather than retrying into. Read X-RateLimiting-Remaining on every response, and use PUT /lists/{list_id}/contacts/batch for imports.
Send PUT /lists/{list_id}/contacts with an email_address plus optional fields, tags and status. PUT on the collection is an upsert keyed on email — safe to replay, and the right call for a sync job. To address an existing contact use /lists/{list_id}/contacts/{contact_id}, where contact_id is the MD5 of the lowercased email address; skipping the lowercase step is the classic source of phantom 404s.
No. EmailOctopus maintains zero official SDKs and says its API “plays nicely with any REST client in your chosen language”. The knowledge base lists 6 community libraries (Node.js, Python, Ruby, Swift, two PHP packages) with the explicit caveat that they are unofficial and untested. Several predate v2 — the Python one targets v1 — so check the repo for api.emailoctopus.com and Bearer auth first. A small in-house client is usually safer.
No. As of August 2026 there is no first-party MCP server, no AI connector and no public EmailOctopus GitHub organisation. Four third-party wrappers work today — Zapier MCP, Pipedream, Composio and viaSocket — but each exposes only its own connector’s actions, mostly contact CRUD and tagging, not the full v2 surface. And with no send endpoint, no MCP server can make an agent send an arbitrary email through EmailOctopus.
This review follows our email infrastructure testing methodology. We disclose affiliate relationships in our editorial independence policy.