mailtanimailtani

MCP Server

The Mailtani MCP (Model Context Protocol) server lets AI agents manage your email marketing directly — contacts, lists, segments, campaigns, emails, tags, custom fields, and event tracking — without leaving your AI tool.

It works with Claude Desktop, Claude Code, Cursor, Codex, and any MCP-compatible client that supports HTTP transport.

Quick start

1. Create an MCP key

Go to Settings → API Keys / MCP, choose Create key, and pick MCP server.

The setup panel below the key list then shows a ready-to-paste config for each AI client, with your key and Project ID already filled in — copy the block for your client and skip to step 3.

If you come back later, click the eye icon next to any MCP key to show its config again.

MCP keys can be shown again because a config is no use if you cannot retrieve it — it usually gets pasted into an editor's settings file on another machine, days after the key was made. Plain API keys are different: they are hashed and shown only once, so copy those immediately. Pick the type that matches what you are doing.

2. Find your Project ID

Your Project ID is shown on the Settings → API Keys / MCP page next to the MCP setup panel, and is already included in the generated config.

3. Connect your AI client

The blocks below use mk_YOUR_API_KEY as a placeholder. The copy on your settings page has the real key substituted, so prefer that over editing these by hand.

Claude Desktop — edit the config file:

  • Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "mailtani": {
      "type": "http",
      "url": "https://mailtani.com/mcp",
      "headers": {
        "Authorization": "Bearer mk_YOUR_API_KEY",
        "X-Project-Id": "YOUR_PROJECT_ID"
      }
    }
  }
}

Claude Code — run in your terminal, as a single line:

claude mcp add --transport http mailtani https://mailtani.com/mcp --header "Authorization: Bearer mk_YOUR_API_KEY" --header "X-Project-Id: YOUR_PROJECT_ID"

Keep it on one line. PowerShell does not support the \ line continuations used in bash, so a wrapped version runs only the first line — which adds the server with no headers and then reports already exists on the retry, leaving you with a server that cannot authenticate. If that happened, run claude mcp remove mailtani -s local first and add it again.

Cursor — go to Cursor → Settings → MCP and add:

{
  "mailtani": {
    "type": "http",
    "url": "https://mailtani.com/mcp",
    "headers": {
      "Authorization": "Bearer mk_YOUR_API_KEY",
      "X-Project-Id": "YOUR_PROJECT_ID"
    }
  }
}

Codex — add to ~/.codex/config.json:

{
  "mcpServers": {
    "mailtani": {
      "type": "http",
      "url": "https://mailtani.com/mcp",
      "headers": {
        "Authorization": "Bearer mk_YOUR_API_KEY",
        "X-Project-Id": "YOUR_PROJECT_ID"
      }
    }
  }
}

Restart your client. A hammer icon (🔨) in Claude Desktop confirms the server is connected.

Authentication

Every request requires two headers:

HeaderDescription
Authorization: Bearer mk_...Your Mailtani API key
X-Project-Id: <id>The project to scope all operations to

All operations are strictly scoped to the project in the header. A tool cannot read or modify data from a different project, even if the API key belongs to the same account.

Available tools

Contacts

list-contacts

List contacts in your project. Supports search, filtering, and pagination.

InputTypeRequiredDescription
searchstring
Optional
Search by email, first name, or last name
list_idinteger
Optional
Filter by list ID
tag_idinteger
Optional
Filter by tag ID
statusstring
Optional
Filter by status (subscribed, unsubscribed, etc.)
pageinteger
Optional
Page number (default: 1)

create-contact

Create a new contact or upsert if the email already exists. Returns upserted: true if updated rather than created.

InputTypeRequiredDescription
emailstring
Required
Contact email address
first_namestring
Optional
First name
last_namestring
Optional
Last name
website_urlstring
Optional
Website URL
list_idsarray
Optional
List IDs to add the contact to
tag_idsarray
Optional
Tag IDs to assign

update-contact

