---
title: SendHQ MCP server
description: Install and use the SendHQ Model Context Protocol server: every tool with parameters, return shapes and examples, client configuration for Claude Code, Codex and Claude Desktop, workflows, errors, idempotency, and safety rules for AI agents.
canonical: https://sendhq.cc/docs/mcp
last-updated: 2026-09-26
---

# 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`.

```sh
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](https://sendhq.cc/docs/mcp/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](https://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:

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

Windows PowerShell:

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

Check the install:

```sh
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:

```sh
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:

```sh
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:

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

### OpenAI Codex

~/.codex/config.toml:

```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:

```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):

```json
{"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 flag | Required | Meaning |
| --- | --- | --- |
| `SENDHQ_API_KEY` | yes | Workspace 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_URL` | no | API 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_ONLY` | no | `1`, `true`, or `yes` behaves like `--read-only`. |
| `--read-only` | no | Expose only tools that neither send email nor change state. Hidden tools are also refused if called by name. |
| `SENDHQ_PROFILE` / `--profile` | no | Use 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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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):

```json
{
  "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`.

| code | HTTP | Retry? | What it means and what to do |
| --- | --- | --- | --- |
| `invalid_arguments` | — | no | Arguments failed the tool's JSON Schema locally; nothing reached SendHQ. Fix the fields listed in `problems`. |
| `auth_error` | 401 | no | Missing, revoked, or wrong API key. Set `SENDHQ_API_KEY` for the server process; a human creates keys in the dashboard. |
| `trial_recipient_restricted` | 402 | no | Integration trial can deliver only to the account email or an SES simulator address. Send there, or the owner activates a paid plan. |
| `payment_required` | 402 | no | Feature needs a paid plan (for example attachments). Send without it or upgrade. |
| `sender_domain_not_owned` | 403 | no | From domain is not in this workspace. Use `list_sending_identities` or `add_domain`. |
| `sender_domain_unverified` | 403 | no | From domain is not verified yet. `get_domain`, publish missing records, `verify_domain`. |
| `domain_limit_reached` | 403 | no | Plan domain limit reached. Remove an unused domain (with approval) or upgrade. |
| `marketing_not_enabled` | 403 | no | Marketing class is not enabled for this domain or plan. Use `transactional` only if the message genuinely is. |
| `forbidden` | 403 | no | Policy does not allow the operation. Adjust the request. |
| `not_found` | 404 | no | ID is not in this workspace. List the resource to find the right ID; restore archived templates first. |
| `idempotency_conflict` | 409 | no | Key reused with a different body. Resend the exact original, or use a new key for a new message. |
| `idempotency_in_progress` | 409 | yes | Original request still running. Wait, then retry with the same key and body. |
| `revision_conflict` | 409 | no | Template draft changed since you read it. `get_template`, merge, save again. |
| `complaint_suppression_locked` | 409 | no | Recipient complained. Never email them again. |
| `inbound_not_ready` | 409 | no | Inbound receiving not ready. `setup_inbound`, publish MX, `verify_inbound`. |
| `conflict` | 409 | no | Resource already exists or is in the wrong state. Read it and adjust. |
| `attachments_too_large` | 413 | no | More than 10 files or 10 MB. Remove or shrink attachments. |
| `recipient_suppressed` | 422 | no | A recipient hard-bounced or complained before. Remove them; see `list_blocked_recipients`. |
| `recipient_unsubscribed` | 422 | no | A recipient opted out of marketing mail. Remove them permanently. |
| `validation_failed` | 422 | no | Content rejected, for example template data that breaks the variable contract. Fix the input. |
| `sender_paused` | 423 | no | This From address is paused by the 7-day bounce/complaint circuit breaker. Stop, fix the list, wait for automatic recovery. |
| `quota_exhausted` | 429 | no | Monthly, daily per-sender, attachment, or trial limit reached. Check `get_account`; wait for reset or upgrade. |
| `rate_limited` | 429 | yes | Slow down; wait `retry_after_seconds`. Sends: same key, same body. |
| `server_error` | 5xx | yes | Temporary 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` | — | yes | Request or response lost. Retry; for sends the same `idempotency_key` makes that safe. |
| `invalid_request` | 400 | no | Malformed request. Read `message` and correct it. |
| `tool_error` | — | no | Local 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`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | yes | Sender, e.g. `Acme <hello@example.com>`. The domain must be verified in this workspace (see list_sending_identities). (max 998 chars) |
| `to` | string[] | yes | Recipients. 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) |
| `cc` | string[] | no | Carbon-copy recipients. (0–100 items) |
| `bcc` | string[] | no | Blind-copy recipients. (0–100 items) |
| `subject` | string | no | Subject line. Omit when sending a template. (max 998 chars) |
| `text` | string | no | Plain-text body. Provide text, html, or template. |
| `html` | string | no | HTML body. SendHQ sanitizes it and derives text when `text` is omitted. |
| `reply_to` | string | no | Reply-To address. |
| `headers` | object | no | Extra 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_class` | string | no | `transactional` (default) or `marketing`. Marketing requires a marketing-enabled plan or domain and adds unsubscribe handling. (one of `transactional`, `marketing`) |
| `reply_to_email_id` | string | no | Reply inside an existing conversation: the `em_…` ID of the message being answered. SendHQ sets In-Reply-To/References and the thread. |
| `thread_id` | string | no | Explicit thread ID to file the message under. |
| `draft_id` | string | no | Send a stored draft's attachments with this message (`dr_…`). The draft is deleted after a successful send. |
| `template` | object | no | Send 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.id` | string | no | Template ID (`tmpl_…`). Provide `id` or `key`. |
| `template.key` | string | no | Template key such as `account-welcome`. Provide `id` or `key`. |
| `template.version_id` | string | no | Optional published release ID (`tmplv_…`). Defaults to the current published release. |
| `template.data` | object | no | Values for the template's typed variables. |
| `labels` | string[] | no | Label 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_key` | string | no | Idempotency-Key header (max 200 chars). Reuse it only to retry this exact request. (max 200 chars) |
| `attachments` | object[] | no | Files 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[].filename` | string | no | File name shown to the recipient. Required with content_base64; defaults to the basename of file_path. (max 255 chars) |
| `attachments[].content_type` | string | no | MIME type, e.g. `application/pdf`. Defaults to `application/octet-stream`. |
| `attachments[].content_base64` | string | no | Standard base64 file content. |
| `attachments[].file_path` | string | no | Absolute 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `emails` | object[] | yes | Messages to send. (1–100 items) Provide at least one of: `html`, `text`, `template`. |
| `idempotency_key` | string | no | Idempotency-Key for the entire batch (max 200 chars). (max 200 chars) |

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

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `direction` | string | no | `in` for received, `out` for sent. (one of `in`, `out`) |
| `status` | string | no | Status filter, e.g. `queued`, `sent`, `delivered`, `bounced`, `complained`, `failed`. |
| `domain` | string | no | Only messages for this domain, or a comma-separated list of domains (matches any). |
| `inbox_id` | string | no | Only messages received by this inbox (`inb_…`). |
| `label` | string | no | Only messages carrying this label: a label ID `lbl_…` or exact name, or a comma-separated list (matches any). Use list_labels to see folders. |
| `archived` | boolean | no | false = the Inbox view (received mail not archived), true = archived only. Omit for all mail. |
| `category` | string | no | `primary` (people), `updates` (newsletters, bulk, automated), or `spam`; or a comma-separated list. Spam is hidden unless requested. |
| `important` | boolean | no | true = only messages flagged important (replies to conversations you started, and senders marked important). |
| `include_spam` | boolean | no | Include spam in the results (for searches across every folder). |
| `from` | string | no | Sender address contains this value. |
| `to` | string | no | Recipient address contains this value. |
| `unread` | boolean | no | true = unread only, false = read only. |
| `after` | string | no | ISO-8601 timestamp; only messages created after it. (date-time) |
| `before` | string | no | ISO-8601 timestamp; only messages created before it. (date-time) |
| `query` | string | no | Free-text search over subjects, bodies, sender/recipient addresses, and attachment filenames. (max 200 chars) |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number of records to skip. Use `pagination.next_offset` from the previous page. (default `0`; 0–…) |

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

