Announcing CIMD support for MCP Client registration
Learn more

Customer.io MCP vs Customer.io API for AI Agents (2026)

Nityashree Yadunath
Product Marketing Manager

TL;DR

  • Customer.io MCP ships eight tools, and three of them are a generic HTTP gateway: cio_read_api, cio_write_api, and cio_delete_api. It is not a catalog of typed marketing tools. It fronts the Journeys UI API and the CDP Data Pipelines API.
  • MCP auth is OAuth only, bound to an individual Customer.io login and that person's role. There is no service-account or static-token path into the MCP server. Customer.io service accounts, which do serve headless workloads, issue sa_live_ tokens that can be created with no expiration.
  • Customer.io's own MCP documentation points terminal and background agents at the CLI and the full API surface instead. Treat that as a design statement, not a temporary gap.
  • The account-level MCP toggle is checked on every call, and admins cannot revoke another user's MCP session. Your agent's access can vanish without notice and cannot be centrally cleaned up at offboarding.
  • Both paths leave you holding N credentials for N users. Scalekit's Customer.io MCP connector handles the OAuth flow, vaulted storage, per-user resolution, and the audit trail, so the MCP versus API decision does not change your auth infrastructure.

Your agent needs to read segments, inspect deliveries, and change campaigns in Customer.io. Customer.io ships a hosted MCP server at mcp.customer.io and three distinct REST APIs plus reporting webhooks. They are not two views of the same thing. The MCP server is a per-user OAuth gateway over two of those API surfaces; the APIs are static-credential endpoints across three different hosts. The decision is mostly an auth decision, and for background agents it is a hard one. Here is the framework.

What Customer.io MCP and Customer.io API actually are

These are two different products with two different intended operators. One is built for a marketer sitting in Claude or Cursor. The other is built for your backend. Read both descriptions before picking, because the naming does not signal the difference.

Customer.io MCP

Customer.io MCP is a hosted, remote MCP server. The endpoints are https://mcp.customer.io/mcp for the US region and https://mcp-eu.customer.io/mcp for the EU region. An account admin enables it once under Settings, then each user connects their own client and authenticates with their own Customer.io login.

The tool surface is deliberately generic. Rather than one tool per marketing object, Customer.io ships three HTTP verbs as tools, a schema introspector, and a skills library that teaches the model how Customer.io works before it calls anything.

Tool
What it does
cio_prime
Loads the LLM-ready reference for the Customer.io API. Called first in a task.
cio_schema
Introspects endpoints, parameters, and response shapes.
cio_read_api
Authenticated GET requests, with pagination and dry-run preview.
cio_write_api
Authenticated POST, PUT, and PATCH requests, with dry-run preview.
cio_delete_api
Authenticated DELETE requests, with dry-run preview.
cio_skills_list
Lists task-specific instruction sets for multi-step operations.
cio_skills_read
Reads the full content of a specific skill.
cio_auth_status
Reports the authenticated user, account, and accessible workspaces.

Splitting reads from writes and deletes is the notable design choice here. In clients that support tool-level permissions, you can auto-approve reads while gating writes and deletes. Official documentation: Get started with the Customer.io MCP server.

The Customer.io API surface

Customer.io does not have one API. It has three, on three hosts, with two different authentication schemes, and the mapping between them is the thing that trips up most first integrations.

The Pipelines API is the recommended ingress path and uses HTTP basic authorization. It is POST-only; deletes and suppressions are expressed as semantic events such as Delete Person rather than as DELETE verbs. The Track API at track.customer.io is the older ingress path, also basic auth, using a Base64-encoded site_id:api_key pair. The App API at api.customer.io/v1 uses bearer authorization with scoped keys, and is the path for triggering broadcasts, sending transactional messages, and fetching data.

Reporting webhooks sit alongside all three and push message activity to your systems. Official documentation: About Customer.io's APIs.

Comparing them where it matters for agents

Four dimensions decide this: what the agent can reach, which credential it holds, what breaks in production, and who owns fixing it. Capability coverage is the least interesting of the four for Customer.io, because the MCP gateway design means coverage is unusually broad. Auth is where the real constraint lives.

What your agent can actually do

