Announcing CIMD support for MCP Client registration
Learn more

Should you use Postmark MCP or Postmark API for building AI Agents?

TL;DR

  • The official Postmark MCP server ships 24 tools across sending, templates, message search, delivery diagnostics, bounces, suppressions, stats, and webhooks. The REST API exposes fifteen endpoint groups. Inbound email, domains, sender signatures, message streams, the Bulk API, and server management have no MCP tool at all.
  • Postmark has no OAuth. Both paths authenticate with the same static header credential: X-Postmark-Server-Token or X-Postmark-Account-Token. Neither token type supports sub-scoped permissions, so a Server Token grants full access to every operation on the server it belongs to.
  • The official MCP server is distributed as a local stdio process configured through POSTMARK_SERVER_TOKEN in your MCP client config. There is no Postmark-hosted remote MCP endpoint, which makes it a poor fit for a server-side multi-tenant agent.
  • diagnoseDelivery is the strongest argument for the MCP path: it fans out message search, suppression lookup, and bounce history in parallel and returns a recommended action. The raw API makes you compose that yourself.
  • The credential problem is identical either way, and it is not token refresh. It is custody and rotation of long-lived, full-access server tokens, one per tenant. Scalekit's Postmark connector holds those tokens in a vault and resolves the right one per tool call, so the MCP versus API choice does not change your auth infrastructure.

Two paths to Postmark, one credential model

Your agent needs to send transactional email, then answer the question every support thread eventually asks: did it actually arrive, and if not, why. Postmark ships an official MCP server and a REST API that has been stable for years. They cover overlapping but not identical ground, and unlike Notion or Slack, the choice between them does not change your auth model at all. That last part is the interesting bit, and it is where most teams get the architecture wrong. Here is the decision framework.

What Postmark MCP and Postmark API actually are

Two objects, one credential model. Establishing both precisely matters, because the differences show up in capability coverage and deployment shape, not in authentication.

The official Postmark MCP server

The Postmark MCP server is maintained by ActiveCampaign and published to npm as @activecampaign/postmark-mcp. It started in Postmark Labs with four tools and reached v2.1.1 with 24 tools across eight categories in July 2026. Node.js v20 or higher is required.

You run it as a local process. The documented configuration is npx -y @activecampaign/postmark-mcp in your MCP client config, with POSTMARK_SERVER_TOKEN, DEFAULT_SENDER_EMAIL, and DEFAULT_MESSAGE_STREAM supplied as environment variables. Every tool carries readOnlyHint, destructiveHint, and idempotentHint annotations so a client can auto-approve reads and gate sends. The source is on the official postmark-mcp repository.

The Postmark REST API

The Postmark REST API is a JSON API over HTTPS at api.postmarkapp.com, organized into fifteen endpoint groups: Email, Bulk, Bounce, Templates, Server, Servers, Message Streams, Messages, Domains, Sender signatures, Stats, Inbound rule triggers, Webhooks, Suppressions, and Data Removal.

Authentication is header based. Server-level operations use X-Postmark-Server-Token; account-level operations such as creating servers or managing domains use X-Postmark-Account-Token. There are no LLM affordances here: schema handling, pagination, per-message error inspection, and retry logic are all yours.

Why this pairing is unlike Notion or Slack

For most tools in this series, the MCP server introduces an OAuth path that the API did not require. Postmark does not work that way. Both paths use the same two static tokens, which means the MCP decision is purely about capability surface and deployment shape. That simplifies one thing and complicates another, as the next section shows.

Comparing them where it matters for agents

Four dimensions decide this: what the agent can do, how it authenticates, what breaks in production, and which workloads each path actually suits.

What your agent can actually do

The MCP server covers the outbound sending and deliverability loop well. Everything that touches account configuration, inbound mail, or stream topology lives only in the API.

Capability
Postmark MCP server (v2.1.1)
Postmark REST API
Send a single transactional email
Yes, sendEmail, up to 50 recipients
Yes, POST /email
Send using a saved template
Yes, sendEmailWithTemplate
Yes
Batch send, distinct message per recipient
Yes, sendBatch, up to 500
Yes, POST /email/batch
Bulk send, one message to a large list
No
Yes, POST /email/bulk, approval required
Template create, edit, delete, validate
Yes
Yes
Outbound message search and event timeline
Yes
Yes
Raw outbound SMTP message dump
No
Yes
One-call delivery diagnosis
Yes, diagnoseDelivery
No, compose it yourself
Bounce log, bounce dump, suppressions
Yes
Yes
Delivery, open, and click statistics
Yes, getDeliveryStats
Yes, Stats API
Webhook list, create, delete
Yes
Yes
Webhook get, edit, verify, statistics
No
Yes
Inbound message search, retry, bypass, rules
No
Yes
Message streams, domains, sender signatures, servers
No
Yes

Where the gap actually bites

Two of those gaps are architectural rather than temporary. Inbound email is an entire half of Postmark's product with its own message search, processing statuses, retry semantics, and blocking rules; none of it is reachable through MCP. Message stream management is the other: a server holds up to 10 streams, and creating, archiving, or reconfiguring one is an API-only operation.

The Bulk API gap is worth calling out separately. sendBatch sends up to 500 distinct messages. The Bulk API sends one message body to a large recipient list with per-recipient template variables, using a submit-and-poll workflow. The MCP server does not wrap it.

The credit the MCP server earns

diagnoseDelivery is genuinely better than what the API gives you. Answering "did this reach the recipient" through the raw API means a message search, a message detail lookup, a suppression check, and a bounce query, then reconciling four payloads. The MCP tool runs those in parallel, tolerates individual failures, and returns a recommended action that varies by suppression reason: SpamComplaint is permanent, HardBounce may be reactivatable, ManualSuppression can simply be deleted.

That is outcome-shaped tool design, not endpoint wrapping. If your agent's job is deliverability triage, it is a real reason to reach for the MCP path.

The auth path each one puts you on

Neither path involves OAuth, browser consent, refresh tokens, or per-user delegation. Postmark's API authentication model defines exactly two credentials, both sent as HTTP headers, both long lived, and neither sub-scopable.

A Server Token authorizes every operation on one Postmark server: sends, template deletion, webhook registration, suppression edits. An Account Token authorizes account-level operations across servers. The official repo is explicit that neither type supports narrower permissions, and recommends a structural workaround: create a dedicated Postmark server used only for agent traffic, so a compromise is bounded to that server's data.

The token model has no user in it

This is the point that determines your production posture. A Postmark credential does not represent a person. It represents a sending server. So the identity question for a Postmark agent is not "which user authorized this," it is "which tenant's sending infrastructure is this agent currently acting on."

For a single-product team sending its own mail, one token is fine. For an agency, a platform, or any B2B product where each customer brings their own Postmark server, you have N long-lived full-access secrets to hold. That shape is closer to multi-tenant access control than to user-delegated OAuth.

What you own in production

On the MCP path, ActiveCampaign owns the tool schemas, the validation rules, and the composite diagnostics. You own the process. Because the server is a local stdio binary keyed to one POSTMARK_SERVER_TOKEN, serving multiple tenants means running one subprocess per tenant with a distinct environment, or swapping the env var between runs. Neither pattern survives concurrency.

You also own the log surface. The server emits structured JSON per tool invocation to stderr, with email addresses partially masked unless LOG_EMAIL_FULL is set, and an optional LOG_FILE that has no rotation or size cap.

Rate limits, payload caps, and batch semantics

On the API path you own everything, including three behaviors that catch agents specifically.

Postmark returns HTTP 429 when request rate exceeds acceptable use, without publishing a fixed per-endpoint tier table, so your backoff has to be adaptive rather than pre-tuned. Payload limits are hard: 10 MB for the Email API, 50 MB total for the Batch API, surfaced as HTTP 413.

The batch semantic is the one that silently breaks pipelines. Batch sends return per-message error codes inside an HTTP 200 response. An agent that checks only the status code will report success on a batch where a third of the recipients were rejected as inactive.

When to use Postmark MCP

Reach for the MCP server when a human is in the loop and the Postmark account is yours.

  • You are triaging deliverability from Claude Desktop, Cursor, or a coding agent and want diagnoseDelivery rather than four manual lookups
  • You are building or validating templates conversationally, including layout binding, without opening the template editor
  • You are pulling open, click, bounce, and spam rates filtered by tag or date during an incident review
  • You are prototyping against a single dedicated Postmark server, with destructive tools gated by client-side annotation prompts

When to use the Postmark API

Reach for the API when the agent runs without a human and without a fixed account.

  • Your agent is multi-tenant and each customer supplies their own Postmark server token
  • You need inbound email: searching received messages, retrying failed processing, bypassing blocked messages, managing inbound rules
  • You need the Bulk API for one-message-to-many sends, or message stream lifecycle operations
  • You need domain, DKIM, Return-Path, SPF, or sender signature verification as part of an onboarding agent
  • You need per-message batch result inspection and your own retry policy on 429 and 413

The credential problem that exists on both paths

Both paths hand your agent a static secret and stop. Neither gives you a vault, a rotation schedule, or a revocation flow. For a Postmark agent that problem has a different shape than for an OAuth tool, and the difference matters.

The N-token problem for a multi-tenant Postmark agent

Run one agent across 200 customer Postmark servers and you are holding 200 long-lived tokens, each granting full send, template delete, webhook create, and suppression edit rights on its server. There is no scope you can trim. A leak of the store is a leak of every tenant's sending identity at once.

That is a large blast radius for a credential that usually ends up in an environment variable or a config row.

Why rotation is the hard part, not refresh

OAuth agents fail at refresh. Postmark agents fail at rotation. A server token does not expire on its own, so nothing forces the question; the token from the integration you shipped fourteen months ago is still valid, still full access, and still in whatever store you first put it in.

Rotation is manual: mint a new token in the Postmark dashboard, update every runtime holding the old one, then revoke. Doing that across N tenants without downtime is infrastructure work, and it is the same work whether you chose MCP or the API. For a deeper look at secure token management for AI agents at scale, the patterns apply directly here.

Where Scalekit fits

The Scalekit Postmark connector registers each tenant's Server API token once, stores it in an encrypted token vault, and resolves the correct one on every tool call by identifier. Credentials never enter the agent runtime or the model context.

It exposes 59 tools, which covers the inbound, message stream, webhook update, and Bulk API surface the official MCP server omits. Because tokens are scoped per server, you create one Scalekit connection per Postmark server token, which is also how you get tenant isolation for free.

Connecting an agent to Postmark with Scalekit

Setup is once per environment. Everything after that is tool calls keyed by an identifier you choose.

Set up the connection once

Copy the Server API token from the API Tokens tab of the Postmark server you want to connect. In the Scalekit dashboard, go to AgentKit, then Connections, then Create Connection, find Postmark, paste the token, and save.

The connection name you pick in the dashboard is the string you pass in code. It must match exactly. This is the single most common integration error, and it fails as a not-found rather than an auth error, which sends people looking in the wrong place.

pip install scalekit-sdk-python anthropic
import os import scalekit.client import anthropic from google.protobuf.json_format import MessageToDict scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions client = anthropic.Anthropic()

Retrieve the tools this tenant is authorized to call

Before the agent loop runs, the agent loads the tools bound to this identifier's connected account, not a flat catalog of every Postmark action Scalekit knows about. For a Postmark agent the identifier is usually a tenant key rather than an end user, because the credential represents a sending server.

list_scoped_tools returns those definitions with input_schema already in Anthropic's tool-use format, so no conversion step is needed.

