# MailCheap API and MCP reference Everything a MailCheap customer does in the dashboard works through the REST API and the MCP server. Both run the same actions as the dashboard, with the same plan limits, sender rules, pre-send checks, paused accounts and first-send steps. ## Authentication Create an API key on the dashboard's API keys page and send it as `Authorization: Bearer ` to https://mailcheap.utka.top/api/v1 or to the MCP server at https://mailcheap.utka.top/mcp. - Full access keys can do everything below, including creating more keys. Keep them on a server or in your AI assistant's settings. - Signups only keys can only add contacts (`POST /api/v1/contacts`, MCP tool `add_contact`), for signup forms in website code. That endpoint also answers browsers (CORS). - Every key gets 300 requests a minute across REST and MCP, and some actions have their own limit, for each key and for the account: its keys, connected apps and dashboard share the account's, so more keys don't raise it. Over a limit, the answer is 429 rate_limited. - Keys are stored hashed and shown once. Revoked keys answer 401 unauthorized. An account has up to 20 active keys. - The MCP server also takes OAuth access tokens, for apps such as ChatGPT, Claude and Grok Bot: the user signs in to MailCheap and allows the app, which then has full access. Discovery starts at https://mailcheap.utka.top/.well-known/oauth-protected-resource/mcp. ## Errors Every error answers `{ "error": { "code": "…", "message": "…" } }`. MCP tools answer the same object in an error result. Codes never change meaning: - `unauthorized` (401): The API key is missing, malformed, unknown or revoked. - `forbidden` (403): The key's level doesn't allow this action: a signups only key can only add contacts. - `not_found` (404): The newsletter, campaign, author, contact, segment, domain or key isn't in this account. - `validation_failed` (400): The input doesn't match the schema or breaks a rule. The message names the field. - `conflict` (409): The resource's state doesn't allow it, e.g. sending a campaign that is already sending or sent. Retrying won't help. - `revision_conflict` (409): The draft changed since the revision sent with update_campaign: get_campaign has the new one to start from. - `rate_limited` (429): Too many requests with this key, or too many of one action for the account, whose keys, apps and dashboard share its limits. Wait and retry. - `subscription_required` (402): The account has no active plan. - `billing_off` (404): The server is self-hosted without billing: every account has every feature, without plan limits. - `plan_limit_emails` (402): The send needs more emails than the plan has left this month. - `plan_limit_subscribers` (402): The account has more active subscribers than its plan allows. - `account_blocked` (403): The account is blocked after spam signals (bounce or complaint rates, Gmail's spam rate in Google Postmaster Tools, a spam trap, the email provider) or by an admin. Nothing is sent, the tracked links in its emails stop redirecting, and only reads, pausing, unscheduling, revoking access and the billing portal work until an admin unblocks it. Its plan stays active without being billed, and can't change until then. - `sender_not_allowed` (422): The from address is neither the newsletter's shared address nor on a verified domain of this account. - `send_check_failed` (422): A pre-send check failed: no postal address, subject, content or author, the email is over 100 KB, or its segment was deleted. - `no_recipients` (422): Nobody is left to send to. - `sending_disabled` (503): Sending is turned off on this server. - `unavailable` (503): A service the action needs isn't configured or didn't answer. Retry later. - `internal_error` (500): Something failed on MailCheap's side. Sending a campaign that is already sending or sent answers `conflict`, so retrying a send never sends twice. ## MCP The MCP server speaks Streamable HTTP at https://mailcheap.utka.top/mcp, without sessions. Each endpoint below is one tool with the same name, input and output; its annotations tell clients whether it's read-only, can't be undone, is safe to repeat or reaches outside MailCheap. The send tools can't be undone: ask the user before calling them. ### ChatGPT 1. Go to Plugins (chatgpt.com/plugins), click + and pick Add custom MCP server. 2. Name it MailCheap, enter https://mailcheap.utka.top/mcp as the server URL and pick OAuth, with no client ID or secret. If ChatGPT asks how to register, pick Client ID Metadata Document. 3. Check I understand and want to continue, then click Create as a plugin. 4. Install the plugin, sign in to MailCheap when ChatGPT asks and click Allow. 5. In a chat, type @ and pick MailCheap. Custom MCP servers are on ChatGPT on the web, for Business, Enterprise and Edu workspaces: on Business, admins and owners add them; on Enterprise and Edu, so can the members an admin allows. On Pro, ChatGPT only uses the read-only tools for now. ### Claude 1. Go to Customize › Connectors and click Add custom connector. 2. Name it MailCheap and enter https://mailcheap.utka.top/mcp. If Claude asks how it identifies itself, keep Use Claude's published identity. Click Add. 3. Click Connect, sign in to MailCheap and click Allow. 4. The connector works in claude.ai, Claude Desktop and the mobile apps. Turn it on or off in a chat from + › Connectors. On Team and Enterprise plans, an owner adds it once in Organization settings › Connectors › Add › Custom › Web, then each member clicks Connect under Customize › Connectors. The Free plan has one custom connector. ### Grok Bot 1. In a chat, ask Grok Bot to add the custom MCP server https://mailcheap.utka.top/mcp. 2. Sign in to MailCheap when it asks, and click Allow. ### Claude Code Terminal: ``` claude mcp add --transport http --scope user mailcheap https://mailcheap.utka.top/mcp \ --header "Authorization: Bearer $MAILCHEAP_API_KEY" ``` The shell puts the key in Claude Code's settings when the command runs. --scope user makes the server available in every project; run /mcp in Claude Code to check it's connected. ### Codex ~/.codex/config.toml: ``` [mcp_servers.mailcheap] url = "https://mailcheap.utka.top/mcp" bearer_token_env_var = "MAILCHEAP_API_KEY" ``` Codex reads the key from MAILCHEAP_API_KEY each time it starts, so export it in your shell profile. The same server from the terminal: codex mcp add mailcheap --url https://mailcheap.utka.top/mcp --bearer-token-env-var MAILCHEAP_API_KEY ### Cursor ~/.cursor/mcp.json: ``` { "mcpServers": { "mailcheap": { "url": "https://mailcheap.utka.top/mcp", "headers": { "Authorization": "Bearer ${env:MAILCHEAP_API_KEY}" } } } } ``` Cursor reads MAILCHEAP_API_KEY from the environment it was opened in. If it isn't set there, put the key itself after Bearer, and keep the file out of version control. ## Endpoints ### Overview Subscribers, email results and sends per hour, as on the dashboard's first page. #### Get the overview `GET /api/v1/overview` · MCP tool `get_overview` · Full access · read-only, safe to repeat The dashboard's overview for one newsletter over a period, the last 30 days unless days or from and to pick another: subscribers at the end of each day of it (each hour of a period of 2 days or less), its email results against as many days before, the emails it sent per hour, day, week or month by its length, emails sent per hour today and yesterday, and the plan's usage this month, which the account's newsletters share. - `newsletter` (query, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `days` (query, 1 | 7 | 30 | 90 | 365): The period: the last 1, 7, 30, 90 or 365 days, 1 for the last 24 hours. Default: 30. - `from` (query, string): A custom period's first day instead of days, YYYY-MM-DD in timeZone, with to: up to 365 days, both included - `to` (query, string): A custom period's last day, YYYY-MM-DD in timeZone, today at the latest - `timeZone` (query, string): IANA time zone for days and hours, e.g. Europe/Paris. Default: UTC. ```bash curl "https://mailcheap.utka.top/api/v1/overview?days=90&timeZone=Europe%2FParis" \ -H "Authorization: Bearer $MAILCHEAP_API_KEY" ``` #### Get a summary `GET /api/v1/summary` · MCP tool `get_summary` · Full access · read-only, safe to repeat What MailCheap's summary email says about one newsletter, for its last whole day or week: who joined and who unsubscribed against the period before, the total, new subscribers per day for the last 14 days or per week for the last 8 weeks, the latest issue's open and click rates against the newsletter's average with its most clicked link, and the summary's opening line. - `newsletter` (query, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `period` (query, "day" | "week"): day: yesterday. week: the 7 days before the latest Monday. Default: week. - `timeZone` (query, string): IANA time zone the days start in, e.g. Europe/Paris. Default: UTC. ```bash curl "https://mailcheap.utka.top/api/v1/summary?period=week&timeZone=Europe%2FParis" \ -H "Authorization: Bearer $MAILCHEAP_API_KEY" ``` ### Newsletters Sender, postal address, welcome and confirmation emails, list cleanup, website and its own domain, and theme. #### List newsletters `GET /api/v1/newsletters` · MCP tool `list_newsletters` · Full access · read-only, safe to repeat Lists the account's newsletters, 3 at most, first created first, with their settings and active contacts. Actions that take a newsletter use the first one when it's left out. No parameters. ```bash curl "https://mailcheap.utka.top/api/v1/newsletters" \ -H "Authorization: Bearer $MAILCHEAP_API_KEY" ``` #### Get a newsletter `GET /api/v1/newsletters/{id}` · MCP tool `get_newsletter` · Full access · read-only, safe to repeat Gets a newsletter's settings: sender, postal address, welcome, confirmation and last-chance emails, list cleanup, archive and its analytics, theme and notifications. - `id` (path, string, required): The newsletter's ID or slug ```bash curl "https://mailcheap.utka.top/api/v1/newsletters/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $MAILCHEAP_API_KEY" ``` #### Create a newsletter `POST /api/v1/newsletters` · MCP tool `create_newsletter` · Full access Creates a newsletter. An account has up to 3, sharing its plan's emails and subscribers; one more answers conflict. Without a fromEmail it sends from the shared address hello@., which MailCheap sets up in a few minutes. List cleanup is on unless cleanup.enabled is false. Without a postalAddress, its emails print the server's default if it has one: MailCheap's address on MailCheap Cloud. - `name` (body, string, required): The newsletter's name, shown in emails and on the archive - `slug` (body, string): Lowercase letters, digits and dashes. Default: made from the name, numbered when another newsletter has it. - `imageUrl` (body, string): Square logo, 128×128 or larger: an https:// link - `fromName` (body, string): Name in the From field - `fromEmail` (body, string): The shared address (hello@., set automatically) or an address on a domain added to this account - `replyTo` (body, string): Where replies go while the newsletter has no author: an issue's replies go to its author, and the newsletter's own emails' to its default author (list_authors). Empty: replies to the shared address go to the account owner. - `postalAddress` (body, string): Printed in the footer of every email. Never set, it's the server's default (MailCheap's address on MailCheap Cloud, none on a self-hosted server); saved empty, there's none and sending waits for one. - `archiveEnabled` (body, boolean): A public page for each sent issue, with a signup form. On for new newsletters: the website goes live after the first issue. - `description` (body, string): One sentence under the name on the archive, 200 characters at most - `author` (body, object): The archive's author page; the author's name is fromName - `author.bio` (body, string): Under the author's name on the archive, 160 characters at most - `author.imageUrl` (body, string): Author photo: an https:// link. The newsletter image otherwise. - `socialLinks` (body, string[]): https:// links shown on the archive, 8 at most - `analytics` (body, object): Analytics tools on the newsletter's website: each loads on every page of it, never in emails, and hears of each signup there (sign_up for Google Analytics and in Tag Manager's data layer, CompleteRegistration for the Meta pixel). Pasted code keeps only its ID. - `analytics.googleAnalytics` (body, string): Google Analytics 4's measurement ID, like G-XXXXXXXXXX, or the code Google gives to paste. Empty removes it. - `analytics.googleTagManager` (body, string): Google Tag Manager's container ID, like GTM-XXXXXXX, or its code. On the shared domain, only Google's own tags in the container run; on the newsletter's own domain (set_website_domain), all of them. - `analytics.metaPixel` (body, string): The Meta pixel's ID, the digits under its name in Meta Events Manager, or its code. - `customCode` (body, string): HTML for the head of every page of the newsletter's website, like another tool's code: