SendHQ

Send and receive email from your verified domains; manage domains, templates and deliverability.

Documentation

SendHQ MCP server

Give an AI agent full, safe control of one SendHQ workspace through 59 strictly typed MCP tools. Local stdio server: sendhq mcp.

curl -fsSL https://downloads.sendhq.cc/install.sh | sh
claude mcp add sendhq --env SENDHQ_API_KEY=re_your_key -- sendhq mcp

What this server is

The SendHQ MCP server lets an AI agent operate one SendHQ workspace through the Model Context Protocol: send email (single, batch, templated, replies, attachments, idempotent retries), read and search sent and received mail (subjects, bodies, and attachment names) and its delivery events, organize mail into labels with auto-filing rules, manage drafts and private attachments, author and publish hosted templates, add and verify domains and their DNS, set up inbound receiving and inbound addresses, inspect deliverability, bounces, complaints, and suppressions, and read account usage, billing state, analytics, and API-key metadata.

It is a local stdio server built into the sendhq CLI binary. Your MCP client starts sendhq mcp as a child process and speaks JSON-RPC over stdin/stdout. Every tool call becomes one documented request to the SendHQ REST API at https://sendhq.cc/api/v1 authenticated with your workspace API key, so the MCP server has exactly the permissions of that key and no more.

  • 59 tools in 8 groups, generated from one catalog that is also published as tools.json.
  • Strict JSON Schemas: unknown arguments, wrong types, and missing required fields are rejected locally before anything reaches SendHQ.
  • Structured errors with a stable code, the HTTP status, an explanation, a concrete remedy, and whether retrying can help.
  • Every tool that sends real email or destroys data says so in the first words of its description and carries MCP safety annotations.
  • --read-only mode hides every sending and mutating tool.
  • Nothing is logged. stdout carries only protocol messages; the API key and message content never reach a log.

Not the documentation MCP endpoint. SendHQ also hosts a small read-only documentation MCP endpoint at https://sendhq.cc/api/mcp (pricing and docs lookup, no account access). The server on this page is the full, account-scoped one; it runs locally or as the hosted connector below.

Use SendHQ in Claude and ChatGPT

No install needed: SendHQ also runs this server as a hosted connector at https://mcp.sendhq.cc/mcp with the same tools. You sign in with your SendHQ account instead of pasting a key.

Claude

  1. Open Settings → Connectors and find SendHQ in the directory, or choose Add custom connector and paste https://mcp.sendhq.cc/mcp.
  2. Click Connect, sign in to SendHQ, review the access and click Allow.
  3. Ask Claude to check your inbox, send an email from your verified domain, or explain a bounce.

ChatGPT

  1. Open Settings → Security and login and turn on Developer mode.
  2. Go to chatgpt.com/plugins, click Create MCP app, name it SendHQ and enter https://mcp.sendhq.cc/mcp.
  3. Sign in to SendHQ and click Allow, then pick SendHQ from the tools menu in a new chat.

Muse by Meta

In Muse, open Connectors and search for SendHQ. Click Connect, sign in to SendHQ and click Allow.

Approval and disconnecting

  • The request_feature tool sends a feature request to the SendHQ team with your account details, so we can follow up by email.
  • Tools that send real email or delete data are labelled as such. Whether the assistant asks you first is set per tool in the assistant: in Claude, choose Needs approval for those tools under Settings → Connectors → SendHQ.
  • The connector gets its own API key, named after the assistant (for example “Claude (AI connector)”). Delete it under API Keys to disconnect immediately.
  • It cannot create or revoke API keys or change billing. Attachments are sent and returned as base64; there is no local file access.
  • Unpaid workspaces (integration trial) can deliver only to the account email or an AWS SES simulator address.

Questions: postmaster@sendhq.cc. Privacy: sendhq.cc/privacy.

Install

Install the sendhq binary (Linux, macOS, and Windows on x86-64 and arm64). The installer verifies the release checksum and puts the binary in ~/.local/bin by default.

macOS and Linux:

curl -fsSL https://downloads.sendhq.cc/install.sh | sh

Windows PowerShell:

irm https://downloads.sendhq.cc/install.ps1 | iex

Check the install:

sendhq version
SENDHQ_API_KEY=re_your_key sendhq doctor

Create an API key in the dashboard at https://sendhq.cc/app#/keys. The MCP server cannot create keys. The one command that runs the server is:

Run the stdio server:

SENDHQ_API_KEY=re_your_key sendhq mcp

You normally never run that by hand: the MCP client launches it. When run in a terminal it waits for JSON-RPC on stdin.

Configure your client

Claude Code

claude mcp add:

claude mcp add sendhq --env SENDHQ_API_KEY=re_your_key -- sendhq mcp

# read-only variant
claude mcp add sendhq-readonly --env SENDHQ_API_KEY=re_your_key -- sendhq mcp --read-only

Add --scope user to make it available in every project, or --scope project to write it to the project's .mcp.json. For a shared .mcp.json, reference the key from the environment instead of committing it; Claude Code expands ${VAR} in .mcp.json.

.mcp.json:

{
  "mcpServers": {
    "sendhq": {
      "command": "sendhq",
      "args": [
        "mcp"
      ],
      "env": {
        "SENDHQ_API_KEY": "${SENDHQ_API_KEY}"
      }
    }
  }
}

OpenAI Codex

~/.codex/config.toml:

[mcp_servers.sendhq]
command = "sendhq"
args = ["mcp"]
env = { SENDHQ_API_KEY = "re_your_key" }

Or from the command line: codex mcp add sendhq --env SENDHQ_API_KEY=re_your_key -- sendhq mcp.

Claude Desktop

Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\) and restart the app. Desktop apps do not inherit your shell PATH, so use the absolute path of the binary (which sendhq).

claude_desktop_config.json:

{
  "mcpServers": {
    "sendhq": {
      "command": "/Users/you/.local/bin/sendhq",
      "args": [
        "mcp"
      ],
      "env": {
        "SENDHQ_API_KEY": "re_your_key"
      }
    }
  }
}

Any other MCP client

Configure a stdio server with command sendhq, arguments ["mcp"] (optionally "--read-only"), and the environment variables below. The server supports MCP protocol versions 2024-11-05, 2025-03-26, 2025-06-18, and 2025-11-25, and implements initialize, ping, tools/list, and tools/call. Tool results carry both a JSON text block and structuredContent.

Raw stdio smoke test (pipe into sendhq mcp):

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_service_health","arguments":{}}}

There is no hosted HTTP transport for the account-scoped server. A remote, write-capable MCP endpoint would need per-user OAuth, which SendHQ does not offer; the local binary keeps the key on the machine that already holds it.

Environment and flags

Variable or flagRequiredMeaning
SENDHQ_API_KEYyesWorkspace API key (re_…). Every tool except get_service_health needs it. Without it the server still starts and every call returns a structured auth_error explaining how to fix it.
SENDHQ_API_BASE_URLnoAPI base URL. Default https://sendhq.cc/api/v1. Use it only for a local or staging deployment. SENDHQ_BASE_URL is accepted as an older alias.
SENDHQ_MCP_READ_ONLYno1, true, or yes behaves like --read-only.
--read-onlynoExpose only tools that neither send email nor change state. Hidden tools are also refused if called by name.
SENDHQ_PROFILE / --profilenoUse a key stored by sendhq auth login in the OS keyring instead of SENDHQ_API_KEY. The environment variable wins when both exist.

The key is sent only as the Authorization: Bearer header to the configured base URL. It is never printed, logged, echoed in errors, or included in tool results.

Safety model for agents

  • Sends real email. send_email, send_batch, and send_template_test deliver mail to real people and consume delivery credits. Their descriptions start with SENDS REAL EMAIL. Call them only when the user explicitly asked for that specific message to be sent, with the recipients, sender, and content confirmed.
  • Destructive. delete_email, delete_draft, delete_attachment, delete_domain, delete_inbox, and remove_suppression are marked destructiveHint: true and their descriptions start with DESTRUCTIVE. Confirm with the user first. remove_suppression weakens a safety block and is appropriate only when a human confirms the address works again.
  • Changes state. Creating or updating drafts, templates, domains, and inboxes, publishing templates, and starting verification change the workspace but do not send mail.
  • Read-only. Everything else is readOnlyHint: true and safe to call freely.
  • DNS is never changed by this server. add_domain returns records for a human to publish; get_domain_connect_link returns a consent URL that a person must open and approve at their DNS provider.
  • Billing is never changed by this server. get_account reads plan, usage, and subscription state only.
  • Unpaid workspaces (integration trial) can deliver only to the account owner's email (get_account → user.email) or an AWS SES simulator address such as success@simulator.amazonses.com, and cannot send attachments.
  • Accepted is not delivered. A successful send returns an ID; delivery, bounce, and complaint evidence arrives later in list_email_events. Never claim inbox placement or that a person read a message.
  • Do not rotate to a different From address to get around a 423 pause, and never re-add unsubscribed or complained recipients.

API keys are out of scope

By design there are no tools that create, modify, rotate, revoke, or delete API keys. An agent must not mint or destroy credentials. list_api_keys returns only names, non-secret prefixes, and last-used times. Key management stays in the dashboard with a signed-in human.

Workflows

1. First send

  1. get_service_health confirms the API is reachable (works without a key).
  2. get_account shows the plan (access.tier), remaining quota, and user.email. On the trial, that email is the only real recipient allowed.
  3. list_sending_identities lists From addresses you can use. If it is empty, do the domain workflow first.
  4. Confirm sender, recipient, subject, and body with the user, then send_email with an idempotency_key.
  5. list_email_events with the returned id shows delivery, bounce, complaint, or reject once the provider reports it (usually seconds to minutes).

First send:

{
  "name": "send_email",
  "arguments": {
    "from": "Acme <hello@example.com>",
    "to": [
      "owner@example.com"
    ],
    "subject": "SendHQ is connected",
    "text": "It works.",
    "idempotency_key": "first-send-2026-09-26"
  }
}

2. Domain verification end to end

  1. add_domain with name: "example.com". The result includes the DNS records (DKIM CNAMEs, SES verification, SPF, recommended DMARC).
  2. get_dns_provider with the domain_id detects the authoritative DNS provider and returns the exact relative host to enter for each record at that provider.
  3. If providers.domainConnect.available is true, get_domain_connect_link returns a consent URL. Give it to the human; nothing changes until they approve at the provider. Otherwise, give the human the records to publish. Never publish a second SPF record: merge include:amazonses.com into the existing v=spf1 value.
  4. verify_domain rechecks DNS and SES. Status moves through pending, checking, and propagating to verified. Poll verify_domain or get_domain every 30–60 seconds; DNS can take minutes to hours.
  5. When status is verified, the domain's addresses appear in list_sending_identities.

3. Bounces, complaints, and suppressions

  1. list_blocked_recipients returns every blocked address with its reason (bounce, complaint, unsubscribe) and a summary count.
  2. list_suppressions returns hard-bounce and complaint suppressions; deliverability_stats gives 30-day delivery, bounce, and complaint rates; list_sender_reputation shows which From addresses are throttled or paused.
  3. A send containing a suppressed recipient fails with 422 recipient_suppressed. Remove that recipient and send again.
  4. Only when a human confirms a bounced mailbox now works, call remove_suppression. Complaint suppressions are permanent (409 complaint_suppression_locked).

4. Receive inbound email

  1. The domain (often a subdomain such as inbound.example.com) must be verified.
  2. setup_inbound provisions receiving and returns one MX record. A human publishes it.
  3. verify_inbound until status is ready.
  4. create_inbox with domain_id and local_part (for example support) creates support@inbound.example.com.
  5. Poll list_emails with direction: "in" and unread: true (optionally inbox_id). Read a message with get_email, its conversation with get_thread, attachments with download_attachment, and mark it handled with mark_email (read: true).
  6. Reply in-thread with send_email and reply_to_email_id; SendHQ sets In-Reply-To, References, and the thread.

5. Webhooks and event notifications

SendHQ does not currently offer customer-configurable webhooks, so there is no webhook tool. Provider notifications are processed inside SendHQ and exposed through reads. Poll instead: list_email_events for one message's outcome, list_emails with status (for example bounced) or after for recent changes, list_emails with direction: "in" and unread: true for new inbound mail, and list_blocked_recipients for new suppressions. Poll no more than about once a minute per question.

6. Diagnose a delivery failure

  1. Find the message: list_emails with direction: "out" and to or query, or get_email if you have the ID. status: failed means SendHQ or the provider rejected it at submission; the email's error explains why.
  2. list_email_events: bounce (permanent or transient, with the provider diagnostic), complaint, reject, or delivery. No events yet means the provider has not reported; wait and check again.
  3. If the send call itself failed, read the error code: sender_domain_unverified → finish domain verification; recipient_suppressed → the address hard-bounced or complained before; sender_paused → inspect list_sender_reputation and fix the list source; trial_recipient_restricted → trial limits; quota_exhausted → get_account usage.
  4. get_domain checks that DKIM, SPF, and DMARC are still published; deliverability_stats shows whether the problem is one message or a trend.
  5. Report what the evidence shows. A delivery event means the recipient's server accepted the message, not that it reached the inbox or was read.

7. Own a task bucket (labels)

  1. create_label with name (for example Agent/Orders) and skip_inbox: true. That makes the label a bucket: received mail that gets it is archived, so it appears only in the label, never the human's Inbox.
  2. Send task mail with send_email (or send_batch) and labels: ["Agent/Orders"]. Replies to that conversation inherit the label automatically and skip the Inbox.
  3. For mail that starts outside your conversations, add a filing rule: create_label_rule with inbox_id (a dedicated address such as orders@…), from, to, or subject. Pass apply_to_existing: true to file mail already received.
  4. Work the bucket: list_emails with label: "Agent/Orders", direction: "in", and unread: true; read with get_email or get_thread, reply with send_email and reply_to_email_id, and mark_email read: true when handled.
  5. Move a stray message in or out with label_email (add / remove). Adding a bucket label to a received message also archives it.
  6. Optionally set_inbox_forwarding sends a copy of everything a receiving address gets to another mailbox (the target confirms by email first).

Send into a bucket:

{
  "name": "send_email",
  "arguments": {
    "from": "Orders <orders@example.com>",
    "to": [
      "customer@example.net"
    ],
    "subject": "Order 1042: confirm delivery window",
    "text": "Reply with a time that works.",
    "labels": [
      "Agent/Orders"
    ],
    "idempotency_key": "order-1042-window"
  }
}

8. Attachments and templates