Example:

```json
{
  "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).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email_id` | string | yes | Email 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email_id` | string | yes | Email ID (starts with `em_`), as returned by a list or create tool. (max 128 chars) |
| `read` | boolean | no | true = read, false = unread. |
| `archived` | boolean | no | true = archive (skip the Inbox), false = move back to the Inbox. |
| `category` | string | no | Move a received message to primary, updates, or spam. (one of `primary`, `updates`, `spam`) |
| `important` | boolean | no | Flag or unflag the message as important. |
| `learn` | boolean | no | false = do not remember this verdict for the sender (default true). |

**Returns:** The updated email object.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email_id` | string | yes | Email ID (starts with `em_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email_id` | string | yes | Email ID (starts with `em_`), as returned by a list or create tool. (max 128 chars) |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `thread_id` | string | yes | Thread ID (usually the `em_…` ID of the first message; see `threadId` on any email). (max 128 chars) |

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

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

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

#### get_label

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

Retrieve one label with counts and auto-filing rules.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label_id` | string | yes | Label ID (starts with `lbl_`) or the exact label name. (max 128 chars) |

**Returns:** Label object.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Label name, e.g. `Billing` or `Clients/Acme`. Unique per workspace (case-insensitive). (max 64 chars) |
| `color` | string | no | Hex color such as `#1a73e8`. Optional. |
| `skip_inbox` | boolean | no | Bucket 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. |
| `rules` | object[] | no | Optional auto-filing rules (max 20). Each needs at least one of inbox_id, from, to, subject. (0–20 items) |
| `rules[].direction` | string | no | Only `in` (received) or `out` (sent) mail. Omit for both. (one of `in`, `out`) |
| `rules[].inbox_id` | string | no | Only mail received by this inbox (`inb_…`). Files each receiving address into its own folder. |
| `rules[].from` | string | no | Sender contains this text (case-insensitive), e.g. `@stripe.com`. (max 200 chars) |
| `rules[].to` | string | no | To/Cc contains this text (case-insensitive). (max 200 chars) |
| `rules[].subject` | string | no | Subject contains this text (case-insensitive). (max 200 chars) |
| `rules[].skip_inbox` | boolean | no | Archive matching received mail so it appears only in the label folder, not the Inbox. |
| `apply_to_existing` | boolean | no | Also file already-retained mail that matches the rules. |

**Returns:** The created label with rules.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label_id` | string | yes | Label ID (starts with `lbl_`) or the exact label name. (max 128 chars) |
| `name` | string | no | New name. (max 64 chars) |
| `color` | string | no | New hex color. |
| `skip_inbox` | boolean | no | Bucket 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label_id` | string | yes | Label ID (starts with `lbl_`) or the exact label name. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label_id` | string | yes | Label ID (starts with `lbl_`) or the exact label name. (max 128 chars) |
| `direction` | string | no | Only `in` (received) or `out` (sent) mail. Omit for both. (one of `in`, `out`) |
| `inbox_id` | string | no | Only mail received by this inbox (`inb_…`). Files each receiving address into its own folder. |
| `from` | string | no | Sender contains this text (case-insensitive), e.g. `@stripe.com`. (max 200 chars) |
| `to` | string | no | To/Cc contains this text (case-insensitive). (max 200 chars) |
| `subject` | string | no | Subject contains this text (case-insensitive). (max 200 chars) |
| `skip_inbox` | boolean | no | Archive matching received mail so it appears only in the label folder, not the Inbox. |
| `apply_to_existing` | boolean | no | Also file already-retained mail that matches. |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label_id` | string | yes | Label ID (starts with `lbl_`) or the exact label name. (max 128 chars) |
| `rule_id` | string | yes | Rule ID (starts with `lrule_`), from get_label. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email_id` | string | yes | Email ID (starts with `em_`), as returned by a list or create tool. (max 128 chars) |
| `add` | string[] | no | Labels to add. (0–10 items) |
| `remove` | string[] | no | Labels to remove. (0–10 items) |
| `create` | boolean | no | Create unknown labels in `add` (default true). |

**Returns:** The updated email with `labels`.

Example:

```json
{
  "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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | no | Sender address on a verified domain (may be empty while drafting). |
| `to` | string[] | no | Recipients. (0–100 items) |
| `cc` | string[] | no | Carbon-copy recipients. (0–100 items) |
| `bcc` | string[] | no | Blind-copy recipients. (0–100 items) |
| `subject` | string | no | Subject line. (max 998 chars) |
| `html` | string | no | HTML body. |
| `text` | string | no | Plain-text body. |
| `reply_to_email_id` | string | no | Email ID this draft replies to. |
| `thread_id` | string | no | Thread ID this draft belongs to. |

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

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number of records to skip. Use `pagination.next_offset` from the previous page. (default `0`; 0–…) |

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

Example:

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

#### get_draft

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

Retrieve one draft with its attachment metadata.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `draft_id` | string | yes | Draft ID (starts with `dr_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** Draft object with `attachments`.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `draft_id` | string | yes | Draft ID (starts with `dr_`), as returned by a list or create tool. (max 128 chars) |
| `from` | string | no | Sender address on a verified domain (may be empty while drafting). |
| `to` | string[] | no | Recipients. (0–100 items) |
| `cc` | string[] | no | Carbon-copy recipients. (0–100 items) |
| `bcc` | string[] | no | Blind-copy recipients. (0–100 items) |
| `subject` | string | no | Subject line. (max 998 chars) |
| `html` | string | no | HTML body. |
| `text` | string | no | Plain-text body. |
| `reply_to_email_id` | string | no | Email ID this draft replies to. |
| `thread_id` | string | no | Thread ID this draft belongs to. |

**Returns:** Updated draft object.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `draft_id` | string | yes | Draft ID (starts with `dr_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `draft_id` | string | yes | Draft ID (starts with `dr_`), as returned by a list or create tool. (max 128 chars) |
| `filename` | string | no | File name shown to the recipient. Defaults to the basename of `file_path`. (max 255 chars) |
| `content_type` | string | no | MIME type, e.g. `application/pdf`. Defaults to `application/octet-stream`. |
| `content_base64` | string | no | Standard base64 file content. |
| `file_path` | string | no | Absolute path of a local file readable by the MCP server process. |

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

Example:

```json
{
  "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).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `attachment_id` | string | yes | Attachment ID (starts with `att_`), as returned by a list or create tool. (max 128 chars) |
| `save_to_path` | string | no | Optional absolute local path to write the file to instead of returning base64. |
| `overwrite` | boolean | no | Allow 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:

```json
{
  "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).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `attachment_id` | string | yes | Attachment ID (starts with `att_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `lifecycle` | string | no | `active` (default), `archived`, or `all`. (one of `active`, `archived`, `all`) |
| `query` | string | no | Search by name or key. (max 120 chars) |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number of records to skip. Use `pagination.next_offset` from the previous page. (default `0`; 0–…) |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Human name. (max 120 chars) |
| `key` | string | no | Stable send key: lowercase letters, numbers, hyphens; starts with a letter (2–64 chars). Derived from name when omitted. |
| `starter` | string | no | Starter content. (one of `blank`, `welcome`, `reset`, `receipt`) |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID (`tmpl_…`) or key. (max 128 chars) |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |
| `revision` | integer | yes | Current draft revision from get_template. (1–…) |
| `name` | string | no | Template name. (max 120 chars) |
| `subject_template` | string | no | Subject with placeholders. (max 998 chars) |
| `preheader_template` | string | no | Preview text. (max 240 chars) |
| `html_template` | string | no | HTML body with placeholders. |
| `text_template` | string | no | Plain-text body with placeholders. |
| `from` | string | no | Default sender for sends of this template. |
| `reply_to` | string | no | Default Reply-To. |
| `variables` | object[] | no | Typed variable contract. Each item: {key (lowercase/underscores), label, type: text\|number\|url\|boolean, required (default true), fallback, description}. |
| `variables[].key` | string | yes |  |
| `variables[].label` | string | no |  |
| `variables[].type` | string | no | (one of `text`, `number`, `url`, `boolean`) |
| `variables[].required` | boolean | no |  |
| `variables[].fallback` | any | no |  |
| `variables[].description` | string | no |  |
| `sample_data` | object | no | Sample values used for previews and tests. |

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

Example:

```json
{
  "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).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |

**Returns:** {draft}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |
| `version_id` | string | no | Optional version ID; defaults to the draft, then the published release. |
| `data` | object | no | Variable values; defaults to the version's sample data. |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |
| `to` | string[] | yes | Test recipients. (1–100 items) |
| `from` | string | no | Sender on a verified domain; defaults to the template's From. |
| `version_id` | string | no | Optional version ID. |
| `data` | object | no | Variable values; defaults to sample data. |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |

**Returns:** {template, published}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |

**Returns:** {template}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Template ID or key. (max 128 chars) |

**Returns:** {template}.

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number of records to skip. Use `pagination.next_offset` from the previous page. (default `0`; 0–…) |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Bare domain name, e.g. `example.com` or `mail.example.com`. (max 253 chars) |
| `default_from` | string | no | Optional default sender address on this domain. |

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

Example:

```json
{
  "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`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain ID (starts with `dom_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain ID (starts with `dom_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {url, providerName}.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain 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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain 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:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | no | Optional domain ID filter. |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

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

#### get_inbox

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

Retrieve one inbound address.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `inbox_id` | string | yes | Inbox ID (starts with `inb_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** Inbox object.

Example:

```json
{
  "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`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | yes | Domain ID (starts with `dom_`), as returned by a list or create tool. (max 128 chars) |
| `local_part` | string | yes | Part before @, e.g. `support`. (max 64 chars) |
| `name` | string | no | Optional display name. |

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

Example:

```json
{
  "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`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `inbox_id` | string | yes | Inbox ID (starts with `inb_`), as returned by a list or create tool. (max 128 chars) |
| `name` | string | no | New display name. |
| `status` | string | no | New status. (one of `active`, `disabled`) |

**Returns:** Updated inbox.

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `inbox_id` | string | yes | Inbox ID (starts with `inb_`), as returned by a list or create tool. (max 128 chars) |
| `forward_to` | string,null | yes | Forwarding target email address, or null to turn forwarding off. (max 254 chars) |

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

Example:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `inbox_id` | string | yes | Inbox ID (starts with `inb_`), as returned by a list or create tool. (max 128 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

```json
{
  "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).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | Suppressed recipient address. (max 320 chars) |

**Returns:** {ok: true}.

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

```json
{
  "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:

```json
{
  "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.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer | no | Window in days: 7, 30 (default), or 90. (one of `7`, `30`, `90`) |

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

Example:

```json
{
  "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}.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size. Defaults to 50. (default `50`; 1–200) |
| `offset` | integer | no | Number 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:

```json
{
  "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:

```json
{
  "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.

| Endpoint | Tool | Notes |
| --- | --- | --- |
| POST /emails | `send_email` | Send one email |
| POST /emails/batch | `send_batch` | Send up to 100 individualized messages |
| GET /emails | `list_emails` | List sent and received email |
| GET /emails/:id | `get_email` | Retrieve an email and its attachments |
| PATCH /emails/:id | `mark_email` | Update read, archive, spam, category, or importance |
| POST /emails/:id/labels | `label_email` | Add or remove labels on an email |
| DELETE /emails/:id | `delete_email` | Delete a retained email |
| GET /emails/:id/events | `list_email_events` | List delivery events for an email |
| GET /threads/:id | `get_thread` | Retrieve a conversation chronologically |
| GET /labels | `list_labels` | List labels with message counts and filing rules |
| POST /labels | `create_label` | Create a label, optionally with auto-filing rules |
| GET /labels/:id | `get_label` | Retrieve a label by ID or name |
| PATCH /labels/:id | `update_label` | Rename, recolor, or turn a label into a bucket |
| DELETE /labels/:id | `delete_label` | Delete a label without deleting its email |
| POST /labels/:id/rules | `create_label_rule` | Add an auto-filing rule to a label |
| DELETE /labels/:id/rules/:rule_id | `delete_label_rule` | Delete an auto-filing rule |
| POST /drafts | `create_draft` | Create a composer draft |
| GET /drafts | `list_drafts` | List composer drafts |
| GET /drafts/:id | `get_draft` | Retrieve a draft and attachments |
| PUT /drafts/:id | `update_draft` | Replace draft content |
| DELETE /drafts/:id | `delete_draft` | Discard a draft |
| POST /drafts/:id/attachments | `upload_attachment` | Upload an attachment to a draft |
| GET /attachments/:id | `download_attachment` | Download a private attachment |
| DELETE /attachments/:id | `delete_attachment` | Delete a private attachment |
| GET /sending-identities | `list_sending_identities` | List verified sender identities |
| GET /templates | `list_templates` | List hosted templates |
| POST /templates | `create_template` | Create a hosted template |
| GET /templates/:id | `get_template` | Retrieve drafts, releases, and usage |
| PUT /templates/:id/draft | `update_template_draft` | Autosave a template draft |
| POST /templates/:id/draft | `create_template_draft` | Create a new draft from the published release |
| POST /templates/:id/render | `render_template` | Render exact server output |
| POST /templates/:id/test | `send_template_test` | Send a test snapshot |
| POST /templates/:id/publish | `publish_template` | Publish an immutable template release |
| POST /templates/:id/archive | `archive_template` | Archive a template |
| POST /templates/:id/restore | `restore_template` | Restore an archived template |
| POST /domains | `add_domain` | Add a sending domain |
| GET /domains | `list_domains` | List domains and cached DNS state |
| GET /domains/:id | `get_domain` | Retrieve domain setup details |
| POST /domains/:id/verify | `verify_domain` | Refresh SES and DNS verification |
| POST /domains/:id/inbound/setup | `setup_inbound` | Provision SES inbound receiving |
| POST /domains/:id/inbound/verify | `verify_inbound` | Verify inbound MX routing |
| DELETE /domains/:id | `delete_domain` | Delete a domain |
| GET /dns/provider | `get_dns_provider` | Detect the authoritative DNS provider and relative record hosts |
| GET /dns/domain-connect/connect | `get_domain_connect_link` | Create a Domain Connect consent link for one-click DNS setup |
| POST /inboxes | `create_inbox` | Create an inbound address |
| GET /inboxes | `list_inboxes` | List inbound addresses |
| GET /inboxes/:id | `get_inbox` | Retrieve an inbound address |
| PATCH /inboxes/:id | `update_inbox` | Rename, enable, or disable an inbox |
| PUT /inboxes/:id/forwarding | `set_inbox_forwarding` | Forward an inbox's received mail to another address |
| DELETE /inboxes/:id | `delete_inbox` | Delete an inbox while retaining messages |
| GET /deliverability/stats | `deliverability_stats` | Retrieve 30-day delivery statistics |
| GET /deliverability/reputation | `list_sender_reputation` | List reputation state by exact sender identity |
| GET /suppressions | `list_suppressions` | List workspace suppressions |
| DELETE /suppressions/:email | `remove_suppression` | Remove an eligible bounce suppression |
| GET /blocked-recipients | `list_blocked_recipients` | List bounces, complaints, and unsubscribes |
| GET /account | `get_account` | Retrieve account, usage, billing state, and workspace counts with an API key |
| GET /analytics | `get_analytics` | Retrieve dashboard sending analytics for 7, 30, or 90 days |
| GET /profile | `get_account` | Session-only twin of `GET /account`; the MCP server reads the API-key route. |
| POST /billing/checkout | not exposed | Billing changes are session-only by design and require the account owner in the dashboard. Billing state is readable with get_account. |
| POST /billing/cancel | not exposed | Billing changes are session-only by design and require the account owner in the dashboard. Billing state is readable with get_account. |
| POST /keys | not exposed | Deliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard. |
| GET /keys | `list_api_keys` | List API-key metadata |
| DELETE /keys/:id | not exposed | Deliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard. |

### Deliberately not available

| Capability | Endpoints | Reason |
| --- | --- | --- |
| Create, rotate, revoke, or delete API keys | `POST /keys`, `DELETE /keys/:id` | Deliberately excluded: an agent must not mint or destroy credentials. Keys are managed by a human in the dashboard. |
| Start a checkout or cancel a subscription | `POST /billing/checkout`, `POST /billing/cancel` | Billing 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/connect` | Requires 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 form | `POST /api/contact` | Public marketing-site form for humans, not a workspace operation. |

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