MCP

Customermates exposes a native MCP endpoint at /api/v1/mcp so Claude, ChatGPT, Cursor, and other agent clients can read and write your CRM directly.

Customermates exposes one MCP endpoint at https://customermates.com/api/v1/mcp. A connected client discovers all 48 CRM tools automatically. There are two ways to connect:

  • Custom connector (OAuth): for Claude (web, desktop, mobile) and ChatGPT. Paste the URL, sign in, approve. No key to manage. Start on Connect with a custom connector.
  • API key: for CLI and editor clients (Claude Code, Codex, Cursor, Gemini CLI) and raw HTTP. Send the 64-character key in the x-api-key header or a config file.

For end-to-end setup on one page, jump to your client: Claude Desktop, ChatGPT, Claude Code, Codex, Cursor, or Gemini. This page is the protocol reference.

When to use MCP

  • You want your AI to read and write the CRM without copying IDs around.
  • You want the AI to discover capabilities rather than hand-writing API calls.
  • You want one endpoint that works across Claude, ChatGPT, Cursor, Codex, and other clients.

Use OpenAPI instead when an engineer or integration service already knows which endpoint it needs. OpenAPI is the canonical HTTP reference. MCP is the agent-native interface built on top.

The endpoint

POST https://customermates.com/api/v1/mcp
Content-Type: application/json
Accept: application/json, text/event-stream
x-api-key: <your-64-character-key>

The endpoint speaks the Model Context Protocol (streamable HTTP variant). tools/list returns every tool with its JSON Schema. tools/call invokes a tool by name.

Because it is the streamable HTTP variant, every request must send Accept: application/json, text/event-stream. Without it the endpoint returns 406 Not Acceptable. The documented client guides configure the supported connection path; raw HTTP callers such as curl or scripts must add the header explicitly.

The x-api-key header is the API-key method. The key is 64 characters, base62 (a-z, A-Z, 0-9), and inherits the permissions of the user who created it. There is no per-key scoping. Custom-connector clients (Claude, ChatGPT) authenticate over OAuth instead and send a bearer token they obtain and refresh for you. See Connect with a custom connector.

Connect a client

Getting an AI client onto your workspace is the same three moves every time:

  1. Create an API key

    Profile → API Keys → New key. Choose the guided setup for Claude, ChatGPT, Cursor, or Gemini, or a standard key for your own integration. The key is shown once, so copy it before closing the dialog.

  2. Point the client at the endpoint

    Give the client POST https://customermates.com/api/v1/mcp with the x-api-key header from the step above, or connect through OAuth where the client supports it. The per-client guides in the table below carry the exact configuration.

  3. Confirm the tools arrived

    Ask the client to list its tools. All 48 should appear, and get_workspace_context is the natural first call: it returns your user, company, roles, and connected accounts in one go.

ClientMethodGuide
Claude web & mobileCustom connector (OAuth)Connect with a custom connector
Claude DesktopConnector (OAuth) or config keyConnect Claude Desktop
ChatGPTConnector (OAuth) or key headerConnect ChatGPT
Claude CodeAPI keyConnect Claude Code
CodexAPI keyConnect Codex
CursorAPI keyConnect Cursor
Gemini CLIAPI keyConnect Gemini
Any MCP clientKey headerUse the endpoint and header above

The server exposes instructions when a client connects. Whether a client presents them to the model and how the model follows them depends on the exact client. Clients that support MCP prompts can also run the built-in get-started prompt for a personalized start.

Server instructions, prompts, and toolsets

The server sends workflow guidance when a client connects: read the schema first, find ids before writing, change relations only through manage_record_links, and ask before deleting or sending. That guidance is not a server-enforced confirmation gate. Verify how the chosen client passes instructions to the model and handles approval before granting write, delete or messaging access. In clients that support MCP prompts, a built-in get-started prompt can summarize the workspace to personalize the start.

The full 48-tool surface is the default. Append ?toolsets= to the endpoint URL to narrow it, for example /api/v1/mcp?toolsets=records,messaging. Keys and details are in Narrowing with ?toolsets=.

How the tool surface is shaped