Attach up to 10 files with send_email attachments (each needs content_base64 or a local file_path; filename defaults to the file's basename) on a paid plan. For hosted templates: create_template → update_template_draft → render_template to preview with sample data → send_template_test (sends one real test) → publish_template, then send with send_email or send_batch using template: {key, data} and exactly one to recipient.

Results, pagination, and errors

A successful call returns the API's JSON object as structuredContent and as a JSON text block. Every list_* tool accepts limit (1–200, default 50) and offset, and adds a pagination object. Keep calling with offset: pagination.next_offset while has_more is true.

Paginated result:

{
  "data": [
    "…"
  ],
  "count": 50,
  "pagination": {
    "offset": 0,
    "limit": 50,
    "returned": 50,
    "total": 180,
    "has_more": true,
    "next_offset": 50
  }
}

A failed call returns isError: true with a structured error. Follow remedy instead of retrying blindly; only retry when retryable is true.

Structured tool error:

{
  "error": {
    "code": "trial_recipient_restricted",
    "status": 402,
    "message": "The integration trial can deliver only to your account email or an AWS SES simulator address",
    "retryable": false,
    "explanation": "This workspace is on the unpaid integration trial. Trial sends can be delivered only to the account owner's email address or an AWS SES simulator address.",
    "remedy": "Send to the account email (get_account -> user.email) or a simulator address such as success@simulator.amazonses.com to test. To email anyone else, the account owner must activate a paid plan in the dashboard (Profile & Billing). Do not retry the same recipients."
  }
}

Optional error fields: request_id (quote it to support), retry_after_seconds, problems (list of schema violations for invalid_arguments), and idempotent_replayed (see Idempotency).

Idempotency

send_email and send_batch accept idempotency_key (max 200 characters), sent as the Idempotency-Key header. Generate one stable key per logical message, for example invoice-4812-receipt.

  • A retry must reuse the same key AND an identical request body. Same key with any change (recipient, subject, body, header, template data, even argument values) returns 409 idempotency_conflict.
  • Same key, same body, original finished: SendHQ returns the stored result without sending again. This is how you safely retry after a timeout or network_error.
  • Same key while the original is still running: 409 idempotency_in_progress, retryable after a short wait.
  • A new logical message needs a new key.
  • Stored failures replay too. If the first attempt failed, retrying with the same key returns that same failure with idempotent_replayed: true and retryable: false. Check list_emails (direction: out) to confirm nothing went out, fix the cause, then send with a new key.
  • The server never retries a POST on its own. Only read-only GET calls are retried automatically (up to 3 attempts on network errors, 429, and 5xx).
  • send_email with inline attachments cannot take an idempotency_key, because it runs several requests. For retry-safe attachment sends: create_draft → upload_attachment → send_email with draft_id and idempotency_key.

Retry-safe send (repeat exactly on timeout):

{
  "name": "send_email",
  "arguments": {
    "from": "Acme <billing@example.com>",
    "to": [
      "owner@example.com"
    ],
    "subject": "Receipt #4812",
    "text": "Thanks for your payment.",
    "idempotency_key": "receipt-4812"
  }
}

Rate limits and quotas

SendHQ does not publish a fixed requests-per-second limit for the API. The limits an agent actually meets are usage limits, returned as 429:

  • Monthly recipient deliveries per plan. Each To, Cc, and Bcc address counts as one delivery. See get_account → usage.recipientDeliveries vs usage.emailQuotaMonth.
  • Daily recipients per exact From address, set by that sender's reputation state (list_sender_reputation → dailyLimit, 2,000 by default on paid plans).
  • Integration trial: 100 recipients in total, only to the account email or SES simulator addresses.
  • Attachments: at most 10 files and 10 MB per message; 10 GB of recipient-weighted attachment transfer per month on paid plans.
  • Per request: To + Cc + Bcc up to 100 addresses; send_batch up to 100 messages.
  • Reputation circuit breaker: in a rolling 7-day window, bounces or complaints above threshold throttle or pause one From address (423 sender_paused). It recovers automatically once rates fall.

quota_exhausted is not retryable until the period resets or the plan changes. rate_limited is retryable after retry_after_seconds; for sends, retry with the same idempotency_key and identical body.

Error catalogue

code is stable; branch on it rather than on message.

codeHTTPRetry?What it means and what to do
invalid_arguments—noArguments failed the tool's JSON Schema locally; nothing reached SendHQ. Fix the fields listed in problems.
auth_error401noMissing, revoked, or wrong API key. Set SENDHQ_API_KEY for the server process; a human creates keys in the dashboard.
trial_recipient_restricted402noIntegration trial can deliver only to the account email or an SES simulator address. Send there, or the owner activates a paid plan.
payment_required402noFeature needs a paid plan (for example attachments). Send without it or upgrade.
sender_domain_not_owned403noFrom domain is not in this workspace. Use list_sending_identities or add_domain.
sender_domain_unverified403noFrom domain is not verified yet. get_domain, publish missing records, verify_domain.
domain_limit_reached403noPlan domain limit reached. Remove an unused domain (with approval) or upgrade.
marketing_not_enabled403noMarketing class is not enabled for this domain or plan. Use transactional only if the message genuinely is.
forbidden403noPolicy does not allow the operation. Adjust the request.
not_found404noID is not in this workspace. List the resource to find the right ID; restore archived templates first.
idempotency_conflict409noKey reused with a different body. Resend the exact original, or use a new key for a new message.
idempotency_in_progress409yesOriginal request still running. Wait, then retry with the same key and body.
revision_conflict409noTemplate draft changed since you read it. get_template, merge, save again.
complaint_suppression_locked409noRecipient complained. Never email them again.
inbound_not_ready409noInbound receiving not ready. setup_inbound, publish MX, verify_inbound.
conflict409noResource already exists or is in the wrong state. Read it and adjust.
attachments_too_large413noMore than 10 files or 10 MB. Remove or shrink attachments.
recipient_suppressed422noA recipient hard-bounced or complained before. Remove them; see list_blocked_recipients.
recipient_unsubscribed422noA recipient opted out of marketing mail. Remove them permanently.
validation_failed422noContent rejected, for example template data that breaks the variable contract. Fix the input.
sender_paused423noThis From address is paused by the 7-day bounce/complaint circuit breaker. Stop, fix the list, wait for automatic recovery.
quota_exhausted429noMonthly, daily per-sender, attachment, or trial limit reached. Check get_account; wait for reset or upgrade.
rate_limited429yesSlow down; wait retry_after_seconds. Sends: same key, same body.
server_error5xxyesTemporary SendHQ or provider failure. Back off and retry; sends with the same key and body. If idempotent_replayed is true, use a new key after confirming nothing was sent.
network_error—yesRequest or response lost. Retry; for sends the same idempotency_key makes that safe.
invalid_request400noMalformed request. Read message and correct it.
tool_error—noLocal failure inside the MCP server (for example an unreadable file_path). Read message.

Tool reference

Every tool with its safety class, the REST endpoint it calls, its parameters, return shape, and an example tools/call params object. Parameters are exact: the server rejects anything not listed.

Emails and threads

send_email

Send one email · Sends real email · POST /emails

SENDS REAL EMAIL. Send one message from a verified domain: raw html/text, a published hosted template, a reply in an existing thread, or a message with attachments. Pass idempotency_key so a retry cannot send twice; a retry must reuse the same key AND an identical request, otherwise SendHQ returns 409. attachments is a convenience that creates a draft, uploads each file, and sends with that draft; it cannot be combined with idempotency_key or draft_id (use create_draft + upload_attachment + send_email with draft_id for retry-safe attachment sends). Unpaid workspaces (integration trial) can deliver only to the account email or an AWS SES simulator address, and cannot send attachments.

Provide at least one of: html, text, template.

ParameterTypeRequiredDescription
fromstringyesSender, e.g. Acme <hello@example.com>. The domain must be verified in this workspace (see list_sending_identities). (max 998 chars)
tostring[]yesRecipients. Each entry is an address, optionally with a display name. To+cc+bcc may total at most 100; every destination consumes one delivery credit. (1–100 items)
ccstring[]noCarbon-copy recipients. (0–100 items)
bccstring[]noBlind-copy recipients. (0–100 items)
subjectstringnoSubject line. Omit when sending a template. (max 998 chars)
textstringnoPlain-text body. Provide text, html, or template.
htmlstringnoHTML body. SendHQ sanitizes it and derives text when text is omitted.
reply_tostringnoReply-To address.
headersobjectnoExtra safe custom headers (string values), e.g. {"X-Entity-Ref-ID": "123"}. Routing headers such as From/To/Message-ID are controlled by SendHQ.
message_classstringnotransactional (default) or marketing. Marketing requires a marketing-enabled plan or domain and adds unsubscribe handling. (one of transactional, marketing)
reply_to_email_idstringnoReply inside an existing conversation: the em_… ID of the message being answered. SendHQ sets In-Reply-To/References and the thread.
thread_idstringnoExplicit thread ID to file the message under.
draft_idstringnoSend a stored draft's attachments with this message (dr_…). The draft is deleted after a successful send.
templateobjectnoSend a published hosted template instead of raw html/text. Requires exactly one to recipient and no cc/bcc; the template supplies the subject. Provide at least one of: id, key.
template.idstringnoTemplate ID (tmpl_…). Provide id or key.
template.keystringnoTemplate key such as account-welcome. Provide id or key.
template.version_idstringnoOptional published release ID (tmplv_…). Defaults to the current published release.
template.dataobjectnoValues for the template's typed variables.
labelsstring[]noLabel names or lbl_… IDs to file this message under. Unknown names are created. Replies in the conversation inherit the labels, and a bucket label (skip_inbox) keeps those replies out of the Inbox. Max 10. (0–10 items)
idempotency_keystringnoIdempotency-Key header (max 200 chars). Reuse it only to retry this exact request. (max 200 chars)
attachmentsobject[]noFiles to attach (max 10 files, 10 MB total). Each needs content_base64 (plus filename) or a local file_path. (0–10 items) Provide at least one of: content_base64, file_path.
attachments[].filenamestringnoFile name shown to the recipient. Required with content_base64; defaults to the basename of file_path. (max 255 chars)
attachments[].content_typestringnoMIME type, e.g. application/pdf. Defaults to application/octet-stream.
attachments[].content_base64stringnoStandard base64 file content.
attachments[].file_pathstringnoAbsolute path of a local file readable by the MCP server process.

Returns: {id: em_…, providerMessageId, threadId, templateId, templateVersionId, isTest}. Acceptance is not delivery: follow up with list_email_events.

Example:

{
  "name": "send_email",
  "arguments": {
    "from": "Acme <hello@example.com>",
    "to": [
      "owner@example.com"
    ],
    "subject": "Your export is ready",
    "text": "Download it from your dashboard.",
    "idempotency_key": "export-ready-42"
  }
}

send_batch

Send a batch of individualized emails · Sends real email · POST /emails/batch

SENDS REAL EMAIL. Send 1–100 independent messages in one request (use this for per-recipient template personalization). Each item has the same shape as send_email (without attachments/idempotency_key). Items succeed or fail individually: HTTP 207 means partial success; inspect each data[i].ok and data[i].error. One idempotency_key covers the whole batch body.

ParameterTypeRequiredDescription
emailsobject[]yesMessages to send. (1–100 items) Provide at least one of: html, text, template.
idempotency_keystringnoIdempotency-Key for the entire batch (max 200 chars). (max 200 chars)

Returns: {data: [{index, ok, id?, error?: {message, status}}], count, successful, failed}.

Example:

{
  "name": "send_batch",
  "arguments": {
    "emails": [
      {
        "from": "Acme <hello@example.com>",
        "to": [
          "owner@example.com"
        ],
        "template": {
          "key": "account-welcome",
          "data": {
            "first_name": "Asha"
          }
        }
      }
    ],
    "idempotency_key": "welcome-batch-2026-09-26"
  }
}

list_emails

List and search email · Read-only · GET /emails

List sent (direction: out) and received (direction: in) email newest-first with filters. Received mail is classified: read the human inbox with direction: in, archived: false, category: primary; triage with important: true; spam is hidden unless category: spam or include_spam: true. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
directionstringnoin for received, out for sent. (one of in, out)
statusstringnoStatus filter, e.g. queued, sent, delivered, bounced, complained, failed.
domainstringnoOnly messages for this domain, or a comma-separated list of domains (matches any).
inbox_idstringnoOnly messages received by this inbox (inb_…).
labelstringnoOnly messages carrying this label: a label ID lbl_… or exact name, or a comma-separated list (matches any). Use list_labels to see folders.
archivedbooleannofalse = the Inbox view (received mail not archived), true = archived only. Omit for all mail.
categorystringnoprimary (people), updates (newsletters, bulk, automated), or spam; or a comma-separated list. Spam is hidden unless requested.
importantbooleannotrue = only messages flagged important (replies to conversations you started, and senders marked important).
include_spambooleannoInclude spam in the results (for searches across every folder).
fromstringnoSender address contains this value.
tostringnoRecipient address contains this value.
unreadbooleannotrue = unread only, false = read only.
afterstringnoISO-8601 timestamp; only messages created after it. (date-time)
beforestringnoISO-8601 timestamp; only messages created before it. (date-time)
querystringnoFree-text search over subjects, bodies, sender/recipient addresses, and attachment filenames. (max 200 chars)
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [email summaries], count, pagination}.

