Manage EmailOctopus lists, contacts, fields, tags, campaign reports and API-triggered automations via the EmailOctopus v2 API.
EmailOctopus ships in the w6w first-party pack. It declares 25 actions, 3 health checks, and the host runs its code in a sandbox that never sees the credential.
io.w6w.emailoctopusEmailOctopus manages email marketing lists, and this app covers the list side of a campaign: contacts, custom fields, tags and the lists that hold them, over the EmailOctopus v2 API.
It handles the everyday contact lifecycle — creating, updating and upserting contacts individually or in a batch, applying and removing tags, and managing custom fields — plus starting an API-triggered automation for a contact who just joined a list. On the reporting side, it reads campaign summaries, click and open reports, so a workflow can act on how a past send performed.
EmailOctopus’s v2 API is read-only for campaigns themselves — there is no way to create or send an email through it — so this app focuses on the list-management and reporting half of the job, the part a script or scheduled automation actually needs. Health checks track EmailOctopus’s own status page, live API reachability and rate-limit headroom, so a workflow can tell a slow patch from an outage.
Three routes to the same 25 actions. The Workflow tab is generated from EmailOctopus's own manifest and carries its real ids, so it is copy-pasteable; the Code and CLI examples are the same call for any action on any app, so every app-specific value in them is a blank you fill in.
create-contact Add a contact to a list. Fails with a 409 conflict if the email address is already on the list — use Create or Update Contact for upsert semantics.
create-field Define a custom field on a list. `tag` is the machine name used in merge syntax and as the key inside a contact's `fields` object.
create-list Create a new contact list. Name is the only accepted attribute (255 characters maximum); double opt-in and custom fields are configured afterwards.
create-tag Define a tag on a list. This only creates the tag; attaching it to a contact is done through the contact update endpoints. A tag the list already has returns a 409 conflict.
delete-contact Permanently remove a contact from a list. Returns 204 with no body. To stop mailing a contact but keep the record, set its status to `unsubscribed` instead.
delete-field Remove a custom field definition from a list, addressed by its tag. The stored values for that field on every contact go with it. Returns 204 with no body.
delete-list Permanently delete a list and every contact on it. Returns 204 with no body; a repeat call returns 404.
delete-tag Delete a tag from a list. It is removed from every contact carrying it. Returns 204 with no body.
get-campaign Fetch a single campaign by id, including its subject, sender, recipient list ids and HTML content.
get-campaign-links-report Per-link click totals for a campaign — each URL with its total and unique click counts. Not paginated: the whole report comes back in one call.
get-campaign-summary-report Aggregate counters for a campaign. `bounced` is `{hard, soft}` and `opened`/`clicked` are `{total, unique}` objects, not plain numbers.
get-contact Fetch a single contact from a list by its id, or by the MD5 hash of its lowercased email address — EmailOctopus accepts either in the same path segment.
get-list Fetch a single list by id, including its custom field definitions, its tags, and its pending/subscribed/unsubscribed counts.
list-campaign-reports List the contacts in one campaign engagement bucket — opened, clicked, bounced, complained, sent, unsubscribed, not-opened or not-clicked. The bucket is required; there is no combined feed.
list-campaigns Fetch one cursor page of the account's campaigns with their status (draft, sending, sent, error), recipients, sender and HTML content.
list-contacts Fetch one cursor page of a list's contacts, optionally narrowed by tag, status, or creation/update date range.
list-lists Fetch one cursor page of the account's lists. Each list carries its `fields`, `tags` and a `counts` breakdown of pending/subscribed/unsubscribed contacts.
list-tags Fetch one cursor page of the tags defined on a list. Tags belong to a list, not to the account, and each row is just `{ tag }`.
start-automation Queue a contact into an automation. The automation must use the "Started via API" trigger; a contact can only be queued once unless "Allow contacts to repeat" is enabled. Returns 204 with no body.
update-contact Update an existing contact by id. Every attribute is optional; omitted attributes are left unchanged.
update-contacts-batch Update many contacts on one list in a single call, addressed by contact id. Returns per-item `success` and `errors` arrays — a 200 does not mean every item succeeded.
update-field Update a custom field, addressed by its current tag. `label`, `tag` and `type` must all be supplied — the body replaces the definition rather than patching it.
update-list Rename a list. `name` is the only attribute the v2 API accepts here — double opt-in cannot be toggled through the API.
update-tag Rename a tag on a list. The tag string is its identity, so the rename applies to every contact carrying it.
upsert-contact Upsert a contact by email address: created if new, updated if it already exists. Safe to retry, unlike Create Contact.
A workflow step names the app and the action, and the editor fills in the
connection when you pick one. This is the Step shape from the
workflow spec, carrying EmailOctopus's real ids.
{
"manifestVersion": "2",
"name": "emailoctopus-example",
"steps": [
{
"id": "create-contact",
"uses": {
"app": "io.w6w.emailoctopus",
"action": "create-contact",
"connection": "conn_YOUR_CONNECTION_ID"
},
"with": {
"listId": "<listId>",
"emailAddress": "<emailAddress>"
}
}
]
}create-contact create-field create-list create-tag get-campaign +20 more actions available
Every app-specific value here is a blank you have to fill in. An
app action is reached through the connection that authenticates it, so the
address is a connection id, not the app id — and connections belong to your account,
so a public page cannot know yours. Create one for EmailOctopus, then fill in
the three blanks: conn_YOUR_CONNECTION_ID, the action key, and the
parameters that action declares. The call itself is real — the shape is transcribed
from the studio's own snippet builder, which prints the same kind of blanks — but
nothing in it is specific to EmailOctopus. The Workflow tab is where this app's
real ids are.
npm install @w6w/sdkyarn add @w6w/sdkpnpm add @w6w/sdkdeno add npm:@w6w/sdkimport { W6wClient, isActionRun } from "@w6w/sdk";
// Reads W6W_BASE_URL and W6W_TOKEN from the environment when omitted.
const client = new W6wClient();
const envelope = await client.run({
urn: "conn_YOUR_CONNECTION_ID",
action: "create-contact",
payload: {
listId: "<value>",
emailAddress: "<value>",
// status: "<value>",
// fields: "<value>",
// tags: "<value>",
},
});
if (isActionRun(envelope)) console.log(envelope.value); npm install -g @w6w/cli w6w run conn_YOUR_CONNECTION_ID --action create-contact --payload '{"listId":"<value>","emailAddress":"<value>"}' Give an AI agent EmailOctopus — without giving it EmailOctopus's credentials. One MCP endpoint exposes every app, function and workflow the caller is entitled to, as tools it can discover and run. Access is granted per team while we onboard.
One tool call{
"name": "w6w_invoke",
"arguments": {
"ref": "app:io.w6w.emailoctopus#create-contact",
"input": {
"listId": "<listId>",
"emailAddress": "<emailAddress>"
}
}
}
Every tool names its target with a single ref. The
app: form above doesn't name a connection at all — the
host resolves which of the caller's EmailOctopus connections to sign
with, and refuses rather than guesses when the answer is ambiguous.
The token is attached host-side, at the moment of the call. It is never a tool argument, never in the model's context, and never in a transcript — so a prompt injection has nothing to exfiltrate.
Tools are derived per end user from what that person has actually connected and is entitled to — not one shared bot identity carrying the union of everyone's access.
Multi-step work runs on the workflow engine and returns a run handle the agent can poll — retries, branching and state survive the conversation that started them.
EmailOctopus's declared health checks are on the surface too, so an agent can tell "the vendor is down" from "your credential expired" before it burns a retry on either.
The MCP surface is part of the hosted platform. EmailOctopus itself is MIT, and the runtime that executes it is source-available (FSL).
EmailOctopus declares its own checks, so its health is a property of the app rather than something the host guesses at.
Rollup from status.emailoctopus.com, an incident.io page serving the Statuspage v2 shape. It publishes a single `Platform` component covering the whole product, so it does not report the API separately — the `api` check does that.
Unauthenticated GET of api.emailoctopus.com/lists. A schema-correct RFC 7807 401 is the expected healthy answer — it proves the API is answering. Credential validity is the `auth:api-key` check's job.
Tokens left in this credential's rate-limit bucket, read from the `X-RateLimiting-Remaining` header (note the spelling — it is not `X-RateLimit-Remaining`). The bucket holds 100 and refills at 10 per second.