Update an existing contact's fields, list memberships, or tags.

InputTypeRequiredDescription
contact_idinteger
Required
ID of the contact
first_namestring
Optional
First name
last_namestring
Optional
Last name
website_urlstring
Optional
Website URL
list_idsarray
Optional
Replace list memberships with these IDs
tag_idsarray
Optional
Replace tag assignments with these IDs

delete-contact

Permanently delete a contact.

InputTypeRequiredDescription
contact_idinteger
Required
ID of the contact to delete

Lists

list-lists

List all contact lists in your project, including contact counts. No inputs required.

create-list

Create a new contact list.

InputTypeRequiredDescription
namestring
Required
Name of the list
descriptionstring
Optional
Optional description
colorstring
Optional
Hex color (default: #228be6)

add-contact-to-list

Add one or more contacts to a list. Maximum 500 contacts per call.

InputTypeRequiredDescription
list_idinteger
Required
ID of the list
contact_idsarray
Required
Contact IDs to add (max 500)

remove-contact-from-list

Remove a single contact from a list.

InputTypeRequiredDescription
list_idinteger
Required
ID of the list
contact_idinteger
Required
ID of the contact to remove

Segments

list-segments

List all segments with their contact counts. No inputs required.

create-segment

Create a segment with filter conditions.

InputTypeRequiredDescription
namestring
Required
Segment name
match_typestring
Required
all or any
conditionsarray
Required
Array of condition objects (max 50)
descriptionstring
Optional
Optional description
colorstring
Optional
Hex color (default: #228be6)

get-segment-contacts

Get paginated contacts that match a segment's conditions.

InputTypeRequiredDescription
segment_idinteger
Required
ID of the segment
pageinteger
Optional
Page number (default: 1)

Campaigns

Campaigns are always created as drafts via MCP, so nothing sends by accident. Review a draft in the Mailtani app, then activate it there or with activate-campaign / bulk-update-campaign-status.

list-campaigns

List campaigns with optional filters and pagination.

InputTypeRequiredDescription
typestring
Optional
Filter by type: broadcast, sequence, or flow
statusstring
Optional
Filter by status: draft, active, paused, etc.
pageinteger
Optional
Page number (default: 1)

create-campaign

Create a single-email campaign of any type as a draft. Prefer create-broadcast, create-sequence or create-flow — they model their shape properly. This is the low-level option for one email with no steps or targeting.

InputTypeRequiredDescription
namestring
Required
Campaign name
typestring
Required
broadcast, sequence, or flow
subjectstring
Required
Email subject line
body_htmlstring
Required
HTML body
sending_email_idinteger
Required
ID of the sending email (from address)

create-broadcast

Create a one-time email blast as a draft.

InputTypeRequiredDescription
namestring
Required
Campaign name
subjectstring
Required
Email subject line
body_htmlstring
Required
HTML body
sending_email_idinteger
Required
ID of the sending email (from address)
list_idsarray
Optional
Contact list IDs to target
segment_idsarray
Optional
Segment IDs to target
scheduled_atstring
Optional
ISO 8601 datetime to schedule the send

create-sequence

Create a multi-step drip sequence as a draft.

InputTypeRequiredDescription
namestring
Required
Campaign name
sending_email_idinteger
Required
ID of the sending email (from address)
stepsarray
Required
Array of step objects (min 1, max 50)

Each step object:

FieldTypeRequiredDescription
subjectstring
Required
Subject line for this step
body_htmlstring
Required
HTML body for this step
delay_daysinteger
Required
Days to wait before sending (0 = immediately)

create-flow

Create a visual automation flow as a draft. The flow graph is built automatically from your steps.

InputTypeRequiredDescription
namestring
Required
Campaign name
trigger_typestring
Required
One of: contact_added, contact_updated, contact_added_to_list, event_received, form_submitted
sending_email_idinteger
Required
ID of the sending email (from address)
stepsarray
Required
Array of step objects (min 1, max 20)

Each step object:

FieldTypeRequiredDescription
subjectstring
Required
Subject line for this step
body_htmlstring
Required
HTML body for this step
delay_daysinteger
Required
Days to wait after the previous step

create-advanced-flow

Create a flow with the full node graph: branches, delays, and campaign start/stop nodes. Use this instead of create-flow whenever the flow needs to branch on a contact property. Returns a draft.

InputTypeRequiredDescription
namestring
Required
Campaign name
trigger_typestring
Required
As for create-flow
event_namestringWhen event_receivedThe exact event to match. Without it the flow fires on every event
sending_email_idinteger
Required
ID of the sending email
nodesarray
Required
Node objects: {id, type, data}
edgesarray
Required
Connections: {source, target, handle}. Use trigger as the source of the entry edge
reply_tostring
Optional
Reply-To address. Defaults to the sending address
track_opensboolean
Optional
Defaults to false. Applied at send time and cannot be backfilled, so a flow left at the default has no open data for anything it already sent. Pass true for marketing flows
skip_unverifiedboolean
Optional
Never send to unverified addresses. Defaults to false
cooldown_daysinteger
Optional
Minimum days before the same contact can re-enrol
ignore_frequency_capboolean
Optional
Exempt from the account frequency cap. Transactional flows only
stop_on_eventsarray
Optional
Event names that end enrolment, e.g. ["user.subscribed"]
stop_whenarray
Optional
Conditions on the contact's fields that end enrolment: [{"field":"custom.plan","operator":"equals","value":"professional"}]

Node types: send_email {subject, body_html, delay_days}, delay {duration, unit} where unit is days or hours, branch {conditions}, audience_filter {conditions}, start_campaign / stop_campaign {campaign_id}, wait_for_reply, wait_for_option.

Edges leaving a branch need handle set to yes or no. A branch missing either arm is rejected: contacts evaluating that way would sit in the flow forever with nothing to tell you.

Stop rules over stop nodes. stop_on_events and stop_when live on the flow that should stop, so a new flow brings its own rule. A stop_campaign node names targets by id and only stops the flows that existed when you wrote it — add another later and nothing stops it, with no error, because the node did stop everything it was told to.

update-flow

Edit an existing flow in place, keeping its id, enrolment history and stats. Refuses to edit an active campaign: contacts are walking its graph, and replacing nodes underneath them strands anyone whose current node disappears. Archive it first.

InputTypeRequiredDescription
campaign_idinteger
Required
The flow to update
statusstring
Optional
draft or archived. Use activate-campaign to make a flow live
nodes / edgesarray
Optional
Replacement graph. Must be passed together

All settings from create-advanced-flow can also be updated.

inspect-email-body

Check an email body before creating a campaign: markdown that will render literally, unbalanced tags, dead links, variables with no fallback, and what the plain-text part will contain.

InputTypeRequiredDescription
body_htmlstring
Required
The HTML body to inspect
subjectstring
Optional
Subject line, checked for HTML and length

simulate-flow

Walk a flow for one contact and see the exact path they would take: which branch arms, which emails, and why. Reads their real stored fields. Sends nothing and writes nothing.

InputTypeRequiredDescription
campaign_idinteger
Required
The flow to simulate
emailstring
Required
Contact whose stored fields decide every branch

Use this before activating any flow that branches. The API test tool answers "would this contact enrol?"; this answers "which email would they actually get?" — a different question, and the one that catches the failure that matters.

A branch reading a field that resolves unexpectedly takes the same arm for everyone, with no error and nothing in the logs. The symptom is a flow telling people who shared their video that they never shared it. Simulating shows the value behind each decision:

branch has-link  took: no
  custom.hasShareLink is NOT SET, equals "true"

NOT SET means the property never reached the contact as a custom field — usually because no custom field of that name exists, so the value was dropped on arrival. A value in quotes that looks wrong ("1" where you expected "true") means it arrived in the wrong shape.

activate-campaign

Activate a single draft campaign so it can send.

InputTypeRequiredDescription
campaign_idinteger
Required
ID of the draft campaign to activate

Only drafts can be activated.

Broadcasts and sequences are rejected when they have no enrollments — activating an empty one would look like it worked while sending to nobody.

Flows are exempt. An event-triggered flow has no enrollments until its trigger event fires, so zero at activation is the normal state rather than a mistake.

bulk-update-campaign-status

Archive or activate up to 100 campaigns in one call. Useful when retiring a whole generation of flows, which would otherwise be dozens of calls against the 30/minute rate limit.

InputTypeRequiredDescription
campaign_idsarray
Required
Campaign IDs to update (1–100)
statusstring
Required
archived or active

Each campaign is checked on its own and one failure never stops the rest. The response reports a result per id — archived/active, already_*, not_found, or skipped with a reason — so a partial run is unambiguous. Sent campaigns are never archived: they are the only record that the send happened.

enroll-contact

Add a contact to a campaign.

InputTypeRequiredDescription
campaign_idinteger
Required
Campaign to enroll into
contact_idinteger
Required
Contact to enroll

Emails

list-emails

List sent emails in your project.

InputTypeRequiredDescription
contact_idinteger
Optional
Filter by contact ID
pageinteger
Optional
Page number (default: 1)

send-email

Send a one-off transactional email. Respects your plan's daily send limit.

InputTypeRequiredDescription
contact_emailstring
Required
Recipient email address
subjectstring
Required
Email subject
body_htmlstring
Required
HTML body
sending_email_idinteger
Required
ID of the sending email (from address)

Tags

list-tags

List all tags with contact counts. No inputs required.

create-tag

Create a new tag.

InputTypeRequiredDescription
namestring
Required
Tag name
colorstring
Optional
Hex color (default: #868e96)

delete-tag

Permanently delete a tag.

InputTypeRequiredDescription
tag_idinteger
Required
ID of the tag

Custom Fields

list-custom-fields

List all custom fields in your project. No inputs required.

create-custom-field

Create a custom field. Idempotent — returns the existing field if the name already exists.

InputTypeRequiredDescription
namestring
Required
Internal name used in {{custom.name}} variables
labelstring
Required
Human-readable label
typestring
Required
text, number, boolean, or date

Events

track-event

Track a custom event for a contact. Properties are persisted as custom field values and can trigger flow automations.

InputTypeRequiredDescription
emailstring
Required
Email of the contact who triggered the event
event_namestring
Required
Event name (e.g. purchase_completed)
propertiesobject
Optional
Key-value properties attached to the event

Rate limits

The MCP server allows 30 requests per minute per API key. Add delays between bulk operations in agent workflows.

Plan limits

OperationLimit
send-emailRespects your daily email send limit
All other toolsNo additional MCP-specific limits

Example agent workflows

Onboard a new signup:

1. create-contact (email, first_name, list_ids: [newsletter-list-id])
2. track-event (email, event_name: "signup_completed")
3. send-email (welcome email via sending_email_id)

Create a welcome sequence:

1. create-sequence (name: "Welcome", steps: [day 0 intro, day 3 tips, day 7 case study])
2. Open app → review → activate

Tag and segment high-value users:

1. list-contacts (search or filter)
2. create-tag (name: "High Value")
3. update-contact (tag_ids: [high-value-tag-id]) for each contact
4. create-segment (conditions matching the tag)

Troubleshooting

"Missing or invalid API key" — Check that your Authorization header starts with Bearer mk_.

"X-Project-Id header is required" — Add the X-Project-Id header with your numeric project ID.

"Contact not found" / "Campaign not found" — The resource either doesn't exist or belongs to a different project than the one in your header.

"Plan limit reached" — You have hit your daily email send limit. It resets at midnight UTC.

Rate limit errors — The server allows 30 requests per minute. Add delays between bulk operations.