Example:

{
  "name": "list_emails",
  "arguments": {
    "direction": "in",
    "unread": true,
    "limit": 25
  }
}

get_email

Get one email · Read-only · GET /emails/:email_id

Retrieve one message with headers, html/text body, status, thread metadata, and attachment metadata (download bytes with download_attachment).

ParameterTypeRequiredDescription
email_idstringyesEmail ID (starts with em_), as returned by a list or create tool. (max 128 chars)

Returns: Email object: {id, direction, status, from, to, cc, bcc, subject, html, text, threadId, messageId, providerMessageId, readAt, createdAt, attachments: [{id, filename, contentType, sizeBytes, available}]}.

Example:

{
  "name": "get_email",
  "arguments": {
    "email_id": "em_123"
  }
}

mark_email

Mark read, archived, spam, or important · Changes state · PATCH /emails/:email_id

Update one message: read, archived, category (primary, updates, spam; received mail only), and important. Reporting spam or marking important teaches SendHQ about that sender for future mail; pass learn: false to change only this message. Pass at least one field.

ParameterTypeRequiredDescription
email_idstringyesEmail ID (starts with em_), as returned by a list or create tool. (max 128 chars)
readbooleannotrue = read, false = unread.
archivedbooleannotrue = archive (skip the Inbox), false = move back to the Inbox.
categorystringnoMove a received message to primary, updates, or spam. (one of primary, updates, spam)
importantbooleannoFlag or unflag the message as important.
learnbooleannofalse = do not remember this verdict for the sender (default true).

Returns: The updated email object.

Example:

{
  "name": "mark_email",
  "arguments": {
    "email_id": "em_123",
    "read": true
  }
}

delete_email

Delete an email · Destructive · DELETE /emails/:email_id

DESTRUCTIVE: permanently delete a retained message and its stored attachments from SendHQ. It does not recall a message that was already delivered.

ParameterTypeRequiredDescription
email_idstringyesEmail ID (starts with em_), as returned by a list or create tool. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_email",
  "arguments": {
    "email_id": "em_123"
  }
}

list_email_events

List delivery events for an email · Read-only · GET /emails/:email_id/events

Provider events for one sent message: delivery, bounce, complaint, reject, open, click. This is the evidence for whether a message was delivered or why it failed. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
email_idstringyesEmail ID (starts with em_), as returned by a list or create tool. (max 128 chars)
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{event_type, recipient, reason, created_at, …}], count, pagination}.

Example:

{
  "name": "list_email_events",
  "arguments": {
    "email_id": "em_123"
  }
}

get_thread

Get a conversation · Read-only · GET /threads/:thread_id

Retrieve every message in a conversation in chronological order (sent and received), each with attachment metadata.

ParameterTypeRequiredDescription
thread_idstringyesThread ID (usually the em_… ID of the first message; see threadId on any email). (max 128 chars)

Returns: {id, subject, data: [emails]}.

Example:

{
  "name": "get_thread",
  "arguments": {
    "thread_id": "em_123"
  }
}

Labels and auto-filing rules

list_labels

List labels · Read-only · GET /labels

List the workspace's labels (folders) with total and unread counts and their auto-filing rules. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{id, name, color, totalCount, unreadCount, rules: [...]}], count, pagination}.

Example:

{
  "name": "list_labels",
  "arguments": {}
}

get_label

Get a label · Read-only · GET /labels/:label_id

Retrieve one label with counts and auto-filing rules.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)

Returns: Label object.

Example:

{
  "name": "get_label",
  "arguments": {
    "label_id": "Billing"
  }
}

create_label

Create a label · Changes state · POST /labels

Create a folder-style label. Set skip_inbox: true to make it a bucket an agent owns: send with labels: [name] and the replies are filed into the label and kept out of the Inbox. Optional auto-filing rules file new sent/received mail (every condition on a rule must match). Set apply_to_existing to also file retained mail.