scoped_response, _ = actions.tools.list_scoped_tools( identifier="tenant_acme", filter={"connection_names": ["postmark"]}, page_size=100, # fetch beyond the default page so no connector tools are missed ) llm_tools = [ { "name": MessageToDict(t.tool).get("definition", {}).get("name"), "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), "input_schema": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), } for t in scoped_response.tools ]

Run the agent loop with the Claude SDK

The loop below is a deliverability triage agent: it searches the tenant's outbound history, inspects bounces, and checks the suppression list before recommending an action. execute_tool resolves the tenant's Postmark token at call time.

messages = [{ "role": "user", "content": ( "Did our welcome email reach jordan@example.com in the last 7 days? " "Check bounces and suppressions on the outbound stream, then tell me " "whether to retry or leave it alone." ), }] while True: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, tools=llm_tools, messages=messages, ) if response.stop_reason == "end_turn": print(response.content[0].text) break tool_results = [] for block in response.content: if block.type == "tool_use": result = actions.execute_tool( tool_name=block.name, tool_input=block.input, connection_name="postmark", identifier="tenant_acme", ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(result.data), }) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results})

Naming the tools the agent will reach for

The connector uses a postmark_ prefix and mirrors Postmark's own resource names. The ones a triage agent converges on:

Tool
What it does
postmark_list_outbound_messages
Filter sent messages by recipient, tag, status, stream, and date range
postmark_get_outbound_message
Full detail plus the tracked event timeline for one message
postmark_list_bounces
Paginated bounce log filtered by type, tag, or message ID
postmark_list_suppressions
Suppressions on a stream, filtered by reason and origin
postmark_send_email_with_template
Send a rendered template with a per-recipient model
postmark_get_outbound_stats_overview
Sent, bounce, spam, open, and click rates for a period

Full input schemas live on the Postmark connector docs page.

The same agent in TypeScript

The Node SDK mirrors the Python shape. listScopedTools returns the same definitions; executeTool takes connector where Python takes connection_name.

npm install @scalekit-sdk/node @anthropic-ai/sdk
import { ScalekitClient } from '@scalekit-sdk/node'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); const { tools } = await scalekit.tools.listScopedTools('tenant_acme', { filter: { connectionNames: ['postmark'] }, pageSize: 100, }); const llmTools = tools.map(t => ({ name: t.tool.definition.name, description: t.tool.definition.description, input_schema: t.tool.definition.input_schema, })); const messages: Anthropic.MessageParam[] = [ { role: 'user', content: 'Summarize bounce rate by tag for the last 14 days.' }, ]; while (true) { const response = await anthropic.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools: llmTools, messages, }); if (response.stop_reason === 'end_turn') { const text = response.content.find(b => b.type === 'text'); if (text?.type === 'text') console.log(text.text); break; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await scalekit.actions.executeTool({ toolName: block.name, toolInput: block.input as Record, identifier: 'tenant_acme', connector: 'postmark', }); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data), }); } } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); }

LangChain, if that is your stack

Scalekit returns native StructuredTool objects, so there is no schema reshaping between the connector and the agent. For more on how LangChain tool calling works and where it needs additional infrastructure, see LangChain tool calling: how it works, where it stops.

from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier="tenant_acme", connection_names=["postmark"], page_size=100, ) tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage("List hard bounces on the outbound stream this week.")] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = tool_map[tc["name"]].invoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

More adapters are on the AgentKit code samples pages, including Google ADK, OpenAI, Vercel AI, and Mastra.

Scoping Postmark down with a Virtual MCP server

If you want the MCP shape without the local-subprocess constraint, a Virtual MCP server gives you a hosted, per-tenant endpoint over the same connector.

Define the server once per agent role

A standard MCP server exposes every tool it has. A Virtual MCP server exposes only what you list. For a triage agent that reads deliverability data and never sends, that distinction is the difference between a read-only surface and one that can delete templates.

