For AI agents

SendHQ MCP server

Give an AI agent full, safe control of one SendHQ workspace: send and receive email, verify domains, publish templates, and investigate deliverability through 104 strictly typed tools. Written for agents first; humans welcome.

104 toolsstdio transport, one command0 key-management tools
Install and connect (Claude Code)
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, run newsletters (consented contacts with tags and custom fields, segments, lists with signup forms, welcome emails, and newsletter sites, and broadcasts with pre-send checks, test sends, scheduling, and bot-filtered open and click reports), 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.

  • 104 tools in 9 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, ChatGPT and Grok

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.

Grok

  1. Go to grok.com/connectors, click New Connector, then Custom.
  2. Enter https://mcp.sendhq.cc/mcp, sign in to SendHQ and click Allow.

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 a mailbox-simulator test 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, send_template_test, send_broadcast, send_broadcast_test, schedule_broadcast (at the scheduled time), and send_broadcast_to_more 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, remove_suppression, the campaign deletes (delete_contact, delete_list, remove_list_contact, delete_broadcast, cancel_broadcast, delete_segment, delete_contact_field), and bulk_update_contacts 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 a mailbox-simulator test 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. Broadcast opens are estimates: privacy proxies and security scanners load images on their own, and requests that look automated are counted apart (machine in get_broadcast_report).
  • 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, domain 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 SendHQ's SPF include (include:amazonses.com) into the existing v=spf1 value.
  4. verify_domain rechecks DNS and DKIM verification. 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

Webhooks push signed email events (email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.failed, email.received) to an endpoint a human configures in the dashboard or with the REST API (see /docs/webhooks). There is no webhook tool, because endpoint responses carry signing secrets. An agent without a webhook polls 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.

9. Send a newsletter

  1. Audience: list_lists (or create_list with a signup form), and optionally preview_segment then create_segment to narrow it, for example to contacts tagged VIP who opened in the last 90 days. Add only people who asked for your email: every subscribed contact needs a consent_basis.
  2. get_newsletter_settings: if postalAddress is empty, ask the user for one and set it with update_newsletter_settings (marketing email needs it).
  3. create_broadcast with list_id, a verified from, subject, preview_text, content (markdown is simplest), and optionally audience (segment_id, limit, order: engaged).
  4. check_broadcast: fix every block item and tell the user about warn items. Then send_broadcast_test to the user's own address so they can look at it.
  5. Only when the user says to send: send_broadcast now (with an idempotency_key), or schedule_broadcast with scheduled_at (unschedule_broadcast to change it).
  6. Follow up with get_broadcast (delivery totals), get_broadcast_report (opens, clicks, links, inbox providers, against your average), and list_broadcast_recipients with activity. Report opens as estimates. With a limited audience, send_broadcast_to_more sends to the next people.
Draft a broadcast
{
  "name": "create_broadcast",
  "arguments": {
    "list_id": "lst_…",
    "from": "Acme <news@example.com>",
    "subject": "What shipped in October",
    "preview_text": "Three new features",
    "markdown": "Hi {{first_name | there}},\n\nHere is what shipped this month.",
    "audience": {
      "segment_id": "seg_…"
    }
  }
}

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 a mailbox-simulator test 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 a mailbox-simulator test 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 mailbox-simulator test 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 a mailbox-simulator test 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_batch, list_emails, get_email, mark_email, delete_email, list_email_events, get_thread
Labels and auto-filing rules: list_labels, get_label, create_label, update_label, delete_label, create_label_rule, delete_label_rule, label_email
Drafts, attachments, and sender identities: list_sending_identities, create_draft, list_drafts, get_draft, update_draft, delete_draft, upload_attachment, download_attachment, delete_attachment
Hosted templates: list_templates, create_template, get_template, update_template_draft, create_template_draft, render_template, send_template_test, publish_template, archive_template, restore_template
Domains and DNS: list_domains, get_domain, add_domain, verify_domain, delete_domain, get_dns_provider, get_domain_connect_link
Inbound email: setup_inbound, verify_inbound, list_inboxes, get_inbox, create_inbox, update_inbox, set_inbox_forwarding, delete_inbox
Deliverability, bounces, and suppressions: deliverability_stats, list_sender_reputation, list_suppressions, remove_suppression, list_blocked_recipients
Campaigns: list_contacts, create_contact, get_contact, update_contact, delete_contact, import_contacts, list_contact_imports, bulk_update_contacts, list_tags, list_contact_fields, set_contact_field, delete_contact_field, list_segments, get_segment, preview_segment, create_segment, update_segment, delete_segment, list_lists, create_list, get_list, update_list, delete_list, add_list_contacts, remove_list_contact, list_broadcasts, create_broadcast, get_broadcast, update_broadcast, delete_broadcast, send_broadcast, cancel_broadcast, list_broadcast_recipients, check_broadcast, send_broadcast_test, schedule_broadcast, unschedule_broadcast, duplicate_broadcast, send_broadcast_to_more, get_broadcast_report, get_newsletter_settings, update_newsletter_settings, get_campaign_overview, list_uploaded_images
Account, usage, analytics, and keys: get_account, get_account_diagnostics, get_analytics, list_api_keys, get_service_health

Emails and threads

Sends real emailsend_email
POST /emails

Send one email

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 a mailbox-simulator test 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 tools/call params
{
  "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"
  }
}
Sends real emailsend_batch
POST /emails/batch

Send a batch of individualized emails

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 tools/call params
{
  "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"
  }
}
Read-onlylist_emails
GET /emails

List and search email

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_emails",
  "arguments": {
    "direction": "in",
    "unread": true,
    "limit": 25
  }
}
Read-onlyget_email
GET /emails/:email_id

Get one email

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)
ReturnsEmail object: {id, direction, status, from, to, cc, bcc, subject, html, text, threadId, messageId, providerMessageId, readAt, createdAt, attachments: [{id, filename, contentType, sizeBytes, available}]}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_email",
  "arguments": {
    "email_id": "em_123"
  }
}
Changes statemark_email
PATCH /emails/:email_id

Mark read, archived, spam, or important

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).
ReturnsThe updated email object.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "mark_email",
  "arguments": {
    "email_id": "em_123",
    "read": true
  }
}
Destructivedelete_email
DELETE /emails/:email_id

Delete an email

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_email",
  "arguments": {
    "email_id": "em_123"
  }
}
Read-onlylist_email_events
GET /emails/:email_id/events

List delivery events for an email

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_email_events",
  "arguments": {
    "email_id": "em_123"
  }
}
Read-onlyget_thread
GET /threads/:thread_id

Get a conversation

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]}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_thread",
  "arguments": {
    "thread_id": "em_123"
  }
}

Labels and auto-filing rules

Read-onlylist_labels
GET /labels

List 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_labels",
  "arguments": {}
}
Read-onlyget_label
GET /labels/:label_id

Get a label

Retrieve one label with counts and auto-filing rules.

ParameterTypeRequiredDescription
label_idstringyesLabel ID (starts with lbl_) or the exact label name. (max 128 chars)
ReturnsLabel object.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_label",
  "arguments": {
    "label_id": "Billing"
  }
}
Changes statecreate_label
POST /labels

Create a label

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.
ReturnsThe created label with rules.
Example tools/call params
{
  "name": "create_label",
  "arguments": {
    "name": "Agent/Orders",
    "skip_inbox": true,
    "rules": [
      {
        "from": "@stripe.com"
      }
    ]
  }
}
Changes stateupdate_label
PATCH /labels/:label_id

Rename, recolor, or bucket a label

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.
ReturnsUpdated label.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_label",
  "arguments": {
    "label_id": "lbl_123",
    "name": "Finance/Billing"
  }
}
Destructivedelete_label
DELETE /labels/:label_id

Delete a label

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_label",
  "arguments": {
    "label_id": "lbl_123"
  }
}
Changes statecreate_label_rule
POST /labels/:label_id/rules

Add an auto-filing rule

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 tools/call params
{
  "name": "create_label_rule",
  "arguments": {
    "label_id": "Billing",
    "inbox_id": "inb_123",
    "skip_inbox": true
  }
}
Destructivedelete_label_rule
DELETE /labels/:label_id/rules/:rule_id