Because cio_read_api and cio_write_api accept an arbitrary path, the MCP surface is roughly co-extensive with the two API surfaces it fronts. The gaps are the surfaces it does not front, plus the operations that are structurally wrong to route through a tool call.

Capability
Customer.io MCP
Customer.io API
Read campaigns, segments, deliveries, metrics
Yes, via cio_read_api
Yes, App API
Create and edit campaigns, segments, templates
Yes, via cio_write_api with write scope
Yes, App API
Send messages, manage suppressions and subscriptions
Yes, with write:live scope and admin approval
Yes, App API
Read sensitive profile attributes
Only with read:sensitive scope and admin approval
Yes, subject to key scopes
Configure integrations, webhooks, channel settings
Yes, with configure scope
Partial
Endpoint and schema discovery at runtime
Yes, via cio_schema
No, you pin the spec yourself
High-volume behavioral event ingestion
Not a fit, one HTTP call per tool call
Yes, Track and Pipelines APIs
Batched ingest and historical backfill
No
Yes, Pipelines and Track v2 /batch
Receiving reporting webhook events
No
Yes
Non-interactive, unattended execution
No, per-user OAuth only
Yes, service accounts and API keys

Where the MCP ceiling is

The ceiling is not breadth. A read-scoped MCP session can already reach most of what a marketer sees, and cio_schema means the agent discovers endpoints instead of you hardcoding them. That is genuinely useful for exploratory and assistive work.

The ceiling is throughput and direction. Every MCP tool call is one HTTP request wrapped in model reasoning and, in permissioned clients, a human approval. That is the wrong shape for ingesting behavioral events at volume, for backfills, or for anything that needs the batch endpoints. It is also the wrong shape for event-driven work: reporting webhooks push to you, and an MCP server has no subscription surface to receive them.

The auth path each one puts you on

MCP auth on Customer.io is per-user OAuth with no alternative. Each user authenticates with their own Customer.io login, and the connection inherits that person's role and permissions. You cannot grant the MCP server more access than you personally have, though you can grant it less.

Scopes are explicit, and a connection starts with read only. Your agent requests additional scopes at connect time and the user approves or denies them during the authorization flow.

Scope
Grants
Gated by
read
Non-sensitive reads across automations, segments, profiles, content, metrics
Required, default
read:sensitive
Profile attributes classified as sensitive
Admin toggle plus the user's own role
write
Create, edit, and delete drafts
User approval
write:live
Send messages, manage subscriptions and suppressions
Admin toggle plus user approval
configure
Integrations, webhooks, channel and workspace settings
User approval

The three write-side scopes are independent. An agent that both edits content and sends messages must request write and write:live separately.

The headless gap Customer.io names in its own docs

The direct API path has no equivalent constraint. Track and Pipelines take a Base64-encoded site_id:api_key basic auth pair; the App API takes a scoped bearer key. Neither needs a browser, a session, or a human.

Customer.io states the position plainly in the MCP documentation itself: if your AI tool runs in a terminal, the CLI is usually the better fit, because it gives the agent direct command-line access to the full API surface with no MCP setup. That is the vendor telling you which path background agents belong on.

Service accounts are the credential MCP does not offer

Customer.io ships service accounts for exactly this workload, and they are more granular than a raw API key.

A service account issues tokens prefixed sa_live_, with expiration options of 30, 60, 90 days, one year, or none at all. Tokens can be marked read-only permanently at creation. You can hold several tokens per account so each integration rotates independently, and revoking one invalidates it immediately without touching the others. Details: Customer.io service accounts.

The multi-tenant reality

Run a lifecycle-marketing agent for forty marketers across eight customer workspaces and the MCP path gives you forty OAuth connections. Each one was established by a browser flow, each carries that individual's role, and each dies when that individual leaves.

The direct API path gives you a choice that MCP does not: one scoped service account per customer workspace, or per-user delegation where attribution matters. Eight credentials or forty, decided by your threat model rather than by the protocol.

Neither path stores, rotates, or revokes any of them for you. For a deeper look at credential ownership across agent tool-calling patterns, the tradeoffs between shared and per-user credentials are worth reviewing before you commit to an architecture.

What you own in production

On the MCP path, Customer.io owns hosting, the schema introspector, the skills library, and endpoint normalization. You own token storage per user, re-authorization when a session dies, and the fact that scope grants are decided in a consent screen you do not control.

You also own two behaviors that will surprise you. Requests carrying unknown fields in the body return 422 rather than silently dropping them, which is good discipline but will break agents that improvise payloads. Tool errors come back as structured JSON, so anything parsing the older plain-text format needs updating.

What the direct API path costs you instead

On the direct path you own the whole stack, and Customer.io's three-host split makes that heavier than it sounds.

You pick the host, handle the basic-versus-bearer difference, manage pagination and retries, and maintain the mapping between Pipelines semantic events and the operations they stand for. Customer.io currently documents the Track API at 1,000 requests per second and notes that limits are subject to change, so pin your assumptions to the reference rather than to a blog post.

Governance controls that can break your agent

Three account-level behaviors belong in your runbook before you ship.

First, the MCP toggle is checked on every API call. An admin switching it off stops your agent mid-run with no deprecation window. Re-enabling restores the same sessions, since the OAuth tokens are not deleted, but detection is entirely on you.

Second, read:sensitive and write:live each depend on an admin toggle in addition to user consent. An agent that works in your workspace can fail in a customer's purely because their admin left one off.

Third, and worst for compliance: admins cannot view or revoke another user's MCP sessions. Only the session's creator can, from their own personal settings. When an employee leaves, no central action terminates their agent's Customer.io access. This is exactly the problem covered in depth in when an employee leaves, who revokes their AI agent's access.

When to use MCP, when to use the API

The split follows the auth model almost exactly.

Use Customer.io MCP when:

  • A marketer or lifecycle operator is present in Claude, ChatGPT, or Cursor and the agent is assisting them with campaign drafting, segment building, or delivery analysis
  • You want the agent to discover endpoints at runtime through cio_schema and cio_skills_read rather than maintaining a pinned client
  • Read-heavy investigation is the job, and you want reads auto-approved while writes and deletes stay gated
  • You want workspace governance and role inheritance enforced by Customer.io rather than reimplemented in your product

Use the Customer.io API directly when:

  • The agent runs unattended: scheduled digests, nightly syncs, churn-risk scoring, pipeline-triggered sends
  • You are ingesting behavioral events at volume, backfilling history, or using the batch endpoints
  • Your agent reacts to reporting webhooks rather than polling for state
  • You need per-workspace service accounts with independent rotation and centrally revocable tokens
  • You are shipping a deterministic pipeline and cannot absorb a schema or availability change you did not schedule

The credential problem that exists on both paths

Every Customer.io agent that serves more than one person ends up holding a pile of credentials. The path you chose determines the token type. It does not determine the infrastructure you have to build around it.

What neither path gives you

MCP hands you an OAuth session per user. Service accounts hand you a sa_live_ token per workspace, which is long-lived by design and can be created with no expiration at all. In both cases the credential has to live encrypted at rest, isolated per tenant, and resolvable at request time without ever entering agent runtime or LLM context.

Revocation is where both paths leave the sharpest edge. A user can kill their MCP session from personal settings and your agent finds out on the next 401. A sa_live_ token that was created with no expiration and stored in a .env file on a laptop stays valid long after the person holding it has been offboarded. The agent does not decide to keep using it. It just does. Understanding secure token management for AI agents at scale is critical before either path goes to production.

Where Scalekit fits

Scalekit's Customer.io MCP connector handles the OAuth flow, vaulted token storage, per-user resolution at request time, and revocation, so the MCP versus API decision does not change your auth infrastructure. Credentials never touch the agent runtime or the LLM context. Connector reference: Customer.io MCP connector and the Customer.io connector overview.

Connect a Customer.io agent with Scalekit

The connector uses Dynamic Client Registration, so there is no Customer.io client ID or secret to register. You create a connection in the dashboard, send the user through one authorization link, and Scalekit injects the right token into every tool call after that.

Install and configure

Install the Python SDK and set your Scalekit credentials. Find these under Developers and API Credentials in the dashboard.