import os from scalekit import ScalekitClient from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENV_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) vmcp_response = scalekit_client.actions.mcp.create_config( name="postmark-deliverability-triage", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="postmark", tools=[ "postmark_list_outbound_messages", "postmark_get_outbound_message", "postmark_list_bounces", "postmark_list_suppressions", "postmark_get_outbound_stats_overview", ], ), ], ) config_id = vmcp_response.config.id mcp_server_url = vmcp_response.config.mcp_server_url

Mint a session token per run

The mcp_server_url is static and shared. The per-tenant part is the session token, minted fresh before each run and passed as bearer auth. Default expiry is about one hour, and create_session_token is also the remint call; there is no separate refresh endpoint.

from datetime import timedelta token_response = scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier="tenant_acme", expiry=timedelta(hours=1), ) mcp_server = { "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, }

Why this matters for a Postmark agent specifically

The official MCP server hands every connecting client all 24 tools, including four destructive ones and four that send real mail. Its own documentation recommends never auto-approving sends in untrusted environments, which is sound advice and also an admission that the surface is wide by default.

Scoping to five read tools removes that risk class before the model sees it, and cuts the tool definitions loaded into every context window. A server with 40 tools at roughly 200 tokens each burns about 8,000 tokens before the agent does any work.

Multi-tool agents get the same treatment

Most real Postmark agents are not Postmark-only. A support triage agent reads a ticket, checks delivery, and posts a summary. One Virtual MCP server can expose Postmark alongside Slack, Gmail, or Freshdesk, each scoped to its own tool list, with one session token covering the whole set for that tenant.

What you get in the logs

Attribution is the part teams notice they are missing three months in, usually during an incident.

Attribution on every tool call

A Postmark server token is anonymous by design. Once an agent holds it, Postmark's own activity view shows a send; it does not show which agent run triggered it or which tenant context it ran under. The official MCP server improves on this with X-Postmark-Client and X-Agent-Label headers, but that identifies the integration, not the request.

Scalekit's agent logs record the full delegation chain on every call: which identifier authorized it, which agent ran it, which tool, and what came back. Logs are exportable to your SIEM, which is what turns an audit trail for agent auth into something a security questionnaire can be answered with.

Failure separation

A failed postmark_send_email has three possible causes: the credential is bad, the tenant's Postmark server rejected the payload, or the agent constructed bad arguments. Those failures look identical in an application log and require three different fixes.

Separating them by source is the difference between a 20-minute diagnosis and an afternoon. executeTool returns an executionId alongside the result so a specific call can be traced end to end. This kind of per-call observability is exactly what agent tool observability requires in production.

Which one to build against

The shape of your deployment decides this, not the capability table.

If a human is in the loop

Use the official MCP server. It is the fastest route to conversational deliverability work on a Postmark account you own, and diagnoseDelivery is a better answer to "did this arrive" than anything you would build in an afternoon against the raw API. Point it at a dedicated Postmark server, set WEBHOOK_URL_ALLOWLIST, and never auto-approve the sending or destructive tools.

If the agent runs in production

Build against the API, and hold the tokens somewhere that is not your application database. Every path to Postmark ends at the same long-lived, full-access, un-scopable server token. Solve custody, per-tenant resolution, rotation, and attribution at the infrastructure layer, and the MCP versus API question turns back into what it should be: a capability question with a short answer. For teams thinking about who holds the token across agent tool-calling patterns, Postmark is a clean case study.

Build Postmark agents without owning the token problem

Browse the Postmark agent connector or read the Postmark connector docs to see the full tool list. Compare related email connectors for SendGrid, Mailgun, and Resend, or start from an agent template if you want a working pattern first. Pricing is on the AgentKit pricing page.

Building something specific with Postmark and want a second pair of eyes on the architecture? Join the Scalekit Slack community, or talk to an engineer if you need an answer today.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.