Delete an auto-filing rule

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_label_rule",
  "arguments": {
    "label_id": "lbl_123",
    "rule_id": "lrule_123"
  }
}
Changes statelabel_email
POST /emails/:email_id/labels

Add or remove labels on an email

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).
ReturnsThe updated email with labels.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "label_email",
  "arguments": {
    "email_id": "em_123",
    "add": [
      "Billing"
    ],
    "remove": [
      "Support"
    ]
  }
}

Drafts, attachments, and sender identities

Read-onlylist_sending_identities
GET /sending-identities

List verified sender 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: [...]}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_sending_identities",
  "arguments": {}
}
Changes statecreate_draft
POST /drafts

Create a draft

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.
ReturnsDraft object {id: dr_…, from, to, cc, bcc, subject, html, text, attachments: []}.
Example tools/call params
{
  "name": "create_draft",
  "arguments": {
    "from": "hello@example.com",
    "to": [
      "owner@example.com"
    ],
    "subject": "Invoice"
  }
}
Read-onlylist_drafts
GET /drafts

List 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_drafts",
  "arguments": {}
}
Read-onlyget_draft
GET /drafts/:draft_id

Get a draft

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)
ReturnsDraft object with attachments.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_draft",
  "arguments": {
    "draft_id": "dr_123"
  }
}
Changes stateupdate_draft
PUT /drafts/:draft_id

Replace draft content

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.
ReturnsUpdated draft object.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_draft",
  "arguments": {
    "draft_id": "dr_123",
    "from": "hello@example.com",
    "to": [
      "owner@example.com"
    ],
    "subject": "Invoice (updated)",
    "text": "Attached."
  }
}
Destructivedelete_draft
DELETE /drafts/:draft_id

Discard a draft

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_draft",
  "arguments": {
    "draft_id": "dr_123"
  }
}
Changes stateupload_attachment
POST /drafts/:draft_id/attachments

Upload an attachment to a draft

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 tools/call params
{
  "name": "upload_attachment",
  "arguments": {
    "draft_id": "dr_123",
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "file_path": "/tmp/invoice.pdf"
  }
}
Read-onlydownload_attachment
GET /attachments/:attachment_id

Download an attachment

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "download_attachment",
  "arguments": {
    "attachment_id": "att_123",
    "save_to_path": "/tmp/invoice.pdf"
  }
}
Destructivedelete_attachment
DELETE /attachments/:attachment_id

Delete an attachment

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_attachment",
  "arguments": {
    "attachment_id": "att_123"
  }
}

Hosted templates

Read-onlylist_templates
GET /templates

List hosted 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_templates",
  "arguments": {
    "lifecycle": "active"
  }
}
Changes statecreate_template
POST /templates

Create a hosted template

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 tools/call params
{
  "name": "create_template",
  "arguments": {
    "name": "Account welcome",
    "key": "account-welcome",
    "starter": "welcome"
  }
}
Read-onlyget_template
GET /templates/:template_id

Get a template

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}
Changes stateupdate_template_draft
PUT /templates/:template_id/draft

Save a template 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 tools/call params
{
  "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"
    }
  }
}
Changes statecreate_template_draft
POST /templates/:template_id/draft

Start a new draft from the published release

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 tools/call params
{
  "name": "create_template_draft",
  "arguments": {
    "template_id": "account-welcome"
  }
}
Read-onlyrender_template
POST /templates/:template_id/render

Render a template preview

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "render_template",
  "arguments": {
    "template_id": "account-welcome",
    "data": {
      "first_name": "Asha"
    }
  }
}
Sends real emailsend_template_test
POST /templates/:template_id/test

Send a template test email

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 a mailbox-simulator test 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 tools/call params
{
  "name": "send_template_test",
  "arguments": {
    "template_id": "account-welcome",
    "to": [
      "owner@example.com"
    ]
  }
}
Changes statepublish_template
POST /templates/:template_id/publish

Publish a template release

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 tools/call params
{
  "name": "publish_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}
Changes statearchive_template
POST /templates/:template_id/archive

Archive a template

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}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "archive_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}
Changes staterestore_template
POST /templates/:template_id/restore

Restore an archived template

Make an archived template active again.

ParameterTypeRequiredDescription
template_idstringyesTemplate ID or key. (max 128 chars)
Returns{template}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "restore_template",
  "arguments": {
    "template_id": "account-welcome"
  }
}

Domains and DNS

Read-onlylist_domains
GET /domains

List 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_domains",
  "arguments": {}
}
Read-onlyget_domain
GET /domains/:domain_id

Get domain setup details

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Changes stateadd_domain
POST /domains

Add a sending domain

Register a domain you control for sending. Returns the DNS records (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 tools/call params
{
  "name": "add_domain",
  "arguments": {
    "name": "example.com"
  }
}
Changes stateverify_domain
POST /domains/:domain_id/verify

Verify a domain

Run a live DNS and DKIM 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}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "verify_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Destructivedelete_domain
DELETE /domains/:domain_id

Delete a domain

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_domain",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Read-onlyget_dns_provider
GET /dns/provider

Detect DNS provider and record hosts

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}}}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_dns_provider",
  "arguments": {
    "domain_id": "dom_123"
  }
}

Inbound email

Changes statesetup_inbound
POST /domains/:domain_id/inbound/setup

Enable inbound receiving for a domain

Provision 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}}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "setup_inbound",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Changes stateverify_inbound
POST /domains/:domain_id/inbound/verify

Verify inbound MX

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}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "verify_inbound",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Read-onlylist_inboxes
GET /inboxes

List inbound addresses

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_inboxes",
  "arguments": {
    "domain_id": "dom_123"
  }
}
Read-onlyget_inbox
GET /inboxes/:inbox_id

Get an inbox

Retrieve one inbound address.

ParameterTypeRequiredDescription
inbox_idstringyesInbox ID (starts with inb_), as returned by a list or create tool. (max 128 chars)
ReturnsInbox object.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_inbox",
  "arguments": {
    "inbox_id": "inb_123"
  }
}
Changes statecreate_inbox
POST /inboxes

Create an inbound address

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 tools/call params
{
  "name": "create_inbox",
  "arguments": {
    "domain_id": "dom_123",
    "local_part": "support",
    "name": "Support"
  }
}
Changes stateupdate_inbox
PATCH /inboxes/:inbox_id

Rename, enable, or disable an inbox

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)
ReturnsUpdated inbox.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_inbox",
  "arguments": {
    "inbox_id": "inb_123",
    "status": "disabled"
  }
}
Sends real emailset_inbox_forwarding
PUT /inboxes/:inbox_id/forwarding

Forward an inbox to another address

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)
ReturnsInbox with forwardTo and forwardStatus (off, pending, or active).
AnnotationsidempotentHint
Example tools/call params
{
  "name": "set_inbox_forwarding",
  "arguments": {
    "inbox_id": "inb_123",
    "forward_to": "team@example.net"
  }
}
Destructivedelete_inbox
DELETE /inboxes/:inbox_id

Delete an inbox

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_inbox",
  "arguments": {
    "inbox_id": "inb_123"
  }
}

Deliverability, bounces, and suppressions

Read-onlydeliverability_stats
GET /deliverability/stats

Get 30-day delivery 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "deliverability_stats",
  "arguments": {}
}
Read-onlylist_sender_reputation
GET /deliverability/reputation

List sender 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_sender_reputation",
  "arguments": {}
}
Read-onlylist_suppressions
GET /suppressions

List 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_suppressions",
  "arguments": {}
}
Destructiveremove_suppression
DELETE /suppressions/:email

Remove a bounce suppression

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}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "remove_suppression",
  "arguments": {
    "email": "fixed-mailbox@example.net"
  }
}
Read-onlylist_blocked_recipients
GET /blocked-recipients

List 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_blocked_recipients",
  "arguments": {}
}

Campaigns

Read-onlylist_contacts
GET /contacts

List contacts

Campaigns: Newest-first contacts with their status, consent evidence, tags, custom fields, engagement (bot-filtered, estimated opens and clicks), and the lists they are subscribed to. Filter by list, status (including dormant), email/name substring (* is an email wildcard, e.g. *@example.com), tag, source, signup date, or a saved segment. Paginated: the result includes pagination {offset, limit, returned, has_more, next_offset}.