The MCP surface is built so that models with weaker planning can use it reliably:

  • Verb-first imperative names: create_contacts, update_deals, delete_records. No batch prefix. The verb matches intent.
  • Merged periphery: custom columns, widgets, and webhooks each live behind one tool (manage_custom_columns, manage_widgets, manage_webhooks) with an action switch, so the model picks an action instead of choosing among many near-identical tools.
  • Inline enum hints: every enum field lists its valid values inline in the description, so the model does not have to resolve external types.
  • Filter examples inline: every filters parameter has a concrete JSON example in its description.
  • Relationship safety: relations change only through manage_record_links (add or remove); passing null on a relationship array is rejected with a remediation hint.
  • Immutable column type: manage_custom_columns keeps type and entityType fixed on update (taken from the existing column); changing a type means delete and recreate.
  • Destructive flags: every destructive tool or action has destructiveHint: true and an IRREVERSIBLE description prefix.

CLI use

If you prefer a local client over a GUI, tools like mcporter can connect to the same endpoint. Store the API key once in the client config and call tools from the shell.

OpenAPI alongside MCP

Both live at the same base URL. MCP is /api/v1/mcp; the OpenAPI spec is at /api/v1/openapi. The OpenAPI operations map 1:1 to REST endpoints. MCP wraps supported operations as typed tools with instructions, annotations and the same product authorization. Those instructions do not add a second server-side confirmation step.

Tool catalog

Customermates exposes 48 MCP tools, all enabled by default. They cover records, workspace, messaging, social posts, documentation and deep research, custom columns, widgets, webhooks, admin, and support. Every destructive tool is flagged and starts its description with IRREVERSIBLE. Relations change only through manage_record_links; the update tools never touch them.

Two flags per tool:

  • Read: no mutation.
  • Destructive: deletes data or cannot be undone. For merged tools the flag applies to their delete-capable actions.

Full JSON Schema for every tool is available live at POST /api/v1/mcp with method: "tools/list".

Records

Seventeen tools work against the five record types (contact, organization, deal, service, task). Records carry custom columns defined per workspace, so get_record_schema is the anchor: it returns the custom-column ids and option values that writes need. Fields such as a deal or task status are configurable singleSelect custom columns, not fixed native fields; get_record_schema returns whatever columns the workspace actually has.