ParameterTypeRequiredDescription
namestringyesLabel name, e.g. Billing or Clients/Acme. Unique per workspace (case-insensitive). (max 64 chars)
colorstringnoHex color such as #1a73e8. Optional.
skip_inboxbooleannoBucket mode: received mail that gets this label (by rule, by replying to a conversation sent with this label, or by hand) is archived so it appears only in the label, not the Inbox.
rulesobject[]noOptional auto-filing rules (max 20). Each needs at least one of inbox_id, from, to, subject. (0–20 items)
rules[].directionstringnoOnly in (received) or out (sent) mail. Omit for both. (one of in, out)
rules[].inbox_idstringnoOnly mail received by this inbox (inb_…). Files each receiving address into its own folder.
rules[].fromstringnoSender contains this text (case-insensitive), e.g. @stripe.com. (max 200 chars)
rules[].tostringnoTo/Cc contains this text (case-insensitive). (max 200 chars)
rules[].subjectstringnoSubject contains this text (case-insensitive). (max 200 chars)
rules[].skip_inboxbooleannoArchive matching received mail so it appears only in the label folder, not the Inbox.
apply_to_existingbooleannoAlso file already-retained mail that matches the rules.

Returns: The created label with rules.

Example:

{
  "name": "create_label",
  "arguments": {
    "name": "Agent/Orders",
    "skip_inbox": true,
    "rules": [
      {
        "from": "@stripe.com"
      }
    ]
  }
}

update_label

Rename, recolor, or bucket a label · Changes state · PATCH /labels/:label_id

Rename a label, change its color, or toggle bucket mode (skip_inbox). Turning bucket mode on archives received mail already in the label.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)
namestringnoNew name. (max 64 chars)
colorstringnoNew hex color.
skip_inboxbooleannoBucket mode: received mail that gets this label (by rule, by replying to a conversation sent with this label, or by hand) is archived so it appears only in the label, not the Inbox.

Returns: Updated label.

Example:

{
  "name": "update_label",
  "arguments": {
    "label_id": "lbl_123",
    "name": "Finance/Billing"
  }
}

delete_label

Delete a label · Destructive · DELETE /labels/:label_id

DESTRUCTIVE: delete a label and its rules. The email itself is kept; it only loses this label.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_label",
  "arguments": {
    "label_id": "lbl_123"
  }
}

create_label_rule

Add an auto-filing rule · Changes state · POST /labels/:label_id/rules

Add a rule to a label so matching new mail is filed automatically. Every condition you set must match. Use inbox_id to give a receiving address its own folder; add skip_inbox to keep it out of the Inbox.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)
directionstringnoOnly in (received) or out (sent) mail. Omit for both. (one of in, out)
inbox_idstringnoOnly mail received by this inbox (inb_…). Files each receiving address into its own folder.
fromstringnoSender contains this text (case-insensitive), e.g. @stripe.com. (max 200 chars)
tostringnoTo/Cc contains this text (case-insensitive). (max 200 chars)
subjectstringnoSubject contains this text (case-insensitive). (max 200 chars)
skip_inboxbooleannoArchive matching received mail so it appears only in the label folder, not the Inbox.
apply_to_existingbooleannoAlso file already-retained mail that matches.

Returns: {id: lrule_…, labelId, direction, inboxId, from, to, subject, skipInbox}.

Example:

{
  "name": "create_label_rule",
  "arguments": {
    "label_id": "Billing",
    "inbox_id": "inb_123",
    "skip_inbox": true
  }
}

delete_label_rule

Delete an auto-filing rule · Destructive · DELETE /labels/:label_id/rules/:rule_id

DESTRUCTIVE: remove one auto-filing rule. Mail already filed keeps its label.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)
rule_idstringyesRule ID (starts with lrule_), from get_label. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_label_rule",
  "arguments": {
    "label_id": "lbl_123",
    "rule_id": "lrule_123"
  }
}

label_email

Add or remove labels on an email · Changes state · POST /emails/:email_id/labels

Move a message between folders: add and/or remove labels by name or lbl_… ID. Unknown names in add are created unless create is false.

ParameterTypeRequiredDescription
email_idstringyesEmail ID (starts with em_), as returned by a list or create tool. (max 128 chars)
addstring[]noLabels to add. (0–10 items)
removestring[]noLabels to remove. (0–10 items)
createbooleannoCreate unknown labels in add (default true).

Returns: The updated email with labels.

Example:

{
  "name": "label_email",
  "arguments": {
    "email_id": "em_123",
    "add": [
      "Billing"
    ],
    "remove": [
      "Support"
    ]
  }
}

Drafts, attachments, and sender identities

list_sending_identities

List verified sender identities · Read-only · GET /sending-identities

Addresses and domains this workspace can send from right now (verified domains, their default From, and active inbox addresses). Call before send_email to pick a valid from.

No parameters.

Returns: {domains: [verified domain names], addresses: [sender addresses], localParts: [...]}.

Example:

{
  "name": "list_sending_identities",
  "arguments": {}
}

create_draft

Create a draft · Changes state · POST /drafts

Create a composer draft. Drafts hold attachments: create a draft, upload_attachment, then send_email with draft_id. Does not send anything.

ParameterTypeRequiredDescription
fromstringnoSender address on a verified domain (may be empty while drafting).
tostring[]noRecipients. (0–100 items)
ccstring[]noCarbon-copy recipients. (0–100 items)
bccstring[]noBlind-copy recipients. (0–100 items)
subjectstringnoSubject line. (max 998 chars)
htmlstringnoHTML body.
textstringnoPlain-text body.
reply_to_email_idstringnoEmail ID this draft replies to.
thread_idstringnoThread ID this draft belongs to.

Returns: Draft object {id: dr_…, from, to, cc, bcc, subject, html, text, attachments: []}.

Example:

{
  "name": "create_draft",
  "arguments": {
    "from": "hello@example.com",
    "to": [
      "owner@example.com"
    ],
    "subject": "Invoice"
  }
}

list_drafts

List drafts · Read-only · GET /drafts

List composer drafts, most recently updated first. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [drafts], count, pagination}.

Example:

{
  "name": "list_drafts",
  "arguments": {}
}

get_draft

Get a draft · Read-only · GET /drafts/:draft_id

Retrieve one draft with its attachment metadata.

ParameterTypeRequiredDescription
draft_idstringyesDraft ID (starts with dr_), as returned by a list or create tool. (max 128 chars)

Returns: Draft object with attachments.

Example:

{
  "name": "get_draft",
  "arguments": {
    "draft_id": "dr_123"
  }
}

update_draft

Replace draft content · Changes state · PUT /drafts/:draft_id

Replace a draft's content and recipients. This is a full replacement: fields you omit are cleared, so read get_draft first and send every field you want to keep. Attachments are unaffected.

ParameterTypeRequiredDescription
draft_idstringyesDraft ID (starts with dr_), as returned by a list or create tool. (max 128 chars)
fromstringnoSender address on a verified domain (may be empty while drafting).
tostring[]noRecipients. (0–100 items)
ccstring[]noCarbon-copy recipients. (0–100 items)
bccstring[]noBlind-copy recipients. (0–100 items)
subjectstringnoSubject line. (max 998 chars)
htmlstringnoHTML body.
textstringnoPlain-text body.
reply_to_email_idstringnoEmail ID this draft replies to.
thread_idstringnoThread ID this draft belongs to.

Returns: Updated draft object.

Example:

{
  "name": "update_draft",
  "arguments": {
    "draft_id": "dr_123",
    "from": "hello@example.com",
    "to": [
      "owner@example.com"
    ],
    "subject": "Invoice (updated)",
    "text": "Attached."
  }
}

delete_draft

Discard a draft · Destructive · DELETE /drafts/:draft_id

DESTRUCTIVE: discard a draft and permanently delete its stored attachments.

ParameterTypeRequiredDescription
draft_idstringyesDraft ID (starts with dr_), as returned by a list or create tool. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_draft",
  "arguments": {
    "draft_id": "dr_123"
  }
}

upload_attachment

Upload an attachment to a draft · Changes state · POST /drafts/:draft_id/attachments

Upload one file to a draft (max 10 files and 10 MB total per message). Provide content_base64 or a local file_path. Attachments require a paid plan at send time.

Provide at least one of: content_base64, file_path.

ParameterTypeRequiredDescription
draft_idstringyesDraft ID (starts with dr_), as returned by a list or create tool. (max 128 chars)
filenamestringnoFile name shown to the recipient. Defaults to the basename of file_path. (max 255 chars)
content_typestringnoMIME type, e.g. application/pdf. Defaults to application/octet-stream.
content_base64stringnoStandard base64 file content.
file_pathstringnoAbsolute path of a local file readable by the MCP server process.