ParameterTypeRequiredDescription
list_idstringnoOnly members of this list (lst_…).
statusstringnoOnly contacts with this status. dormant: stopped opening and did not answer list cleanup, so broadcasts skip them. (one of subscribed, pending, unsubscribed, bounced, complained, dormant)
querystringnoEmail or name contains this text; * is a wildcard on the email. (max 200 chars)
tagstringnoOnly contacts with this tag. (max 60 chars)
sourcestringnoHow the contact was added. (one of form, api, import)
afterstringnoSigned up at or after this time. ISO 8601 date or date-time. (max 40 chars)
beforestringnoSigned up before this time. ISO 8601 date or date-time. (max 40 chars)
segment_idstringnoOnly contacts matching this saved segment (seg_…).
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: [contacts with tags, engagement, dormant, country], count, total, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_contacts",
  "arguments": {
    "status": "subscribed"
  }
}
Changes statecreate_contact
POST /contacts

Add a contact

Campaigns: Add one contact to the workspace, optionally to lists and with tags. A subscribed contact (the default) needs consent_basis: how this person agreed to receive your email. Only add people who asked for your email. Use status: unsubscribed to record an opt-out. Addresses already suppressed or unsubscribed from the workspace are stored with that status. Fails with 409 if the email already exists.

ParameterTypeRequiredDescription
emailstringyesEmail address. (max 320 chars)
namestringnoDisplay name; the first word is {{first_name}}. (max 200 chars)
fieldsobjectnoCustom fields (string, number, or boolean values), e.g. {"plan": "pro"}. Usable in broadcasts as {{fields.plan}} or {{plan}}. On update, a null value removes the field.
tagsstring[]noTags to attach, e.g. ["VIP"]. Up to 50. (0–50 items)
list_idsstring[]noLists to add the contact to (lst_…). (0–100 items)
statusstringnosubscribed (default, requires consent_basis) or unsubscribed. (one of subscribed, unsubscribed)
consent_basisstringnoHow these people agreed to receive your email, recorded as consent evidence, e.g. Signed up on acme.com/newsletter (double opt-in), exported from Mailchimp. 10-500 characters. (max 500 chars)
ReturnsThe contact.
Example tools/call params
{
  "name": "create_contact",
  "arguments": {
    "email": "ada@example.net",
    "name": "Ada Lovelace",
    "consent_basis": "Ticked the newsletter box at checkout on acme.com"
  }
}
Read-onlyget_contact
GET /contacts/:contact_id

Get a contact

Campaigns: One contact by ID or email: status, consent evidence (source, basis, opt-in and confirmation times), custom fields, and subscribed lists.

ParameterTypeRequiredDescription
contact_idstringyesContact ID (starts with ct_) or the contact's email address. (max 320 chars)
ReturnsThe contact.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_contact",
  "arguments": {
    "contact_id": "ct_123"
  }
}
Changes stateupdate_contact
PATCH /contacts/:contact_id

Update, tag, or unsubscribe a contact

Campaigns: Change a contact's name, merge custom fields (null removes one), change tags (tags replaces the whole set; add_tags and remove_tags change it), or set status to unsubscribed. A contact can never be resubscribed through the API; only the recipient can, by confirming a signup form.

ParameterTypeRequiredDescription
contact_idstringyesContact ID (starts with ct_) or the contact's email address. (max 320 chars)
namestringnoNew display name. (max 200 chars)
fieldsobjectnoCustom fields (string, number, or boolean values), e.g. {"plan": "pro"}. Usable in broadcasts as {{fields.plan}} or {{plan}}. On update, a null value removes the field.
tagsstring[]noReplace all of the contact's tags with these. (0–50 items)
add_tagsstring[]noTags to add. (0–50 items)
remove_tagsstring[]noTags to remove. (0–50 items)
statusstringnoOnly unsubscribed is accepted. (one of unsubscribed)
ReturnsThe updated contact.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_contact",
  "arguments": {
    "contact_id": "ct_123",
    "fields": {
      "plan": "team"
    }
  }
}
Destructivedelete_contact
DELETE /contacts/:contact_id

Delete a contact

DESTRUCTIVE: permanently delete a contact and its list memberships. Suppressions and recorded unsubscribes are kept, so the address still cannot be mailed if it is added again. Part of Campaigns.

ParameterTypeRequiredDescription
contact_idstringyesContact ID (starts with ct_) or the contact's email address. (max 320 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_contact",
  "arguments": {
    "contact_id": "ct_123"
  }
}
Changes stateimport_contacts
POST /contact-imports

Import contacts from CSV

Campaigns: Import up to 5,000 CSV rows (5 MB) per call, optionally into a list. The header row needs an email column; name (or first_name/last_name), status, and tags (separated by ; or |) are recognized and other columns become custom fields. consent_basis is required and recorded on every new contact. tags are added to every accepted row. Rows whose status says unsubscribed import as unsubscribed; existing contacts keep their status. Returns counts of accepted, created, existing, skipped (duplicates), and invalid rows with line numbers.

ParameterTypeRequiredDescription
csvstringyesThe CSV file content, header row first. (max 5242880 chars)
list_idstringnoAdd every accepted row to this list (lst_…).
consent_basisstringyesHow these people agreed to receive your email, recorded as consent evidence, e.g. Signed up on acme.com/newsletter (double opt-in), exported from Mailchimp. 10-500 characters. (max 500 chars)
tagsstring[]noTags to add to every accepted row, e.g. ["imported-2026-11"]. (0–50 items)
update_existingbooleannoMerge names and fields into contacts that already exist. Their status never changes.
Returns{id, accepted, created, existing, skipped, invalid, errors: [{line, reason}], tags}.
Example tools/call params
{
  "name": "import_contacts",
  "arguments": {
    "csv": "email,name\nada@example.net,Ada Lovelace\n",
    "consent_basis": "Newsletter signups exported from Mailchimp (double opt-in)"
  }
}
Read-onlylist_contact_imports
GET /contact-imports

List contact imports

Campaigns: Recent CSV imports with their consent basis and row counts.

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: [imports], count, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_contact_imports",
  "arguments": {}
}
Destructivebulk_update_contacts
POST /contacts/bulk

Bulk update contacts

DESTRUCTIVE when action is delete (permanent) or unsubscribe (cannot be undone through the API). Apply one action to up to 10,000 contacts: the IDs in contact_ids, or everyone matching filter (the list_contacts filters). Actions: tag / untag (with tags), add_to_list / remove_from_list (with list_id), unsubscribe, delete. Adding to a list mirrors each contact's status, so nobody is resubscribed. delete needs confirm_count equal to the number of contacts it would delete: call once without it to get that count (409), confirm with the user, then repeat with it. Part of Campaigns.

ParameterTypeRequiredDescription
actionstringyesWhat to do. (one of tag, untag, add_to_list, remove_from_list, unsubscribe, delete)
contact_idsstring[]noContact IDs (ct_…). Use this or filter. (0–10000 items)
filterobjectnoEveryone matching these list_contacts filters. Use this or contact_ids.
filter.list_idstringnoMembers of this list.
filter.statusstringnoOnly contacts with this status. dormant: stopped opening and did not answer list cleanup, so broadcasts skip them. (one of subscribed, pending, unsubscribed, bounced, complained, dormant)
filter.querystringnoEmail or name contains; * is an email wildcard. (max 200 chars)
filter.tagstringnoHas this tag. (max 60 chars)
filter.sourcestringnoHow the contact was added. (one of form, api, import)
filter.afterstringnoSigned up at or after. ISO 8601 date or date-time. (max 40 chars)
filter.beforestringnoSigned up before. ISO 8601 date or date-time. (max 40 chars)
filter.segment_idstringnoMatches this segment.
tagsstring[]noFor tag and untag. (0–50 items)
list_idstringnoFor add_to_list and remove_from_list (lst_…).
confirm_countintegernoFor delete: the number of contacts that will be deleted, from the 409 response. (0–…)
Returns{ok, action, count}; for an unconfirmed delete, a 409 error whose message gives the count.
AnnotationsdestructiveHint
Example tools/call params
{
  "name": "bulk_update_contacts",
  "arguments": {
    "action": "tag",
    "tags": [
      "october"
    ],
    "filter": {
      "list_id": "lst_123",
      "status": "subscribed"
    }
  }
}
Read-onlylist_tags
GET /tags

List contact tags

Campaigns: The workspace's contact tags, most used first, with how many contacts carry each (up to 500). Paginated.

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: [{tag, count}], count, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_tags",
  "arguments": {}
}
Read-onlylist_contact_fields
GET /contact-fields

List custom contact fields

Campaigns: Defined custom contact fields with label and type (text, number, date). Defined fields are known to check_broadcast's merge-tag check and typed in segment conditions. Contacts may also carry undefined keys. Paginated.

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: [{key, label, type, createdAt}], count, types, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_contact_fields",
  "arguments": {}
}
Changes stateset_contact_field
PUT /contact-fields/:field_key

Define a custom contact field

Campaigns: Create or change a custom field definition (label and type). Values live on each contact's fields. Built-in merge tags (email, name, first_name, last_name, fields, unsubscribe_url) cannot be used as keys. A workspace may define 50 fields.

ParameterTypeRequiredDescription
field_keystringyesField key: a lowercase letter, then up to 39 of a-z, 0-9, and _ (e.g. plan). Used in content as {{fields.plan}}. (max 40 chars)
labelstringnoLabel shown in the dashboard. Defaults to the key. (max 80 chars)
typestringnotext (default), number, or date. (one of text, number, date)
Returns{key, label, type, createdAt}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "set_contact_field",
  "arguments": {
    "field_key": "plan",
    "label": "Plan",
    "type": "text"
  }
}
Destructivedelete_contact_field
DELETE /contact-fields/:field_key

Delete a custom field definition

DESTRUCTIVE: delete a custom field definition. Values already stored on contacts are kept. Fails with 404 when the field is not defined. Part of Campaigns.

ParameterTypeRequiredDescription
field_keystringyesField key: a lowercase letter, then up to 39 of a-z, 0-9, and _ (e.g. plan). Used in content as {{fields.plan}}. (max 40 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_contact_field",
  "arguments": {
    "field_key": "plan"
  }
}
Read-onlylist_segments
GET /segments

List segments

Campaigns: Saved segments with count (subscribed, non-dormant contacts in the workspace that match now) and fields, the condition fields and operators each accepts. Paginated.

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, match, conditions, count}], count, fields, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_segments",
  "arguments": {}
}
Read-onlyget_segment
GET /segments/:segment_id

Get a segment

Campaigns: One segment with its conditions and live count.

ParameterTypeRequiredDescription
segment_idstringyesSegment ID (starts with seg_), as returned by list_segments or create_segment. (max 128 chars)
Returns{id, name, match, conditions, count}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_segment",
  "arguments": {
    "segment_id": "seg_123"
  }
}
Read-onlypreview_segment
POST /segments/preview

Preview segment conditions

Campaigns: Count and sample (up to 5) the subscribed, non-dormant contacts that match conditions without saving them. Read-only; use it to check conditions before create_segment.

ParameterTypeRequiredDescription
matchstringnoall (default): every condition must match. any: at least one. (one of all, any)
conditionsobject[]noUp to 20 conditions. Opened and clicked use bot-filtered, estimated engagement. (0–20 items)
conditions[].fieldstringyestag, list, status, source, email, name, signed_up, opened, clicked, opens, sends_without_open, country, or a custom field as fields.<key> (or field together with key). (max 48 chars)
conditions[].keystringnoCustom field key when field is field. (max 40 chars)
conditions[].opstringyestag: has, not_has. list: in, not_in (a list ID). status: is, is_not (subscribed, pending, unsubscribed, bounced, complained, dormant). source: is, is_not (form, api, import). email: contains, not_contains, ends_with. name: is_set, not_set. signed_up: before, after (YYYY-MM-DD), in_last_days, not_in_last_days. opened / clicked: in_last_days, not_in_last_days, ever, never. opens / sends_without_open: gte, lte. country: is, is_not (two-letter code). Custom fields: equals, not_equals, contains, gt, lt, before, after, is_set, not_set. (one of has, not_has, in, not_in, is, is_not, contains, not_contains, ends_with, is_set, not_set, before, after, in_last_days, not_in_last_days, ever, never, gte, lte, equals, not_equals, gt, lt)
conditions[].valuestring,numbernoRequired except for is_set, not_set, ever, and never. A number of days for in_last_days / not_in_last_days, a number for gte, lte, gt, lt, a date (YYYY-MM-DD) for before / after.
Returns{count, sample: [contacts]}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "preview_segment",
  "arguments": {
    "match": "all",
    "conditions": [
      {
        "field": "tag",
        "op": "has",
        "value": "VIP"
      },
      {
        "field": "opened",
        "op": "in_last_days",
        "value": 90
      }
    ]
  }
}
Changes statecreate_segment
POST /segments

Create a segment

Campaigns: Save a segment: up to 20 conditions joined by match. Use it as a broadcast's audience.segment_id, as list_contacts segment_id, or in bulk_update_contacts filters. A workspace may have 200 segments.

ParameterTypeRequiredDescription
namestringyesSegment name. (max 120 chars)
matchstringnoall (default): every condition must match. any: at least one. (one of all, any)
conditionsobject[]noUp to 20 conditions. Opened and clicked use bot-filtered, estimated engagement. (0–20 items)
conditions[].fieldstringyestag, list, status, source, email, name, signed_up, opened, clicked, opens, sends_without_open, country, or a custom field as fields.<key> (or field together with key). (max 48 chars)
conditions[].keystringnoCustom field key when field is field. (max 40 chars)
conditions[].opstringyestag: has, not_has. list: in, not_in (a list ID). status: is, is_not (subscribed, pending, unsubscribed, bounced, complained, dormant). source: is, is_not (form, api, import). email: contains, not_contains, ends_with. name: is_set, not_set. signed_up: before, after (YYYY-MM-DD), in_last_days, not_in_last_days. opened / clicked: in_last_days, not_in_last_days, ever, never. opens / sends_without_open: gte, lte. country: is, is_not (two-letter code). Custom fields: equals, not_equals, contains, gt, lt, before, after, is_set, not_set. (one of has, not_has, in, not_in, is, is_not, contains, not_contains, ends_with, is_set, not_set, before, after, in_last_days, not_in_last_days, ever, never, gte, lte, equals, not_equals, gt, lt)
conditions[].valuestring,numbernoRequired except for is_set, not_set, ever, and never. A number of days for in_last_days / not_in_last_days, a number for gte, lte, gt, lt, a date (YYYY-MM-DD) for before / after.
ReturnsThe segment with its count.
Example tools/call params
{
  "name": "create_segment",
  "arguments": {
    "name": "Engaged pro customers",
    "conditions": [
      {
        "field": "fields.plan",
        "op": "equals",
        "value": "pro"
      },
      {
        "field": "opened",
        "op": "in_last_days",
        "value": 90
      }
    ]
  }
}
Changes stateupdate_segment
PATCH /segments/:segment_id

Update a segment

Campaigns: Rename a segment or change match or conditions (which replaces the whole list). Broadcasts using it pick up the change when they start sending.

ParameterTypeRequiredDescription
segment_idstringyesSegment ID (starts with seg_), as returned by list_segments or create_segment. (max 128 chars)
namestringnoSegment name. (max 120 chars)
matchstringnoall (default): every condition must match. any: at least one. (one of all, any)
conditionsobject[]noUp to 20 conditions. Opened and clicked use bot-filtered, estimated engagement. (0–20 items)
conditions[].fieldstringyestag, list, status, source, email, name, signed_up, opened, clicked, opens, sends_without_open, country, or a custom field as fields.<key> (or field together with key). (max 48 chars)
conditions[].keystringnoCustom field key when field is field. (max 40 chars)
conditions[].opstringyestag: has, not_has. list: in, not_in (a list ID). status: is, is_not (subscribed, pending, unsubscribed, bounced, complained, dormant). source: is, is_not (form, api, import). email: contains, not_contains, ends_with. name: is_set, not_set. signed_up: before, after (YYYY-MM-DD), in_last_days, not_in_last_days. opened / clicked: in_last_days, not_in_last_days, ever, never. opens / sends_without_open: gte, lte. country: is, is_not (two-letter code). Custom fields: equals, not_equals, contains, gt, lt, before, after, is_set, not_set. (one of has, not_has, in, not_in, is, is_not, contains, not_contains, ends_with, is_set, not_set, before, after, in_last_days, not_in_last_days, ever, never, gte, lte, equals, not_equals, gt, lt)
conditions[].valuestring,numbernoRequired except for is_set, not_set, ever, and never. A number of days for in_last_days / not_in_last_days, a number for gte, lte, gt, lt, a date (YYYY-MM-DD) for before / after.
ReturnsThe updated segment with its count.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_segment",
  "arguments": {
    "segment_id": "seg_123",
    "name": "Pro customers"
  }
}
Destructivedelete_segment
DELETE /segments/:segment_id

Delete a segment

DESTRUCTIVE: delete a saved segment. Contacts are not affected. Fails with 409 while a sending or scheduled broadcast uses it. Part of Campaigns.

ParameterTypeRequiredDescription
segment_idstringyesSegment ID (starts with seg_), as returned by list_segments or create_segment. (max 128 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_segment",
  "arguments": {
    "segment_id": "seg_123"
  }
}
Read-onlylist_lists
GET /lists

List contact lists

Campaigns: Contact lists with subscribed, pending, and unsubscribed counts and their hosted signup form settings.

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: [lists], count, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_lists",
  "arguments": {}
}
Changes statecreate_list
POST /lists

Create a list

Campaigns: Create a contact list. Set form_enabled to publish a hosted double opt-in signup form (its URL is in form.url); form_from is the sender, on a verified domain, of the confirmation email, and redirect_url is an optional https page shown after confirming. welcome sends one welcome email when someone confirms. site publishes a public newsletter site at https://sendhq.cc/n/<slug> with the signup form, sent issues, and RSS.

ParameterTypeRequiredDescription
namestringyesList name, shown on the signup form. (max 200 chars)
descriptionstringnoShort description shown on the signup form. (max 500 chars)
form_enabledbooleannoPublish the hosted signup form.
form_fromstringnoSender of signup confirmation emails, e.g. Acme <news@example.com>, on a verified domain. (max 320 chars)
redirect_urlstringnohttps page to open after a signup is confirmed. (max 500 chars)
welcomeobjectnoWelcome email sent once, from form_from, when someone confirms a signup. Needs subject and html or markdown when enabled.
welcome.enabledbooleannoSend it.
welcome.subjectstringnoSubject; merge tags allowed. (max 300 chars)
welcome.htmlstringnoHTML body; merge tags allowed. (max 500000 chars)
welcome.markdownstringnoMarkdown body instead of html, rendered with the workspace design. (max 200000 chars)
siteobjectnoPublic newsletter site at https://sendhq.cc/n/<slug>: a signup form, every issue sent with web.enabled, and an RSS feed at /n/<slug>/feed.xml. Needs the signup form on. A reserved or taken slug fails with 409.
site.enabledbooleannoPublish the site.
site.slugstring,nullnoAddress after /n/ (3-48 characters, a-z, 0-9, -). Defaults to the list name. (max 48 chars)
site.titlestring,nullnoSite title. Defaults to the list name. (max 120 chars)
site.descriptionstring,nullnoShort description under the title. (max 300 chars)
site.accentstring,nullnoAccent color, e.g. #0a7d4f.
ReturnsThe list.
Example tools/call params
{
  "name": "create_list",
  "arguments": {
    "name": "Product updates"
  }
}
Read-onlyget_list
GET /lists/:list_id

Get a list

Campaigns: One list with member counts and signup form settings.

ParameterTypeRequiredDescription
list_idstringyesList ID (starts with lst_), as returned by a list or create tool. (max 128 chars)
ReturnsThe list.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_list",
  "arguments": {
    "list_id": "lst_123"
  }
}
Changes stateupdate_list
PATCH /lists/:list_id

Update a list

Campaigns: Rename a list, change its description, or change its hosted signup form, welcome email, or newsletter site. Omitted keys inside welcome and site keep their value.

ParameterTypeRequiredDescription
list_idstringyesList ID (starts with lst_), as returned by a list or create tool. (max 128 chars)
namestringnoNew name. (max 200 chars)
descriptionstringnoNew description. (max 500 chars)
form_enabledbooleannoPublish or unpublish the hosted signup form.
form_fromstringnoSender of signup confirmation emails, on a verified domain. (max 320 chars)
redirect_urlstringnohttps page to open after a signup is confirmed. (max 500 chars)
welcomeobjectnoWelcome email sent once, from form_from, when someone confirms a signup. Needs subject and html or markdown when enabled.
welcome.enabledbooleannoSend it.
welcome.subjectstringnoSubject; merge tags allowed. (max 300 chars)
welcome.htmlstringnoHTML body; merge tags allowed. (max 500000 chars)
welcome.markdownstringnoMarkdown body instead of html, rendered with the workspace design. (max 200000 chars)
siteobjectnoPublic newsletter site at https://sendhq.cc/n/<slug>: a signup form, every issue sent with web.enabled, and an RSS feed at /n/<slug>/feed.xml. Needs the signup form on. A reserved or taken slug fails with 409.
site.enabledbooleannoPublish the site.
site.slugstring,nullnoAddress after /n/ (3-48 characters, a-z, 0-9, -). Defaults to the list name. (max 48 chars)
site.titlestring,nullnoSite title. Defaults to the list name. (max 120 chars)
site.descriptionstring,nullnoShort description under the title. (max 300 chars)
site.accentstring,nullnoAccent color, e.g. #0a7d4f.
ReturnsThe updated list.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_list",
  "arguments": {
    "list_id": "lst_123",
    "form_enabled": false
  }
}
Destructivedelete_list
DELETE /lists/:list_id

Delete a list

DESTRUCTIVE: delete a list and its memberships. The contacts themselves are kept. Fails with 409 while a broadcast to the list is sending. Part of Campaigns.

ParameterTypeRequiredDescription
list_idstringyesList ID (starts with lst_), as returned by a list or create tool. (max 128 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_list",
  "arguments": {
    "list_id": "lst_123"
  }
}
Changes stateadd_list_contacts
POST /lists/:list_id/contacts

Add contacts to a list

Campaigns: Add up to 500 existing contacts to a list by ID or email. Subscribed contacts join subscribed; others join in their current state. Unknown references come back in notFound.

ParameterTypeRequiredDescription
list_idstringyesList ID (starts with lst_), as returned by a list or create tool. (max 128 chars)
contact_idsstring[]noContact IDs (ct_…). (0–500 items)
emailsstring[]noContact email addresses. (0–500 items)
Returns{listId, added, notFound, list}.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "add_list_contacts",
  "arguments": {
    "list_id": "lst_123",
    "emails": [
      "ada@example.net"
    ]
  }
}
Destructiveremove_list_contact
DELETE /lists/:list_id/contacts/:contact_id

Remove a contact from a list

DESTRUCTIVE: remove one contact from a list. The contact and its status are kept. Part of Campaigns.

ParameterTypeRequiredDescription
list_idstringyesList ID (starts with lst_), as returned by a list or create tool. (max 128 chars)
contact_idstringyesContact ID (starts with ct_) or the contact's email address. (max 320 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "remove_list_contact",
  "arguments": {
    "list_id": "lst_123",
    "contact_id": "ct_123"
  }
}
Read-onlylist_broadcasts
GET /broadcasts

List broadcasts

Campaigns: Newest-first broadcasts with their status. Paginated: the result includes pagination.

ParameterTypeRequiredDescription
statusstringnoOnly broadcasts with this status. (one of draft, sending, sent, cancelled, failed)
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: [broadcasts], count, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_broadcasts",
  "arguments": {}
}
Changes statecreate_broadcast
POST /broadcasts

Create a broadcast draft

Campaigns: Create a draft broadcast to one list. Content is html and/or text, markdown, a block document in content, or a published hosted template. Raw content supports merge tags: {{first_name}}, {{name}}, {{email}}, {{fields.plan}}, with fallbacks like {{first_name | there}}. audience narrows it to a segment and/or a number of people (most engaged first with order: engaged); tracking turns open and click tracking off; web publishes it on the list's newsletter site once sent. Nothing is sent until send_broadcast or schedule_broadcast; run check_broadcast and send_broadcast_test first.

ParameterTypeRequiredDescription
namestringnoInternal name. Defaults to the subject. (max 200 chars)
list_idstringnoThe list to send to (lst_…).
fromstringnoSender on a verified domain, e.g. Acme <news@example.com>. (max 320 chars)
reply_tostringnoReply-To address. (max 320 chars)
subjectstringnoSubject line; merge tags allowed. (max 998 chars)
preview_textstringnoInbox preview text shown after the subject. (max 300 chars)
htmlstringnoHTML body; merge tags allowed and escaped.
textstringnoPlain-text body; merge tags allowed.
templateobjectnoA published hosted template instead of subject/html/text. Template variables are filled from the contact's name, email, and fields.
template.idstringno
template.keystringno
template.version_idstringno
markdownstringnoMarkdown body instead of html: converted to a block document and rendered with the workspace design (get_newsletter_settings). Merge tags allowed. Not combinable with html or content. (max 1000000 chars)
contentobject,nullnoBlock document ({"type": "doc", "content": [...]}, the dashboard editor's format: paragraph, heading, bulletList, orderedList, blockquote, image, button, callout, horizontalRule, codeBlock). Rendered with the workspace design unless html is also sent.
editorstringnoWhich editor the dashboard opens: html or blocks. Set automatically for markdown and content. (one of html, blocks)
audienceobjectnoWho gets it, within the list. Subscribed, non-dormant members only.
audience.segment_idstring,nullnoOnly list members matching this segment (seg_…), or null for the whole list.
audience.orderstringnoWith a limit: signup (default) or engaged (most engaged first). (one of signup, engaged)
audience.limitinteger,nullnoSend to at most this many people (send_broadcast_to_more sends to more later), or null for everyone. (1–10000000)
trackingobjectnoOpen and click tracking, both on by default. Opens are estimates (bot-filtered pixel loads), never proof that someone read the email.
tracking.opensbooleannoAdd an open pixel.
tracking.clicksbooleannoRewrite links to signed SendHQ redirects (the unsubscribe link never is).
webobjectnoPublish this issue on the list's newsletter site (see create_list site) once it is sent.
web.enabledbooleannoPublish it.
web.slugstring,nullnoIssue address; defaults to the subject. (max 100 chars)
web.pinnedbooleannoKeep it at the top of the site.
ReturnsThe draft broadcast.
Example tools/call params
{
  "name": "create_broadcast",
  "arguments": {
    "list_id": "lst_123",
    "from": "news@example.com",
    "subject": "October update",
    "markdown": "Hi {{first_name | there}}, here's what shipped.",
    "audience": {
      "order": "engaged",
      "limit": 1000
    }
  }
}
Read-onlyget_broadcast
GET /broadcasts/:broadcast_id

Get a broadcast

Campaigns: One broadcast with its content and, once sending, recipient totals (queued, sending, sent, held, skipped, failed). While a limit or reputation pause holds delivery, lastError explains why and nextAttemptAt says when it resumes.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
ReturnsThe broadcast.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Changes stateupdate_broadcast
PATCH /broadcasts/:broadcast_id

Edit a draft broadcast

Campaigns: Change any field of an unscheduled draft broadcast (content, audience, tracking, web). A scheduled draft must be unscheduled first (unschedule_broadcast); sent or sending broadcasts cannot be edited.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
namestringnoInternal name. (max 200 chars)
list_idstringnoThe list to send to.
fromstringnoSender on a verified domain. (max 320 chars)
reply_tostringnoReply-To address. (max 320 chars)
subjectstringnoSubject line. (max 998 chars)
preview_textstringnoInbox preview text. (max 300 chars)
htmlstringnoHTML body.
textstringnoPlain-text body.
templateobject,nullnoA published hosted template, or null to remove it.
markdownstringnoMarkdown body instead of html: converted to a block document and rendered with the workspace design (get_newsletter_settings). Merge tags allowed. Not combinable with html or content. (max 1000000 chars)
contentobject,nullnoBlock document ({"type": "doc", "content": [...]}, the dashboard editor's format: paragraph, heading, bulletList, orderedList, blockquote, image, button, callout, horizontalRule, codeBlock). Rendered with the workspace design unless html is also sent.
editorstringnoWhich editor the dashboard opens: html or blocks. Set automatically for markdown and content. (one of html, blocks)
audienceobjectnoWho gets it, within the list. Subscribed, non-dormant members only.
audience.segment_idstring,nullnoOnly list members matching this segment (seg_…), or null for the whole list.
audience.orderstringnoWith a limit: signup (default) or engaged (most engaged first). (one of signup, engaged)
audience.limitinteger,nullnoSend to at most this many people (send_broadcast_to_more sends to more later), or null for everyone. (1–10000000)
trackingobjectnoOpen and click tracking, both on by default. Opens are estimates (bot-filtered pixel loads), never proof that someone read the email.
tracking.opensbooleannoAdd an open pixel.
tracking.clicksbooleannoRewrite links to signed SendHQ redirects (the unsubscribe link never is).
webobjectnoPublish this issue on the list's newsletter site (see create_list site) once it is sent.
web.enabledbooleannoPublish it.
web.slugstring,nullnoIssue address; defaults to the subject. (max 100 chars)
web.pinnedbooleannoKeep it at the top of the site.
ReturnsThe updated draft.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_broadcast",
  "arguments": {
    "broadcast_id": "bc_123",
    "subject": "October product update"
  }
}
Destructivedelete_broadcast
DELETE /broadcasts/:broadcast_id

Delete a draft broadcast

DESTRUCTIVE: permanently delete a draft broadcast. Broadcasts that were sent cannot be deleted. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
Returns{ok: true}.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "delete_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Sends real emailsend_broadcast
POST /broadcasts/:broadcast_id/send

Send a broadcast

SENDS REAL EMAIL to the broadcast's audience: every subscribed, non-dormant member of its list, narrowed by its segment and limit. Only call it when the user asked to send this broadcast now. Each message goes through the normal checks (verified domain, marketing plan, suppressions, unsubscribes, reputation, daily and monthly limits) and carries one-click unsubscribe from this list; members that cannot be mailed are skipped. Each contact is sent at most once. Delivery continues in the background: poll get_broadcast for totals and get_broadcast_report for engagement. A scheduled broadcast fails with 409 until unscheduled. Unpaid workspaces (integration trial) can send only to the account email. Pass idempotency_key so a retry cannot start the send twice. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
idempotency_keystringnoIdempotency-Key header (max 200 chars). Reuse it only to retry this exact request. (max 200 chars)
ReturnsThe broadcast with status sending and totals.
Example tools/call params
{
  "name": "send_broadcast",
  "arguments": {
    "broadcast_id": "bc_123",
    "idempotency_key": "october-update"
  }
}
Destructivecancel_broadcast
POST /broadcasts/:broadcast_id/cancel

Cancel a broadcast

DESTRUCTIVE: stop a draft or sending broadcast for good. Recipients not yet sent are marked skipped; messages already handed to the provider are not recalled. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
ReturnsThe cancelled broadcast with totals.
AnnotationsdestructiveHint idempotentHint
Example tools/call params
{
  "name": "cancel_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Read-onlylist_broadcast_recipients
GET /broadcasts/:broadcast_id/recipients

List broadcast recipients

Campaigns: Per-recipient outcomes and engagement of a broadcast: queued, sending, sent, held, skipped (with the reason), or failed (with the error), the inbox provider, delivered/opened/clicked/bounced/complained/unsubscribed times, and the email ID to inspect delivery events with list_email_events. Filter by outcome, by activity (who opened, clicked, did not open, ...), or by email. Opens are bot-filtered estimates, never proof of reading. Paginated.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
statusstringnoOnly recipients with this outcome. (one of queued, sending, sent, held, skipped, failed)
activitystringnoOnly recipients who did this. (one of opened, clicked, not_opened, not_delivered, bounced, complained, unsubscribed)
querystringnoRecipient email contains this text. (max 200 chars)
includestringnolinks adds the links each recipient clicked (implied by activity: clicked). (one of links)
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: [{contactId, email, status, skipReason, emailId, error, provider, deliveredAt, openedAt, clickedAt, opens, clicks, bouncedAt, complainedAt, unsubscribedAt, links?}], count, totals, pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_broadcast_recipients",
  "arguments": {
    "broadcast_id": "bc_123",
    "status": "failed"
  }
}
Read-onlycheck_broadcast
GET /broadcasts/:broadcast_id/checks

Run a broadcast's pre-send checks

Campaigns: The review list shown before sending: sender verification, subject, preview text, content, audience size, size against Gmail's 102 KB clipping limit, postal address, merge tags without data or fallback, empty or shortened links, image descriptions, placeholder text, spam-prone wording, the trial's own-address rule, and whether a test was sent. ready is false while any check has level block; schedule_broadcast refuses then. Fix warn items or tell the user about them.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
Returns{ready, checks: [{id, level: ok|warn|block, label, detail}], audience: {total, matching}}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "check_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Sends real emailsend_broadcast_test
POST /broadcasts/:broadcast_id/test

Send a broadcast test

SENDS REAL EMAIL: a copy of the draft marked [Test] to up to 5 addresses (default: the saved test recipients, or the account email), with merge tags filled from contact_id or a sample contact. Never goes to the list. Only send to addresses the user gave or owns. Limited to 100 test sends a day. Template broadcasts are tested with send_template_test. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
tostring[]noTest recipients (up to 5). (1–5 items)
contact_idstringnoFill merge tags from this contact (ct_…). (max 320 chars)
rememberbooleannoSave to as the default test recipients.
Returns{data: [{to, id, status: sent|failed, error?}]}.
Example tools/call params
{
  "name": "send_broadcast_test",
  "arguments": {
    "broadcast_id": "bc_123",
    "to": [
      "owner@example.com"
    ]
  }
}
Sends real emailschedule_broadcast
POST /broadcasts/:broadcast_id/schedule

Schedule a broadcast

SENDS REAL EMAIL at scheduled_at to the broadcast's audience, with the same checks as send_broadcast. Only call it when the user asked to send this broadcast at that time. The pre-send checks run now (422 with checks if one blocks). It starts within about a minute of the time; if it can no longer send then, it returns to a draft with the reason in lastError. A scheduled draft cannot be edited until unschedule_broadcast. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
scheduled_atstringyesWhen to send: ISO 8601 date-time with a timezone, at least a minute and at most a year ahead, e.g. 2026-11-10T09:00:00Z. (max 40 chars)
ReturnsThe broadcast with scheduledAt.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "schedule_broadcast",
  "arguments": {
    "broadcast_id": "bc_123",
    "scheduled_at": "2026-11-10T09:00:00Z"
  }
}
Changes stateunschedule_broadcast
POST /broadcasts/:broadcast_id/unschedule