ToolReadDestructivePurpose
get_record_schemaSchema and custom-column metadata, never record data. One entity type, or all five when entity is omitted. Call before any create or update.
list_recordsSearch, filter, sort, paginate one entity type. Always returns the total. Deals include totalValue and totalQuantity, services include amount.
search_recordsFree-text search across one or more entity types in one call.
get_recordsFull record data by id, up to 100, mixed entity types allowed; contacts also by email, phone, or provider:handle. Always returns fields; add markdown notes per item with include=withNotes.
create_contactsCreate up to 100 contacts, custom-column values and relation ids inline.
create_organizationsCreate up to 100 organizations, custom-column values and contact/user/deal/task ids inline.
create_dealsCreate up to 100 deals, services as an inline array.
create_servicesCreate up to 100 services, custom-column values and user/deal/task ids inline.
create_tasksCreate up to 100 tasks, custom-column values and relation ids inline.
update_contactsPartial update by contact key (id, email, phone, or provider:value); org/deal/user/task relations untouched, but a provided identifiers array REPLACES the contact's messaging channels (unlisted ones are unlinked).
update_organizationsPartial update by id. Never touches relations.
update_dealsPartial update by id, including singleSelect custom-column values and services (an inline {serviceId, quantity} array that REPLACES the deal's full service set); org/user/contact/task relations untouched (use manage_record_links).
update_servicesPartial update by id.
update_tasksPartial update by id, including singleSelect custom-column values. Never touches relations.
update_record_notesReplace or append markdown notes on 1 to 100 records, selected by mode.
manage_record_linksAdd or remove ids on one relation (action add or remove). The only way to change relations.
delete_recordsIRREVERSIBLE hard-delete of 1 to 100 records by id (contacts also by email, phone, or provider:value).

All create and update tools take custom-column values via customFieldValues; call get_record_schema first for the column ids.

Workspace

ToolReadDestructivePurpose
get_workspace_contextYour user, the company profile, all roles including permissions, and your connected messaging accounts (your own plus any shared with the workspace) in one call. The natural first call of a session.
list_usersTeam members with id, name, email, roleId, and status.

Messaging

Messaging-backed tools are available from the Pro tier in cloud mode. get_activities can still return audit-log changes without the messaging entitlement when the caller has Audit Log permission. Its messaging, connected-account, and calendar sources require Inbox permission plus the messaging entitlement.

ToolReadDestructivePurpose
get_messaging_threadsTwo modes: without threadId lists inbox threads with filters and sorting (threads with no message yet are hidden unless they hold a draft; the draft filter isolates threads that hold one); with threadId returns one thread plus a page of its messages (default 25, newest first, drafts included).
get_activitiesActivity timeline with an optional low-level entity scope plus AND-combined filters. Filters support category/raw kind, conversation, provider, connected account, and related contact, organization, deal, service, or task. Each activity filter field may appear once; alternatives belong in the value array of one membership rule. Relationship fields accept in, notIn, hasSome, and hasNone; membership takes 1–50 UUIDs. Relationship UUIDs must resolve to records you can read; unresolvable ids are rejected. The result includes availableSources, scopeTruncated, pageLimitReached, total, and page. Pages are capped at 40.
get_calendarsThree modes: list: "calendars" (default) lists the calendars of accessible connected accounts; list: "events" lists calendar events ordered by start time, filterable by calendarId or a startsAt date range; with eventId it returns one event's detail including organizer and attendees. Ids match the entityId of calendar webhook events.
send_chat_messageDelivers immediately. With threadId replies in an existing chat; with connectedAccountId plus attendeeIdentifiers starts a new one (optional chatName names a group). To send a saved draft, pass both draftMessageId and draftRevision. New LinkedIn chats default to Classic; set linkedinProduct to sales_navigator or recruiter to send an InMail (needs inmailSubject), or inmail:true to InMail someone outside your network on Classic.
send_emailDelivers immediately. Send or reply from a connected email account; sending a saved draft requires both draftMessageId and draftRevision. The account's enabled signature is appended automatically, so never write a sign-off into the body.
save_message_draftPrepare a message for review: the draft shows up in the inbox and the user sends it. With threadId it drafts a reply; with connectedAccountId plus recipients it prepares a brand-new conversation that exists only as a draft. Returns the message id and opaque revision token; saving again updates the thread's one draft. The signature is appended when the draft is sent, so never write a sign-off into the body.
discard_message_draftDelete the exact saved draft revision using its message id and opaque revision token.
update_messaging_threadSet the thread state: unread, open, closed, or spam.
move_email_threadMove an email conversation into another mailbox folder at the provider.
connect_messaging_accountGenerate a link the user opens in a browser to connect a channel (WhatsApp, LinkedIn, email, Instagram, Telegram). You return the link; the user finishes auth there. Expires in 30 minutes.

Social posts

ToolReadDestructivePurpose
get_social_postsPosts on LinkedIn or Instagram, read through a connected account. Use authorIdentifier=me for the account owner. For another person, use get_social_profile.id, get_social_posts.items[].author.id (list mode), get_social_posts.author.id (single-post mode), get_social_post_engagement.items[].author.id (comments), get_social_post_engagement.items[].sender.id (reactions), or manage_social_relations.items[].user.id. Resolve get_messaging_threads.items[].participants[].identifier (list mode) or get_messaging_threads.thread.participants[].identifier (detail mode) through get_social_profile first; do not pass a thread participant identifier directly. Pass get_social_posts.items[].id as postId to fetch one post. On continuation, repeat the same account, author, and limit with next_cursor.
get_social_post_engagementEngagement on a post: kind=comments (default) lists comments, kind=reactions lists who reacted; with commentId it returns the reactions on that comment.
get_social_profileA person or company profile. For a person, use profileType=person with me, get_messaging_threads.items[].participants[].identifier, get_messaging_threads.thread.participants[].identifier, get_social_posts.items[].author.id, get_social_posts.author.id, get_social_post_engagement.items[].author.id, get_social_post_engagement.items[].sender.id, manage_social_relations.items[].user.id, a LinkedIn Classic public profile slug, or an Instagram username. For a LinkedIn company, use profileType=company with linkedin_search_sales_companies.items[].id, linkedin_search_sales_leads.items[].current_positions[].company_id, linkedin_manage_sales_lists.items[].current_positions[].company_id, or get_social_profile.current_positions[].company_id. Reuse get_social_profile.id with the same profileType.
manage_social_relationsConnection requests: list invitations (received by default, or your own sent/outgoing via direction), invite with get_social_profile.id (sends a real request), accept, or cancel by invitationId.
linkedin_search_sales_leadsFinds people via LinkedIn Sales Navigator: either from a pasted search URL or as a structured search with filters (keywords, location, industry, company, job title, seniority and more). Resolve linkedin_search_sales_leads.items[].current_positions[].company_id with get_social_profile and profileType=company. Requires a Sales Navigator subscription.
linkedin_search_sales_companiesFinds companies via LinkedIn Sales Navigator: either from a pasted company search URL or as a structured search with filters (keywords, location, industry, headcount, annual revenue and more). Pass linkedin_search_sales_companies.items[].id to get_social_profile with profileType=company. Requires a Sales Navigator subscription.
linkedin_get_sales_search_parametersResolves the ids behind LinkedIn Sales Navigator search inputs by type (locations, industries, job titles, functions, companies, schools, groups and more) plus your lead/account lists and saved/recent searches; keyword is optional, so a bare type enumerates the whole family.
linkedin_manage_sales_listsLinkedIn Sales Navigator lead and account lists: list them, browse the members of one, or save a lead using linkedin_search_sales_leads.items[].id or get_social_profile.id, or a company using linkedin_search_sales_companies.items[].id or an items[].current_positions[].company_id path documented above. New lists are created in Sales Navigator itself.

Typical social read flow

Choose a LinkedIn or Instagram entry whose status is ok from get_workspace_context.connectedAccounts, and use its id as connectedAccountId. When the person comes from an inbox thread, resolve get_messaging_threads.items[].participants[].identifier from list mode or get_messaging_threads.thread.participants[].identifier from detail mode first:

{
  "connectedAccountId": "00000000-0000-4000-8000-000000000001",
  "identifier": "<get_messaging_threads.items[].participants[].identifier>",
  "profileType": "person"
}

Call get_social_profile with that request, then pass get_social_profile.id to get_social_posts.authorIdentifier for the first page:

{
  "connectedAccountId": "00000000-0000-4000-8000-000000000001",
  "authorIdentifier": "<get_social_profile.id>",
  "limit": 10
}

If next_cursor is non-null, repeat the same connectedAccountId, authorIdentifier, and limit, set cursor to that value, and omit offset:

{
  "connectedAccountId": "00000000-0000-4000-8000-000000000001",
  "authorIdentifier": "<same get_social_profile.id>",
  "cursor": "<next_cursor>",
  "limit": 10
}

For a company, call get_social_profile with profileType=company and an identifier from linkedin_search_sales_companies.items[].id, linkedin_search_sales_leads.items[].current_positions[].company_id, linkedin_manage_sales_lists.items[].current_positions[].company_id, or get_social_profile.current_positions[].company_id.

Documentation and deep research

ToolReadDestructivePurpose
search_docsFull-text search over the docs; defaults to the product guides (source=docs); pass source=api or all to include the REST API reference. Returns slug, source, title, url, snippet.
get_docs_pageOne documentation page as markdown with its canonical URL. Lists valid slugs on a miss.
searchRequired by ChatGPT deep research connectors; federates CRM records and docs. Interactive agents should prefer search_records or search_docs.
fetchDeep-research companion to search: fetches one result by its id.

Custom columns

ToolReadDestructivePurpose
manage_custom_columnsOne tool with an action switch: list, upsert (create or update), delete. Covers all ten column types; type and entityType are immutable on update. Delete is IRREVERSIBLE and removes every stored value.

Widgets

ToolReadDestructivePurpose
manage_widgetsOne tool with an action switch: list, get, create, update, delete. Omitted create kind remains chart. Activity creation accepts name, optional timelineFilters, and optional showFilters; each activity filter field may appear once. Update infers the immutable stored kind, preserves omitted fields, and clears filters with timelineFilters: []. Create rejects newly inaccessible relationship UUIDs. Update may retain or remove an unavailable relationship UUID only when that UUID is already stored on the widget; adding another inaccessible UUID is rejected. list/get/create/update all return kind; get also returns chart data for charts and reusable timelineFilters for activity widgets. Chart-only and activity-only fields cannot be mixed.

Routines

ToolReadDestructivePurpose
manage_routinesOne tool with an action switch: list, runs, create, update, pause, run_now, delete. A routine is saved instructions the assistant runs on a cron schedule or when a CRM event fires. Omitting enabled on create produces a LIVE routine, so pass enabled: false to draft one. A change of triggerKind must arrive with that kind's schedule or events. pause disables the routine and settles its queued runs to skipped, which re-enabling does not undo. run_now applies to scheduled routines only. Runs are paginated by cursor and carry status, summary and trigger.

Webhooks

ToolReadDestructivePurpose
manage_webhooksOne tool with an action switch: list, get, create, update, delete, plus the delivery log (action list_deliveries, scoped to one webhook's current url when you pass its id) and re-delivery (action resend_delivery).

Admin and team

ToolReadDestructivePurpose
update_workspace_settingstarget profile updates your own name, country, and avatar; target company updates the workspace currency (admin only).
manage_teamInvite members by email (action invite, up to 20, sends real invitation emails) or change a member's role and status (action update_member).

Support

ToolReadDestructivePurpose
request_supportOpen a support ticket with the Customermates team (subject plus description). The team follows up by email and in the in-app chat. Returns the ticket number.

Narrowing with ?toolsets=

All 48 tools are on by default. To expose only part of the surface, append ?toolsets= with comma-separated group keys to the endpoint URL:

https://customermates.com/api/v1/mcp?toolsets=records,messaging

Keys: records, workspace, messaging, social, docs, custom-columns, widgets, routines, webhooks, admin, support. No parameter means everything; unknown keys are ignored. search and fetch are always on so deep-research connectors keep working on any narrowed surface.

When a call is refused

A refusal is data, not a crash. The result carries isError: true, a human-readable message in content, and a machine-readable envelope in _meta.failure with a kind and the offending issues, each naming the field path it belongs to:

{
  "isError": true,
  "content": [{ "type": "text", "text": "Webhook ID not found or not accessible." }],
  "_meta": {
    "failure": {
      "kind": "not_found",
      "issues": [{ "code": "custom", "path": [], "message": "Webhook ID not found or not accessible.", "customCode": "webhookNotFound" }]
    }
  }
}

kind is one of validation, authentication, authorization, not_found, conflict, rate_limit, or unavailable. Branch on it instead of matching message text:

  • validation: the arguments were wrong. Read issues[].path, fix that field, and call again. get_record_schema resolves most of these.
  • authorization: the caller's role does not permit it. Retrying never helps; say what was refused and which permission it needs. Roles are workspace-defined, so read get_workspace_context.roles rather than assuming a fixed set.
  • not_found: the id does not exist, or belongs to another workspace. Every call is tenant-scoped, so an id from elsewhere reads as missing rather than forbidden.
  • conflict: something already holds the resource. Read current state before retrying.
  • rate_limit and unavailable: transient or capacity-bound. Back off, and surface unavailable as a provider problem rather than a user mistake.
  • authentication: the key is missing, expired, or revoked. The user must issue a new one.

Entitlements refuse the same way. Messaging-backed tools need the Pro tier, Sales Navigator tools need that LinkedIn subscription, and connecting an account stops when the plan's channel limit is reached. Permission and entitlement are independent: a caller can have Inbox permission and still be refused for the plan, and the reverse.

Tool metadata and server-side constraints

  • Client confirmation guidance. The server instructions tell the model to confirm before delete_records and every send tool. The MCP route executes an authorized tool call once the client makes it, so verify confirmation behavior in the client.
  • Relations change only via manage_record_links. The update tools never touch relations, and null on a relationship array is rejected server-side with a remediation hint.
  • Draft, then send. send_email and send_chat_message deliver immediately. When asked to prepare a message, the agent uses save_message_draft and the user sends from the inbox. A draft needs no existing conversation: pass connectedAccountId and recipients and the thread is created locally, appears in the inbox, and reaches the provider only when it is sent.
  • Destructive flags everywhere. Every destructive tool or action has destructiveHint: true and an IRREVERSIBLE description prefix.
  • Every enum field lists its valid values inline in the description, and every filters field includes a concrete JSON example.

Frequently asked questions

How do I authenticate against the MCP endpoint?

Send your 64-character API key in the x-api-key header, or connect through OAuth where the client supports it. Create keys at Profile → API Keys → New key. Every key carries your own permissions, so a client can never do more than you can.

Which AI clients can connect?

Claude on web, mobile and desktop, ChatGPT, Claude Code, Codex and Cursor have documented connection paths above. Another client may connect when it supports MCP over streamable HTTP and the required authentication, but verify its exact compatibility and instruction handling before use.

Can I reduce the number of tools a client sees?

Yes. Append ?toolsets= with a comma-separated list of the groups above to the endpoint URL and only those tools are advertised. The two connector tools search and fetch stay always on.

How do I recognize dangerous tools?

Every destructive tool is flagged in the catalog above and starts its description with IRREVERSIBLE. Clients that honor MCP annotations also receive destructiveHint and can ask for confirmation before calling. This metadata helps the client; it is not a second server-side approval gate.

Do tools return machine-readable results?

Yes. Every tool declares an output schema, visible live via tools/list, and returns structuredContent conforming to it next to the compact text form, so a client can chain results without parsing text.

Should I use MCP or the REST API?

Both exist side by side: MCP is for AI clients that discover and call tools on their own, the OpenAPI-documented REST API is for your own code and integrations. They share the same permissions and data.

Next