Returns: {id: att_…, filename, contentType, sizeBytes, available}.

Example:

{
  "name": "upload_attachment",
  "arguments": {
    "draft_id": "dr_123",
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "file_path": "/tmp/invoice.pdf"
  }
}

download_attachment

Download an attachment · Read-only · GET /attachments/:attachment_id

Download a private attachment (sent, received, or draft). Returns base64 content, or writes the file when save_to_path is set (refuses to overwrite unless overwrite is true).

ParameterTypeRequiredDescription
attachment_idstringyesAttachment ID (starts with att_), as returned by a list or create tool. (max 128 chars)
save_to_pathstringnoOptional absolute local path to write the file to instead of returning base64.
overwritebooleannoAllow replacing an existing file at save_to_path. Defaults to false.

Returns: {attachment_id, filename, content_type, size_bytes, content_base64} or {attachment_id, filename, content_type, size_bytes, saved_to}.

Example:

{
  "name": "download_attachment",
  "arguments": {
    "attachment_id": "att_123",
    "save_to_path": "/tmp/invoice.pdf"
  }
}

delete_attachment

Delete an attachment · Destructive · DELETE /attachments/:attachment_id

DESTRUCTIVE: permanently delete a stored attachment (for example, remove a file from a draft before sending).

ParameterTypeRequiredDescription
attachment_idstringyesAttachment ID (starts with att_), as returned by a list or create tool. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_attachment",
  "arguments": {
    "attachment_id": "att_123"
  }
}

Hosted templates

list_templates

List hosted templates · Read-only · GET /templates

List hosted email templates with publish state and usage. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
lifecyclestringnoactive (default), archived, or all. (one of active, archived, all)
querystringnoSearch by name or key. (max 120 chars)
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [templates], count, pagination}.

Example:

{
  "name": "list_templates",
  "arguments": {
    "lifecycle": "active"
  }
}

create_template

Create a hosted template · Changes state · POST /templates

Create a template with an editable draft, optionally from a starter (welcome, reset, receipt, or blank). Publish it before sending by key.

ParameterTypeRequiredDescription
namestringyesHuman name. (max 120 chars)
keystringnoStable send key: lowercase letters, numbers, hyphens; starts with a letter (2–64 chars). Derived from name when omitted.
starterstringnoStarter content. (one of blank, welcome, reset, receipt)

Returns: {template, draft, activeVersion, versions, usage}.

Example:

{
  "name": "create_template",
  "arguments": {
    "name": "Account welcome",
    "key": "account-welcome",
    "starter": "welcome"
  }
}

get_template

Get a template · Read-only · GET /templates/:template_id

Retrieve a template's current draft (with revision), active published release, release history, and usage. Accepts ID or key.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID (tmpl_…) or key. (max 128 chars)

Returns: {template, draft: {id, revision, subjectTemplate, htmlTemplate, textTemplate, variables, sampleData, …} | null, activeVersion, versions, usage}.

Example:

{
  "name": "get_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}

update_template_draft

Save a template draft · Changes state · PUT /templates/:template_id/draft

Save the template's editable draft using optimistic concurrency: pass the current revision from get_template (409 means someone else saved first; re-read and retry). This is a full replacement of draft content: omitted fields are cleared, so send every field you want to keep. Use {{variable}} placeholders.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)
revisionintegeryesCurrent draft revision from get_template. (1–…)
namestringnoTemplate name. (max 120 chars)
subject_templatestringnoSubject with placeholders. (max 998 chars)
preheader_templatestringnoPreview text. (max 240 chars)
html_templatestringnoHTML body with placeholders.
text_templatestringnoPlain-text body with placeholders.
fromstringnoDefault sender for sends of this template.
reply_tostringnoDefault Reply-To.
variablesobject[]noTyped variable contract. Each item: {key (lowercase/underscores), label, type: text|number|url|boolean, required (default true), fallback, description}.
variables[].keystringyes
variables[].labelstringno
variables[].typestringno(one of text, number, url, boolean)
variables[].requiredbooleanno
variables[].fallbackanyno
variables[].descriptionstringno
sample_dataobjectnoSample values used for previews and tests.

Returns: {template, draft: {revision: next}, validation: {valid, findings}}.

Example:

{
  "name": "update_template_draft",
  "arguments": {
    "template_id": "account-welcome",
    "revision": 3,
    "name": "Account welcome",
    "subject_template": "Welcome, {{first_name}}",
    "text_template": "Hi {{first_name}}",
    "variables": [
      {
        "key": "first_name",
        "type": "text",
        "required": true
      }
    ],
    "sample_data": {
      "first_name": "Asha"
    }
  }
}

create_template_draft

Start a new draft from the published release · Changes state · POST /templates/:template_id/draft

Create a new editable draft copied from the current published release (409 if a draft already exists or nothing is published).

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)

Returns: {draft}.

Example:

{
  "name": "create_template_draft",
  "arguments": {
    "template_id": "account-welcome"
  }
}

render_template

Render a template preview · Read-only · POST /templates/:template_id/render

Render the exact server output (subject, html, text) for the draft, the published release, or a specific version with the given data. Does not send. Returns 422 with findings when data violates the variable contract.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)
version_idstringnoOptional version ID; defaults to the draft, then the published release.
dataobjectnoVariable values; defaults to the version's sample data.

Returns: {subject, html, text, preheader, versionId, versionNumber, isDraft, findings}.

Example:

{
  "name": "render_template",
  "arguments": {
    "template_id": "account-welcome",
    "data": {
      "first_name": "Asha"
    }
  }
}

send_template_test

Send a template test email · Sends real email · POST /templates/:template_id/test

SENDS REAL EMAIL. Send a [Test]-prefixed snapshot of the draft (or a given version) to the given recipients. Counts against usage; trial workspaces can only send to the account email or an SES simulator address.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)
tostring[]yesTest recipients. (1–100 items)
fromstringnoSender on a verified domain; defaults to the template's From.
version_idstringnoOptional version ID.
dataobjectnoVariable values; defaults to sample data.

Returns: {id: em_…, providerMessageId, threadId, isTest: true}.

Example:

{
  "name": "send_template_test",
  "arguments": {
    "template_id": "account-welcome",
    "to": [
      "owner@example.com"
    ]
  }
}

publish_template

Publish a template release · Changes state · POST /templates/:template_id/publish

Publish the current draft as an immutable release that send_email with template.key will use. Fails with 422 findings on validation errors, or 409 if it would break the live variable contract of a template already used in production.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)

Returns: {template, published}.

Example:

{
  "name": "publish_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}

archive_template

Archive a template · Changes state · POST /templates/:template_id/archive

Stop new sends that use this template (history is kept; reversible with restore_template). Any integration sending this key will start failing with 404.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)

Returns: {template}.

Example:

{
  "name": "archive_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}

restore_template

Restore an archived template · Changes state · POST /templates/:template_id/restore

Make an archived template active again.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)

Returns: {template}.

Example:

{
  "name": "restore_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}

Domains and DNS

list_domains

List domains · Read-only · GET /domains

List sending domains with aggregate setup_status (verified | checking | pending), per-record DNS state, and inbound status. Can be slow: unverified domains are re-checked live. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [domains with records], count, pagination}.

Example:

{
  "name": "list_domains",
  "arguments": {}
}

get_domain

Get domain setup details · Read-only · GET /domains/:domain_id

Retrieve one domain with the exact DNS records to publish (type, name, value), each record's live state from two public resolvers, dns_issues with fixes, and inbound status.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {id, name, status, setup_status, dns_propagating, records: [{type, name, value, verified, dns_state}], dns_issues: [{code, message, …}], inbound_domain, inbound_status}.

Example:

{
  "name": "get_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}

add_domain

Add a sending domain · Changes state · POST /domains

Register a domain you control for sending. Returns the DNS records (SES Easy DKIM CNAMEs) the owner must publish. Does not change DNS itself. Counts against the plan's domain limit.

ParameterTypeRequiredDescription
namestringyesBare domain name, e.g. example.com or mail.example.com. (max 253 chars)
default_fromstringnoOptional default sender address on this domain.

Returns: {id: dom_…, name, status: pending, records: [...], ses: {configured}}.

Example:

{
  "name": "add_domain",
  "arguments": {
    "name": "example.com"
  }
}

verify_domain

Verify a domain · Changes state · POST /domains/:domain_id/verify

Run a live SES/DNS verification check now. Safe to repeat; poll every 30–60 s after DNS changes (propagation can take minutes to hours). Sending is allowed once status is verified.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {domain, checks: {ses, dkim, dkim_status}, status: verified|pending}.

Example:

{
  "name": "verify_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}

delete_domain

Delete a domain · Destructive · DELETE /domains/:domain_id

DESTRUCTIVE: remove the domain from the workspace, including its inbound receiving route. Sends from it fail immediately afterwards. It does not delete DNS records at your DNS provider.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}

get_dns_provider

Detect DNS provider and record hosts · Read-only · GET /dns/provider

Detect the domain's authoritative DNS provider and return the relative host to type into that provider for each record, the recommended DMARC record, inbound MX guidance, and whether one-click setup (Domain Connect) is available.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {detectionStatus, detected, zone, nameservers, recordHosts: {recordId: host}, inbound, recommendations, authentication, providers: {domainConnect: {available, providerName}}}.

Example:

{
  "name": "get_dns_provider",
  "arguments": {
    "domain_id": "dom_123"
  }
}

get_domain_connect_link

Get a one-click DNS setup link · Read-only · GET /dns/domain-connect/connect

When get_dns_provider reports providers.domainConnect.available, create a signed consent URL. Give it to the human: they open it and approve the DNS change at their provider. Nothing changes until they approve. 409 if unsupported.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {url, providerName}.

Example:

{
  "name": "get_domain_connect_link",
  "arguments": {
    "domain_id": "dom_123"
  }
}

Inbound email

setup_inbound

Enable inbound receiving for a domain · Changes state · POST /domains/:domain_id/inbound/setup

Provision SES inbound receiving for a verified domain. Uses the root domain when it has no conflicting MX, otherwise inbound.<domain>. Returns the MX record the owner must publish; it does not edit DNS.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {domain: receiving domain, status: dns_pending|ready, record: {type: MX, name, value}}.

Example:

{
  "name": "setup_inbound",
  "arguments": {
    "domain_id": "dom_123"
  }
}

verify_inbound

Verify inbound MX · Changes state · POST /domains/:domain_id/inbound/verify

Re-check the inbound MX record. Status becomes ready when both public resolvers see it.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)

Returns: {domain, status: ready|dns_pending|propagating|checking, record}.

Example:

{
  "name": "verify_inbound",
  "arguments": {
    "domain_id": "dom_123"
  }
}

list_inboxes

List inbound addresses · Read-only · GET /inboxes

List receiving addresses, optionally for one domain. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
domain_idstringnoOptional domain ID filter.
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{id, address, name, status, domainId}], count, pagination}.

Example:

{
  "name": "list_inboxes",
  "arguments": {
    "domain_id": "dom_123"
  }
}

get_inbox

Get an inbox · Read-only · GET /inboxes/:inbox_id

Retrieve one inbound address.

ParameterTypeRequiredDescription
inbox_idstringyesInbox ID (starts with inb_), as returned by a list or create tool. (max 128 chars)

Returns: Inbox object.

Example:

{
  "name": "get_inbox",
  "arguments": {
    "inbox_id": "inb_123"
  }
}

create_inbox

Create an inbound address · Changes state · POST /inboxes

Create an address such as support@<receiving domain> on a domain whose inbound status is ready (run setup_inbound and verify_inbound first). Received mail appears in list_emails with direction in.

ParameterTypeRequiredDescription
domain_idstringyesDomain ID (starts with dom_), as returned by a list or create tool. (max 128 chars)
local_partstringyesPart before @, e.g. support. (max 64 chars)
namestringnoOptional display name.

Returns: {id: inb_…, address, name, status: active}.

Example:

{
  "name": "create_inbox",
  "arguments": {
    "domain_id": "dom_123",
    "local_part": "support",
    "name": "Support"
  }
}

update_inbox

Rename, enable, or disable an inbox · Changes state · PATCH /inboxes/:inbox_id

Rename an inbox or set its status to active / disabled.

ParameterTypeRequiredDescription
inbox_idstringyesInbox ID (starts with inb_), as returned by a list or create tool. (max 128 chars)
namestringnoNew display name.
statusstringnoNew status. (one of active, disabled)

Returns: Updated inbox.

Example:

{
  "name": "update_inbox",
  "arguments": {
    "inbox_id": "inb_123",
    "status": "disabled"
  }
}

set_inbox_forwarding

Forward an inbox to another address · Sends real email · PUT /inboxes/:inbox_id/forwarding

SENDS REAL EMAIL when forwarding to someone other than the account owner: sets where an inbox's received mail is forwarded. The owner's own address activates immediately; any other address gets a confirmation email and forwarding stays pending until someone there confirms. Pass forward_to: null to turn forwarding off. Forwarded copies come from the inbox address with the original sender as Reply-To.

ParameterTypeRequiredDescription
inbox_idstringyesInbox ID (starts with inb_), as returned by a list or create tool. (max 128 chars)
forward_tostring,nullyesForwarding target email address, or null to turn forwarding off. (max 254 chars)

Returns: Inbox with forwardTo and forwardStatus (off, pending, or active).

Example:

{
  "name": "set_inbox_forwarding",
  "arguments": {
    "inbox_id": "inb_123",
    "forward_to": "team@example.net"
  }
}

delete_inbox

Delete an inbox · Destructive · DELETE /inboxes/:inbox_id

DESTRUCTIVE: delete an inbound address. Mail already received is retained; new mail to the address is no longer filed to it.

ParameterTypeRequiredDescription
inbox_idstringyesInbox ID (starts with inb_), as returned by a list or create tool. (max 128 chars)

Returns: {ok: true}.

Example:

{
  "name": "delete_inbox",
  "arguments": {
    "inbox_id": "inb_123"
  }
}

Deliverability, bounces, and suppressions

deliverability_stats

Get 30-day delivery stats · Read-only · GET /deliverability/stats

Workspace-wide 30-day totals: sent, delivery, bounce, complaint, reject, open, click, and deliveryRate (%).

No parameters.

Returns: {window: 30d, sent, delivery, bounce, complaint, reject, open, click, deliveryRate}.

Example:

{
  "name": "deliverability_stats",
  "arguments": {}
}

list_sender_reputation

List sender reputation · Read-only · GET /deliverability/reputation

Reputation state per exact From address: active, throttled (lower daily limit), or paused (sends return 423), with the reason and daily limit. Check this when sends fail with 423 or 429. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{sender, status, dailyLimit, reason, cleanSince, warnedAt, pausedAt, evaluatedAt}], count, pagination}.

Example:

{
  "name": "list_sender_reputation",
  "arguments": {}
}

list_suppressions

List suppressions · Read-only · GET /suppressions

Workspace suppression list: recipients blocked after a permanent bounce or a spam complaint. Sends to them fail with 422. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{email, reason, detail, created_at}], count, pagination}.

Example:

{
  "name": "list_suppressions",
  "arguments": {}
}

remove_suppression

Remove a bounce suppression · Destructive · DELETE /suppressions/:email

DESTRUCTIVE (weakens a safety block): remove a bounce suppression so the address can be mailed again. Only do this when the human confirms the address is now valid. Complaint suppressions cannot be removed (409).

ParameterTypeRequiredDescription
emailstringyesSuppressed recipient address. (max 320 chars)

Returns: {ok: true}.

Example:

{
  "name": "remove_suppression",
  "arguments": {
    "email": "fixed-mailbox@example.net"
  }
}

list_blocked_recipients

List blocked recipients · Read-only · GET /blocked-recipients