Unschedule a broadcast

Campaigns: Return a scheduled broadcast to an editable draft so it does not send. Fails with 409 when it is not scheduled.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
ReturnsThe draft broadcast.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "unschedule_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Changes stateduplicate_broadcast
POST /broadcasts/:broadcast_id/duplicate

Duplicate a broadcast

Campaigns: Copy any broadcast (draft, sent, or cancelled) into a new draft named <name> (copy) with the same list, sender, content, audience, and tracking settings. Nothing is sent.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
ReturnsThe new draft broadcast.
Example tools/call params
{
  "name": "duplicate_broadcast",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Sends real emailsend_broadcast_to_more
POST /broadcasts/:broadcast_id/send-more

Send a broadcast to more people

SENDS REAL EMAIL: sends a sent or sending broadcast to up to limit more people in its audience who have not received it (the rest of a limited audience, or people who joined since), in its audience order. Only call it when the user asked for this. Same checks as send_broadcast; each contact still gets it at most once. Fails with 422 when everyone already got it. Pass idempotency_key so a retry cannot add people twice. Part of Campaigns.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
limitintegeryesHow many more people at most. (1–10000000)
idempotency_keystringnoIdempotency-Key header (max 200 chars). Reuse it only to retry this exact request. (max 200 chars)
Returns{added, broadcast}.
Example tools/call params
{
  "name": "send_broadcast_to_more",
  "arguments": {
    "broadcast_id": "bc_123",
    "limit": 500,
    "idempotency_key": "october-update-more-1"
  }
}
Read-onlyget_broadcast_report
GET /broadcasts/:broadcast_id/report

Get a broadcast report

Campaigns: Engagement for a sending or sent broadcast: unique and total opens and clicks, bounces, complaints, and unsubscribes with rates in percent, the same per inbox provider, clicks per link, first opens and clicks by hour, and the average of the previous five broadcasts. Opens are estimates: automated requests (scanners, link previews, privacy prefetching) are counted only in machine. Never claim that a person read the email. Fails with 409 for a draft.

ParameterTypeRequiredDescription
broadcast_idstringyesBroadcast ID (starts with bc_), as returned by a list or create tool. (max 128 chars)
Returns{stats: {sent, delivered, opened, clicked, bounced, complained, unsubscribed, opens, clicks, rates}, providers, links, timeline, average, machine, tracking, totals, remaining}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_broadcast_report",
  "arguments": {
    "broadcast_id": "bc_123"
  }
}
Read-onlyget_newsletter_settings
GET /newsletter-settings

Get newsletter settings

Campaigns: The postal address added to every marketing email footer, the design (accent color, font, alignment) used for Markdown and block content, list cleanup settings, and default test recipients.

No parameters.

Returns{postalAddress, design: {accentColor, font, align}, cleanup: {enabled, afterSends, quietDays, subject, body, from}, testRecipients}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_newsletter_settings",
  "arguments": {}
}
Changes stateupdate_newsletter_settings
PATCH /newsletter-settings

Update newsletter settings

Campaigns: Change the postal address (required in marketing email by CAN-SPAM and CASL), design, list cleanup, or default test recipients. Omitted keys keep their value. List cleanup is on by default and sends email automatically: a subscriber who got afterSends broadcasts in a row without opening or clicking and has been quiet for quietDays gets one "still want these?" email; a week later those who did not confirm become dormant (kept, but skipped by broadcasts). Confirm with the user before turning it on or changing its email.

ParameterTypeRequiredDescription
postal_addressstringnoSender's postal address for the footer. Empty string removes it. (max 300 chars)
designobjectnoRendering design for Markdown and block content.
design.accentColorstring,nullnoHex color like #0a7d4f, or null for the default.
design.fontstringnoFont family. (one of sans, serif, mono)
design.alignstringnoText alignment. (one of left, center)
cleanupobjectnoList cleanup of readers who stopped opening.
cleanup.enabledbooleannoTurn list cleanup on or off.
cleanup.afterSendsintegernoBroadcasts in a row without an open or click (3-50). (3–50)
cleanup.quietDaysintegernoDays without engagement (30-730). (30–730)
cleanup.subjectstring,nullnoSubject of the cleanup email; merge tags allowed. (max 200 chars)
cleanup.bodystring,nullnoBody text of the cleanup email. (max 2000 chars)
cleanup.fromstring,nullnoSender on a verified domain. Defaults to the last sent broadcast's sender. (max 320 chars)
test_recipientsstring[]noDefault addresses for send_broadcast_test (up to 5). (0–5 items)
ReturnsThe updated settings.
AnnotationsidempotentHint
Example tools/call params
{
  "name": "update_newsletter_settings",
  "arguments": {
    "postal_address": "Acme Inc., 1 Main St, Springfield",
    "design": {
      "accentColor": "#0a7d4f"
    }
  }
}
Read-onlyget_campaign_overview
GET /campaigns/overview

Get campaign overview