pip install scalekit-sdk-python langchain langchain-openai
# .env SCALEKIT_ENV_URL=<your-environment-url> SCALEKIT_CLIENT_ID=<your-client-id> SCALEKIT_CLIENT_SECRET=<your-client-secret>

In the Scalekit dashboard, go to AgentKit, then Connections, then Create Connection, and search for Customer.io MCP. Note the connection name you are given.

The connection_name string in your code must match the connection name configured in the Scalekit dashboard exactly. This is the single most common integration error.

Authorize the user

Initialize the client and put the user through the Customer.io consent flow once. The scopes the user approves at this screen are the ceiling on everything the agent can do afterward.

import os import scalekit.client from dotenv import load_dotenv load_dotenv() 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 # Must match the connection name in the Scalekit dashboard. CONNECTION_NAME = "customeriomcp" IDENTIFIER = "marketer@acme.com" account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=IDENTIFIER, ) if account.connected_account.status != "ACTIVE": magic_link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=IDENTIFIER, ) print("Authorize Customer.io MCP:", magic_link.link) input("Press Enter after authorizing...")

Retrieve the authorized tool surface

Before the agent runs, retrieve the tools this user's connected account is authorized to call. This is not a catalog lookup. list_scoped_tools returns the surface that this specific connected account permits, which is why a marketer with a read-only Customer.io role and an admin get different results from identical code.

scoped = actions.tools.list_scoped_tools( identifier=IDENTIFIER, page_size=100, ) for tool in scoped.tools: print(tool.name, "|", tool.description)

Then prime the model. The connector documentation is explicit that customeriomcp_cio_prime should be your first call, because it loads the Customer.io API structure and recommended workflows into context before the agent starts guessing at paths.

primer = actions.execute_tool( tool_name="customeriomcp_cio_prime", tool_input={}, identifier=IDENTIFIER, ) print(primer.data)

Run the agent loop with LangChain

Scalekit returns native LangChain StructuredTool objects, so the agent code carries no Scalekit-specific logic past initialization. Narrowing tool_names here is deliberate: this agent reports on campaigns, so it never receives cio_write_api or cio_delete_api, and cannot be talked into a mutation it was not built for. For a full walkthrough of how LangChain tool calling works and where it stops, that context is useful before wiring up the loop.

from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier=IDENTIFIER, connection_names=[CONNECTION_NAME], tool_names=[ "customeriomcp_cio_prime", "customeriomcp_cio_schema", "customeriomcp_cio_read_api", ], page_size=100, ) tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ HumanMessage( "Which triggered campaigns sent in the last 7 days have an open rate " "below 20 percent? Report campaign name, sends, and open rate." ) ] 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"]))

Dry-run before every mutation

When the agent does need write access, the MCP tools support a dry_run flag that validates and returns the request without executing it. Treat this as mandatory in any workflow that touches live sends, not as a debugging convenience.

preview = actions.execute_tool( tool_name="customeriomcp_cio_write_api", tool_input={ "path": "/v1/environments/{environment_id}/campaigns", "params": {"environment_id": "env_abc123"}, "body": {"name": "Trial Day 7 Nudge", "type": "triggered"}, "dry_run": True, }, identifier=IDENTIFIER, ) print(preview.data)

The direct API path, through the same auth layer

Scalekit's prebuilt Customer.io connector targets the MCP server. If your agent needs the Track, Pipelines, or App API instead, define those as a custom connector and call them through Tool Proxy. The connection, connected account, and authorization model stay identical, so you are not standing up a second auth stack for the second path.

response = actions.request( connection_name="customerio-app-api", # your custom connector definition identifier=IDENTIFIER, path="/v1/customers", method="GET", query_params={"email": "jane%2Bnotifications%40example.com"}, ) customers = response.json()

Percent-encoding matters here. Customer.io returns 200 with empty results rather than an error when a query parameter is not encoded, so an agent will happily report "no such customer" for a customer that exists. Setup guide: Add your own connector.

Virtual MCP servers for multi-tool, multi-tenant agents

A standard MCP server exposes everything it has. Customer.io MCP is unusually dangerous in this respect, because cio_write_api and cio_delete_api are not narrow tools. They are arbitrary write and delete access to the whole workspace, bounded only by the scopes in the consent screen.

Define the server once

A Virtual MCP server declares exactly which connections and which tools an agent role can see. You create it once per agent role, not once per user.

from scalekit.actions.types import McpConfigConnectionToolMapping config = actions.mcp.create_config( name="lifecycle-reporting-agent", description="Read-only Customer.io reporting surface", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="customeriomcp", tools=[ "customeriomcp_cio_prime", "customeriomcp_cio_schema", "customeriomcp_cio_read_api", ], ), ], )

Add further McpConfigConnectionToolMapping entries for the other connections the agent needs. A churn-response agent that reads Customer.io deliveries and opens tickets elsewhere gets both surfaces from one endpoint, each scoped to the tools you named.

Mint per-user access at runtime

One server definition serves every user. Before each run, confirm the user has authorized the required connections and get their instance URL, which is bound to their connected accounts and no one else's.

instance = actions.mcp.ensure_instance( config_name="lifecycle-reporting-agent", user_identifier=IDENTIFIER, ) auth_state = actions.mcp.get_instance_auth_state( instance_id=instance.instance.id, include_auth_links=True, ) for conn in auth_state.connections: if conn.connected_account_status != "ACTIVE": print(f"Authorize {conn.connection_name}: {conn.authentication_link}") mcp_url = instance.instance.url

Why this matters for token cost

The economics are not marginal. A server with 40 tools at roughly 200 tokens each burns about 8,000 tokens before the agent does any work. Scoping to 5 to 10 tools cuts that overhead by around 80 percent, and shrinks the decision space the model is choosing from. Surface reduction is the lever. Model upgrades help. They are not the lever. This is one reason MCP can be up to 32× more expensive than CLI — and why scoping matters so much.

Pointing a framework at the endpoint

Any MCP-capable framework connects to the instance URL over streamable HTTP. With LangChain, that is langchain-mcp-adapters rather than the native adapter.

import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage async def run(mcp_url: str, prompt: str): async with MultiServerMCPClient( {"scalekit": {"transport": "streamable_http", "url": mcp_url}} ) as client: tools = client.get_tools() tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage(prompt)] while True: response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = await tool_map[tc["name"]].ainvoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) asyncio.run(run(mcp_url, "Summarize delivery performance for active campaigns."))

Observability for downstream Customer.io tool calls

Customer.io's own audit trail records what happened inside the workspace. It does not record which agent run triggered it, which prompt led there, or which of your tenants the action belonged to. For a marketing platform where the write path sends real messages to real customers, that gap is not academic.

What the auth log has to answer

When a campaign gets edited or a send goes out, three questions arrive at once: who authorized the credential, which agent executed the call, and was that action inside the scope you granted. Scalekit's auth logs tie every Customer.io tool call to the user whose connected account resolved it, with 90 days of history and SIEM-ready export. The broader case for audit trails for agent auth in B2B SaaS explains why per-call attribution matters at enterprise scale.

The failure this prevents

A shared Customer.io API key looks correct in a demo. In production it collapses attribution: every send, every segment edit, and every suppression appears as one service account, and no log query can reconstruct which person's request produced it. Per-user connected accounts keep the chain intact from prompt to API call, and scope is checked before the request reaches Customer.io rather than being suggested inside a prompt.

Which one to build against

If a marketer is in the loop and the agent is helping them explore, draft, and analyze, use Customer.io MCP. The schema introspector and skills library genuinely reduce integration work, role inheritance is enforced by Customer.io, and read-write separation gives you a sane approval model out of the gate.

If the agent runs unattended, ingests events at volume, reacts to webhooks, or acts across multiple customer workspaces, build against the API with service accounts. Customer.io's documentation already points that workload away from MCP, and the absence of any non-interactive credential path into the MCP server is architectural, not a roadmap item.

Most production deployments end up running both, and that is fine. The credential layer is identical either way, and that is the part that needs production-grade infrastructure.

Talk to other Customer.io agent builders

Building lifecycle-marketing agents on Customer.io and want to compare notes on scope design, dry-run policies, or per-tenant isolation? Join the Scalekit Slack community.

If you need help now, talk to an engineer and we will walk through your Customer.io auth model with you.

Browse the Scalekit Customer.io MCP connector.

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.