Every recipient SendHQ will refuse: bounces, complaints, and domain-scoped marketing unsubscribes, with a summary by kind. Reads up to the newest 500. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{email, domain, kind: bounce|complaint|unsubscribe, reason, detail, source, status, created_at}], count, summary: {total, bounce, complaint, unsubscribe}, pagination}.

Example:

{
  "name": "list_blocked_recipients",
  "arguments": {}
}

Account, usage, analytics, and keys

get_account

Get account, usage, and billing · Read-only · GET /account

Account owner email, plan/access tier, current-period recipient deliveries used vs quota, domains used vs limit, attachment transfer, reputation summary, subscription state, published plans, and workspace counts. Use it to check remaining quota or who the trial can deliver to (the account email).

No parameters.

Returns: {user: {email, …}, usage: {domainsUsed, domainLimit, recipientDeliveries, emailQuotaMonth, attachmentBytes, attachmentByteLimit, periodKey}, access: {tier, planCode}, reputation, infrastructure, billing: {status, subscriptions, …}, plans, workspace: {mailer, stats}}.

Example:

{
  "name": "get_account",
  "arguments": {}
}

get_analytics

Get sending analytics · Read-only · GET /analytics

Dashboard analytics for the last 7, 30, or 90 days: sent/received/delivered/bounced/blocked/opened/clicked/complaint totals, a daily timeline, top sending domains, and top subjects.

ParameterTypeRequiredDescription
daysintegernoWindow in days: 7, 30 (default), or 90. (one of 7, 30, 90)

Returns: {window, days, metrics, timeline: [{day, sent, received}], domains, topContent}.

Example:

{
  "name": "get_analytics",
  "arguments": {
    "days": 30
  }
}

list_api_keys

List API key metadata · Read-only · GET /keys

List API key names, non-secret prefixes, and last-used times. Read-only: this MCP server cannot create, rotate, or revoke keys; a human does that in the dashboard. Paginated: the result includes pagination {offset, limit, returned, total?, has_more, next_offset}.

ParameterTypeRequiredDescription
limitintegernoPage size. Defaults to 50. (default 50; 1–200)
offsetintegernoNumber of records to skip. Use pagination.next_offset from the previous page. (default 0; 0–…)

Returns: {data: [{id, name, prefix, lastUsedAt, createdAt}], count, pagination}.

Example:

{
  "name": "list_api_keys",
  "arguments": {}
}

get_service_health

Check SendHQ service health · Read-only · GET /health

Check that the SendHQ API is up and which mail provider is active. Does not need a valid API key.

No parameters.

Returns: {ok, service, mailer}.

Example:

{
  "name": "get_service_health",
  "arguments": {}
}

API coverage inventory

Every operation in the public API and the tool that covers it. Everything a user can do in the dashboard that has an API is covered; the exclusions below are deliberate.

EndpointToolNotes
POST /emailssend_emailSend one email
POST /emails/batchsend_batchSend up to 100 individualized messages
GET /emailslist_emailsList sent and received email
GET /emails/:idget_emailRetrieve an email and its attachments
PATCH /emails/:idmark_emailUpdate read, archive, spam, category, or importance
POST /emails/:id/labelslabel_emailAdd or remove labels on an email
DELETE /emails/:iddelete_emailDelete a retained email
GET /emails/:id/eventslist_email_eventsList delivery events for an email
GET /threads/:idget_threadRetrieve a conversation chronologically
GET /labelslist_labelsList labels with message counts and filing rules
POST /labelscreate_labelCreate a label, optionally with auto-filing rules
GET /labels/:idget_labelRetrieve a label by ID or name
PATCH /labels/:idupdate_labelRename, recolor, or turn a label into a bucket
DELETE /labels/:iddelete_labelDelete a label without deleting its email
POST /labels/:id/rulescreate_label_ruleAdd an auto-filing rule to a label
DELETE /labels/:id/rules/:rule_iddelete_label_ruleDelete an auto-filing rule
POST /draftscreate_draftCreate a composer draft
GET /draftslist_draftsList composer drafts
GET /drafts/:idget_draftRetrieve a draft and attachments
PUT /drafts/:idupdate_draftReplace draft content
DELETE /drafts/:iddelete_draftDiscard a draft
POST /drafts/:id/attachmentsupload_attachmentUpload an attachment to a draft
GET /attachments/:iddownload_attachmentDownload a private attachment
DELETE /attachments/:iddelete_attachmentDelete a private attachment
GET /sending-identitieslist_sending_identitiesList verified sender identities
GET /templateslist_templatesList hosted templates
POST /templatescreate_templateCreate a hosted template
GET /templates/:idget_templateRetrieve drafts, releases, and usage
PUT /templates/:id/draftupdate_template_draftAutosave a template draft
POST /templates/:id/draftcreate_template_draftCreate a new draft from the published release
POST /templates/:id/renderrender_templateRender exact server output
POST /templates/:id/testsend_template_testSend a test snapshot
POST /templates/:id/publishpublish_templatePublish an immutable template release
POST /templates/:id/archivearchive_templateArchive a template
POST /templates/:id/restorerestore_templateRestore an archived template
POST /domainsadd_domainAdd a sending domain
GET /domainslist_domainsList domains and cached DNS state
GET /domains/:idget_domainRetrieve domain setup details
POST /domains/:id/verifyverify_domainRefresh SES and DNS verification
POST /domains/:id/inbound/setupsetup_inboundProvision SES inbound receiving
POST /domains/:id/inbound/verifyverify_inboundVerify inbound MX routing
DELETE /domains/:iddelete_domainDelete a domain
GET /dns/providerget_dns_providerDetect the authoritative DNS provider and relative record hosts
GET /dns/domain-connect/connectget_domain_connect_linkCreate a Domain Connect consent link for one-click DNS setup
POST /inboxescreate_inboxCreate an inbound address
GET /inboxeslist_inboxesList inbound addresses
GET /inboxes/:idget_inboxRetrieve an inbound address
PATCH /inboxes/:idupdate_inboxRename, enable, or disable an inbox
PUT /inboxes/:id/forwardingset_inbox_forwardingForward an inbox's received mail to another address
DELETE /inboxes/:iddelete_inboxDelete an inbox while retaining messages
GET /deliverability/statsdeliverability_statsRetrieve 30-day delivery statistics
GET /deliverability/reputationlist_sender_reputationList reputation state by exact sender identity
GET /suppressionslist_suppressionsList workspace suppressions
DELETE /suppressions/:emailremove_suppressionRemove an eligible bounce suppression
GET /blocked-recipientslist_blocked_recipientsList bounces, complaints, and unsubscribes
GET /accountget_accountRetrieve account, usage, billing state, and workspace counts with an API key
GET /analyticsget_analyticsRetrieve dashboard sending analytics for 7, 30, or 90 days
GET /profileget_accountSession-only twin of GET /account; the MCP server reads the API-key route.
POST /billing/checkoutnot exposedBilling changes are session-only by design and require the account owner in the dashboard. Billing state is readable with get_account.
POST /billing/cancelnot exposedBilling changes are session-only by design and require the account owner in the dashboard. Billing state is readable with get_account.
POST /keysnot exposedDeliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard.
GET /keyslist_api_keysList API-key metadata
DELETE /keys/:idnot exposedDeliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard.

Deliberately not available

CapabilityEndpointsReason
Create, rotate, revoke, or delete API keysPOST /keys, DELETE /keys/:idDeliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard.
Start a checkout or cancel a subscriptionPOST /billing/checkout, POST /billing/cancelBilling changes are session-only by design and require the account owner in the dashboard. Billing state is readable with get_account.
Cloudflare one-click DNS (OAuth)GET /api/dns/cloudflare/connectRequires an interactive browser session and Cloudflare OAuth consent. Use get_domain records, get_dns_provider hosts, or get_domain_connect_link instead.
Sign up, log in, log out, Google account linking/api/auth/*Human browser authentication; the MCP server authenticates with an API key.
Contact support formPOST /api/contactPublic marketing-site form for humans, not a workspace operation.

Machine-readable catalog: /docs/mcp/tools.json (schemas, annotations, endpoint mapping, exclusions). Markdown version of this page: /docs/mcp.md. With the CLI installed, sendhq commands --format json prints the same catalog.