Campaigns: Subscriber growth and recent results for the last days (7-365, default 30): active, dormant, and pending subscribers, joined and left, a daily series, and the six latest broadcasts with open and click rates (bot-filtered estimates) and their average.

ParameterTypeRequiredDescription
daysintegernoWindow in days, 7 to 365. Defaults to 30. (7–365)
Returns{subscribers: {active, dormant, pending, joined, left}, series: [{day, subscribers, joined, left}], recent: [{id, name, subject, status, startedAt, sent, openRate, clickRate}], averages}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_campaign_overview",
  "arguments": {
    "days": 30
  }
}
Read-onlylist_uploaded_images
GET /uploads/images

List uploaded images

Campaigns: The workspace's 60 most recent editor image uploads with their public URLs (https://sendhq.cc/i/<id>), to reuse in broadcast HTML, Markdown, or image blocks. Uploading needs the dashboard or the REST API (raw image body). Paginated.

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, url, width, height, size, filename, createdAt}], pagination}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_uploaded_images",
  "arguments": {}
}

Account, usage, analytics, and keys

Read-onlyget_account
GET /account

Get account, usage, and billing

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}}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_account",
  "arguments": {}
}
Read-onlyget_account_diagnostics
GET /account/diagnostics

Diagnose sending and receiving

One read that explains what is wrong with sending or receiving in this workspace: an issues list (unverified domains and missing DKIM records, inbound MX not ready, paused or throttled senders, quota or daily limit reached, trial restrictions, high bounce or complaint rates, failing webhooks), plan and usage against limits, each domain with its DKIM and inbound state, sending identities, 30-day deliverability and sender reputation, suppressions and unsubscribes, and the last 30 days of messages that were not sent (with reasons) plus bounce, complaint and reject events. Start here when a user asks why mail is not arriving. Never returns message bodies or secrets.

No parameters.

Returns{generatedAt, windowDays, issues: [{severity, code, message}], plan: {tier, usage, sendingLimit}, domains: [{name, status, dkim: {required, verified, missing}, inbound}], sendingIdentities, inbound, deliverability: {sent, delivered, bounced, complained, rates, senders}, suppressions: {total, byReason, unsubscribes, recent}, failures: {notSent, recentNotSent, recentProviderEvents}, webhooks}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_account_diagnostics",
  "arguments": {}
}
Read-onlyget_analytics
GET /analytics

Get sending 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "get_analytics",
  "arguments": {
    "days": 30
  }
}
Read-onlylist_api_keys
GET /keys

List API key metadata

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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "name": "list_api_keys",
  "arguments": {}
}
Read-onlyget_service_health
GET /health

Check SendHQ service 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}.
AnnotationsreadOnlyHint idempotentHint
Example tools/call params
{
  "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 domain and DNS verification
POST /domains/:id/inbound/setupsetup_inboundProvision 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 /webhooksnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
POST /webhooksnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
GET /webhooks/:idnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
PATCH /webhooks/:idnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
DELETE /webhooks/:idnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
POST /webhooks/:id/rotate-secretnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
POST /webhooks/:id/testnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
GET /webhooks/:id/deliveriesnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
GET /webhooks/:id/deliveries/:deliveryIdnot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
POST /webhooks/:id/deliveries/:deliveryId/retrynot exposedWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
GET /contactslist_contactsList contacts
POST /contactscreate_contactAdd a contact with its consent basis
POST /contacts/bulkbulk_update_contactsTag, move, unsubscribe, or delete many contacts at once
GET /contacts/exportnot exposedImage uploads take a raw binary body and the export returns a CSV file meant for a person to download. Agents reuse uploaded images with list_uploaded_images and read the same contacts, filters, tags, and fields as JSON with list_contacts (paginated).
GET /contacts/:idget_contactRetrieve a contact by ID or email
PATCH /contacts/:idupdate_contactUpdate a contact's name, fields, or tags, or unsubscribe it
DELETE /contacts/:iddelete_contactDelete a contact and its list memberships
POST /contact-importsimport_contactsImport contacts from CSV
GET /contact-importslist_contact_importsList recent contact imports
GET /tagslist_tagsList contact tags with how many contacts carry each
GET /contact-fieldslist_contact_fieldsList custom contact field definitions
PUT /contact-fields/:keyset_contact_fieldDefine or change a custom contact field
DELETE /contact-fields/:keydelete_contact_fieldDelete a custom field definition
GET /segmentslist_segmentsList saved segments with live counts
POST /segmentscreate_segmentSave a segment
POST /segments/previewpreview_segmentCount and sample the contacts that match unsaved conditions
GET /segments/:idget_segmentRetrieve a segment and its live count
PATCH /segments/:idupdate_segmentRename a segment or change its conditions
DELETE /segments/:iddelete_segmentDelete a segment
GET /listslist_listsList contact lists with member counts
POST /listscreate_listCreate a list with an optional signup form, welcome email, and newsletter site
GET /lists/:idget_listRetrieve a list
PATCH /lists/:idupdate_listRename a list or change its signup form, welcome email, or site
DELETE /lists/:iddelete_listDelete a list without deleting its contacts
POST /lists/:id/contactsadd_list_contactsAdd existing contacts to a list
DELETE /lists/:id/contacts/:contact_idremove_list_contactRemove a contact from a list
GET /broadcastslist_broadcastsList broadcasts
POST /broadcastscreate_broadcastCreate a broadcast draft
GET /broadcasts/:idget_broadcastRetrieve a broadcast and its delivery totals
PATCH /broadcasts/:idupdate_broadcastEdit a draft broadcast
DELETE /broadcasts/:iddelete_broadcastDelete a draft broadcast
GET /broadcasts/:id/checkscheck_broadcastRun the pre-send checks for a broadcast
POST /broadcasts/:id/testsend_broadcast_testSend a test copy of a draft
POST /broadcasts/:id/scheduleschedule_broadcastSchedule a draft to send later
POST /broadcasts/:id/unscheduleunschedule_broadcastUnschedule a draft
POST /broadcasts/:id/duplicateduplicate_broadcastCopy a broadcast into a new draft
POST /broadcasts/:id/sendsend_broadcastSend a broadcast to its audience
POST /broadcasts/:id/send-moresend_broadcast_to_moreSend a sent broadcast to more of its audience
POST /broadcasts/:id/cancelcancel_broadcastCancel a draft or sending broadcast
GET /broadcasts/:id/recipientslist_broadcast_recipientsList a broadcast's recipients, outcomes, and engagement
GET /broadcasts/:id/reportget_broadcast_reportRetrieve a broadcast's engagement report
GET /campaigns/overviewget_campaign_overviewRetrieve subscriber growth and recent broadcast performance
GET /newsletter-settingsget_newsletter_settingsRetrieve newsletter settings
PATCH /newsletter-settingsupdate_newsletter_settingsChange newsletter settings
POST /uploads/imagesnot exposedImage uploads take a raw binary body and the export returns a CSV file meant for a person to download. Agents reuse uploaded images with list_uploaded_images and read the same contacts, filters, tags, and fields as JSON with list_contacts (paginated).
GET /uploads/imageslist_uploaded_imagesList recently uploaded images
GET /accountget_accountRetrieve account, usage, billing state, and workspace counts with an API key
GET /account/diagnosticsget_account_diagnosticsDiagnose sending and receiving problems in the workspace
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.
Webhook endpoints and deliveriesGET /webhooks, POST /webhooks, GET /webhooks/:id, PATCH /webhooks/:id, DELETE /webhooks/:id, POST /webhooks/:id/rotate-secret, POST /webhooks/:id/test, GET /webhooks/:id/deliveries, GET /webhooks/:id/deliveries/:deliveryId, POST /webhooks/:id/deliveries/:deliveryId/retryWebhook endpoints are configured once by a human, in the dashboard or the REST API, and their responses carry signing secrets an agent should not hold. An agent reads the same outcomes with list_email_events, list_emails, and list_blocked_recipients.
Upload editor images and export contacts as CSVPOST /uploads/images, GET /contacts/exportImage uploads take a raw binary body and the export returns a CSV file meant for a person to download. Agents reuse uploaded images with list_uploaded_images and read the same contacts, filters, tags, and fields as JSON with list_contacts (paginated).

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.