# Customermates documentation (full text) > Customermates is an open-source, AI-native CRM. This file concatenates every English docs page in reading order. Per-page markdown: https://customermates.com/en/raw/docs/.md - index: https://customermates.com/llms.txt --- # Customermates CRM Documentation Source: https://customermates.com/en/docs/intro-page Customermates is an open-source CRM with a native MCP endpoint. Claude, ChatGPT, Cursor, and any other client that speaks the Model Context Protocol can read and write your CRM directly, without copy-paste. ## Pick your client and start | Your AI | Full setup on one page | |---|---| | Claude & ChatGPT (custom connector) | [Connect with a custom connector](/docs/connect-custom-connector) | | Claude Code | [Connect Claude Code](/docs/connect-cli#claude-code) | | Cursor | [Connect Cursor](/docs/connect-cli#cursor) | | Any MCP client | [MCP overview](/docs/mcp) | Each page is self-contained: the connect steps and your first commands. The server briefs your client automatically on connect, so you do not need to move between pages. For a walkthrough that includes a webhook and a first AI interaction, see [Quickstart](/docs/quickstart). ## What Customermates is A CRM with five record types (contacts, organizations, deals, services, tasks), custom columns, table and card views, and dashboards, plus: - **Native MCP endpoint** at `/api/v1/mcp`, so an MCP client can discover and call CRM operations without a plugin. - **Open source** under AGPLv3. Enterprise components are under a separate commercial license. Self-host or use the managed cloud. - **EU-hosted and GDPR-compliant** managed version. - **Webhooks** with signed payloads, retries, and a delivery log. - **OpenAPI 3.1** specification for the full REST surface at `/api/v1/openapi`. ## Four ways Customermates connects to your stack | I want to | Use | |---|---| | Have an MCP client update the CRM in conversation | [MCP](/docs/mcp) | | Build a custom integration in code | [OpenAPI](/docs/openapi) | | React to CRM changes in Slack, a sequencer, BI, or your warehouse | [Webhooks](/docs/webhooks) | | Compose workflows visually without code | [n8n](/docs/n8n) | All four authenticate with an API key. MCP clients such as Claude and ChatGPT can also connect over OAuth without a key. See [API keys](/docs/api-keys). ## Design approach Customermates treats the AI client as a first-class user. Tools are named imperatively, errors are actionable, relationships can be modified incrementally, and the MCP surface is kept small (46 tools, all on by default) so that smaller models can navigate it reliably. ## How the docs are organized - **Getting started**: [Quickstart](/docs/quickstart), [Concepts](/docs/concepts). - **Connect your AI**: [Custom connector](/docs/connect-custom-connector), [CLI & editors](/docs/connect-cli). - **App guide**: [Dashboard](/docs/app-dashboard), [Records](/docs/app-records), [Inbox](/docs/app-inbox), [Global Search](/docs/app-search), the [Onboarding Wizard](/docs/app-onboarding), and the other app screens. Every documented app area carries stable anchor ids for AI agents, and the anchors are test-enforced against the interface. - **Integrations**: [MCP](/docs/mcp), [Webhooks](/docs/webhooks), [OpenAPI](/docs/openapi), [n8n](/docs/n8n). - **Self-hosting**: [Get started and manage](/docs/self-hosting), [Architecture and security](/docs/architecture-security). - **Reference**: [MCP tool catalog](/docs/mcp#tool-catalog), [Filter syntax](/docs/filter-syntax), [API keys](/docs/api-keys). ## Try it The embedded demo on this page is a live instance. You can create and delete records. Nothing persists across sessions. --- # Customermates CRM Quickstart: Set Up in Minutes Source: https://customermates.com/en/docs/quickstart Create a Customermates account, connect an AI client, and start issuing prompts. Customermates is an open-source, AI-native CRM. Claude and ChatGPT connect with a custom connector and need no API key. CLI clients and editors connect with an API key. Sign up at [customermates.com/auth/signup](/auth/signup). No credit card is required. The first 7 days are free, after which you pick a plan. **Using Claude (web, desktop, mobile) or ChatGPT?** Add a custom connector, no API key needed. In your AI client: **Settings → Connectors → Add custom connector**, paste `https://customermates.com/api/v1/mcp`, sign in, and approve. Full steps: [Connect with a custom connector](/docs/connect-custom-connector). **Using a CLI or editor?** First create an API key (**Profile → API Keys → New key**) and copy the 64-character string, which is shown once. Then follow your client's page: - [Claude Code](/docs/connect-cli#claude-code): one terminal command - [Cursor](/docs/connect-cli#cursor): Settings UI, two clicks - [Codex](/docs/connect-cli#codex): one TOML block in `~/.codex/config.toml` - [Gemini](/docs/connect-cli#gemini-cli): one settings block The server sends its instructions to the client on connect, so the AI knows the tool surface and workspace conventions from the start. In clients that support MCP prompts, such as Claude, you can also run the built-in `get-started` prompt for a personalized start: your name, your role, and a first summary of your workspace. - *"Set the status of the Acme deal to Won and add a note that the contract was signed today."* - *"Pull the last ten contacts I created. Any without an email address?"* - *"Create a contact for Jane Doe at Initech, link it to the Initech organization, and start a deal for 12 hours of consulting."* The agent reports which tools it called and what changed. Field names such as a deal's status are workspace-configurable custom columns, not fixed product fields. Call `get_record_schema` to see the columns that exist in your workspace. To notify other tools when the CRM changes: Go to **Company → Webhooks → New**. Set the URL to a throwaway endpoint from [webhook.site](https://webhook.site). Under events, select `contact.created` and `deal.updated`, then save. Edit a contact. Within a few seconds the delivery appears at webhook.site with the full payload. Full webhook docs: [Webhooks](/docs/webhooks). ## Next - [Core concepts](/docs/concepts): records, custom columns, and relations at a glance. - [MCP tool catalog](/docs/mcp#tool-catalog): every tool your AI can call over the Model Context Protocol (MCP). --- # CRM Data Model: Entities, Relations, Custom Fields Source: https://customermates.com/en/docs/concepts Customermates tracks five record types, lets each one carry user-defined fields, links them to each other through typed relationships, and emits webhooks whenever any of that changes. ## The five record types | Record type | What it represents | Typical fields | |---|---|---| | **Contact** | A person | firstName, lastName, notes | | **Organization** | A company | name, notes | | **Deal** | A sales opportunity | name, totalValue, totalQuantity | | **Service** | A product or offering attached to a deal | name, amount | | **Task** | A todo item | name, assignees | Each record has `id`, `createdAt`, `updatedAt`, and supports markdown `notes`. Everything else is either a relationship to another record or a custom column value. There are no fixed status or stage fields on any record type; use custom columns for those. Call `get_record_schema` to read the columns that exist in a given workspace. ## Relationships Relationships are typed and many-to-many. A contact belongs to zero or more organizations and zero or more deals. A deal belongs to zero or more contacts, organizations, and services (with quantities), plus assignees. | From to | Example | |---|---| | Contact to Organization | "Max works at Initech" | | Contact to Deal | "Max is a stakeholder on the Q2 contract" | | Organization to Deal | "The Q2 contract is with Initech" | | Deal to Service (with quantity) | "5 hours consulting plus 1 setup fee" | | Deal / Service / Task to User | "Assigned to Julia" | | Task to Contact / Organization / Deal / Service | "Follow up on the Q2 contract" | Tasks link to users, contacts, organizations, deals, and services. ## Custom columns The default schema covers the basics. For anything else, add a custom column. Ten types: | Type | Use for | |---|---| | **Plain** | Free-text values | | **Date** | Calendar dates (renewal, next touch) | | **DateRange** | A start and end date | | **DateTime** | Timestamps | | **DateTimeRange** | A start and end timestamp | | **Currency** | Money amounts, stored with an ISO currency code | | **Single-select** | A value from a fixed option list | | **Link** | One or more URLs | | **Email** | One or more email addresses | | **Phone** | One or more phone numbers | Single-select columns are how you model your own workflows. A deal "Status" or a task "Priority" is a single-select column you define, with the options you choose, per workspace. A board view can then group records by any single-select column. Custom columns are first-class in filters, widgets, and the MCP surface. ## Widgets Widgets are dashboard charts driven by live data. You pick a record type, a group-by axis (plain field or custom column), an aggregation (count, deal value, deal quantity), and a display type (bar, doughnut, or radar). Filters narrow the data set. A widget re-renders whenever underlying records change. ## Webhooks Every write emits a domain event: `contact.created`, `deal.updated`, `task.deleted`, and so on. You subscribe by URL and event list. Each delivery includes the changed record plus a `changes` map showing what moved from what to what, so subscribers can act on diffs without polling. See [Webhooks](/docs/webhooks) for the full event catalog and payload shape. ## Notes Notes are per-record markdown. They render through a Tiptap editor in the UI and are plain markdown everywhere else. You can replace them or append to them via `update_record_notes` with the matching `mode`. ## Users, roles, and the company Users are your teammates. Roles are defined per workspace and control what each user can read and write. The company is the tenant. Every record lives inside one company, and cross-tenant access is not possible. ## Next - **[Custom columns](/docs/mcp#tool-catalog)**: recipes and gotchas. - **[Filter syntax](/docs/filter-syntax)**: operators and examples. - **[MCP tool catalog](/docs/mcp#tool-catalog)**: every tool that operates on these concepts. --- # Connect Claude & ChatGPT (custom connector) Source: https://customermates.com/en/docs/connect-custom-connector Customermates is an open-source, AI-native CRM that your AI operates directly over MCP. The custom connector is the simplest way to connect: paste one URL, sign in to Customermates, and approve access. There is no API key to copy, no config file to edit, and no shim to install. You authorize once and the connection persists. This is the recommended path for **Claude** (web, desktop, and mobile) and **ChatGPT**. On a CLI or editor (Claude Code, Codex, Cursor, Gemini CLI), use [the API key method](/docs/connect-cli) instead. > Custom connectors are a paid feature: Claude (Pro, Max, Team, Enterprise) and ChatGPT (Plus, Pro, Business, Enterprise). Free tiers cannot add MCP servers. ## The URL ``` https://customermates.com/api/v1/mcp ``` Self-hosting? Swap in your instance URL. ## Claude **Claude → Settings → Connectors → Add custom connector.** Leave authentication on the default (OAuth). No header, no key. A Customermates window opens. Sign in, then click **Approve** on the Authorize access screen. Back in Claude, Customermates appears with its tools listed. Add it once and it syncs to Claude on web, desktop, and mobile. The connection is tied to your Claude account, not one device, and refreshes in the background. One click instead: [Add to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Customermates&connectorUrl=https%3A%2F%2Fcustomermates.com%2Fapi%2Fv1%2Fmcp) pre-fills the add-connector dialog for you (Claude web and Teams only). ## ChatGPT In ChatGPT: **Settings → Connectors → Add connector → Custom connector (MCP)**. - **Name:** Customermates - **URL:** `https://customermates.com/api/v1/mcp` (self-host: your instance) - **Authentication:** OAuth Save. ChatGPT opens a Customermates window. Sign in, then click **Approve** on the Authorize access screen. Every Customermates tool is listed, with no key to store or rotate. ### ChatGPT with an API key Prefer a static key? Add the connector the same way, but authenticate with a header: **Profile → API Keys → New key**. Copy the 64-character string immediately. It is shown once. In the connector form set **Authentication: Header**, **Header name: `x-api-key`**, **Header value:** your key. Save. ChatGPT reaches the server and lists every tool. ## Sign in and approve The connector opens a Customermates window: Or you are already signed in. An **Authorize access** screen names the app that is connecting and the destination it will send access to. Click **Approve**. You return to your AI, connected, with all 46 tools listed. The connector accepts any client, so the **Authorize access** screen is the gate. It names the app and shows the destination host. Approve only a connection you started yourself; decline anything you did not start. ## It syncs and stays connected - Add it once on Claude web and it appears on **desktop and mobile** too. The connection is tied to your Claude account, not one device. - It refreshes in the background. Use it within any 30-day window and you never re-authorize; go idle longer and you approve once more. - Every call runs as the Customermates user who approved, inside that account's data. API keys inherit the owning user's permissions; there is no per-key scoping. ## Try your first prompt The server sends its instructions to your AI when it connects, so the client already has the workflow and safety rules. In clients that support MCP prompts (like Claude), you can also run the built-in `get-started` prompt for a personalized start: your name, your role, and a first summary of your workspace. - *"Pull the last ten contacts I created. Any without an email address?"* - *"Create a contact for Jane Doe at Initech, link it to the Initech organization, and start a deal for 12 hours of consulting."* - *"Set the Status column on the Acme deal to Won and add a note that the contract was signed today."* The record types are contact, organization, deal, service, and task. Fields such as a deal's status or a task's priority are configurable custom columns per workspace, not fixed product fields. Ask your AI to call `get_record_schema` to see the columns and their allowed values before writing. ## Confirming tool calls Your AI decides how much to confirm before each tool runs, and you control it in the client. In Claude: **Settings → Connectors → Customermates**, then set each tool (or the read-only group) to *Always allow* or *Ask*. Reads (`get_*`, `list_*`, `search_*`) are flagged read-only, and writes and deletes are flagged too, so you can leave those on *Ask*. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | "Couldn't register with the sign-in service" (Claude) or "Connector verification failed" (ChatGPT) | Server URL wrong or unreachable; on header auth, a bad key | Confirm it is exactly `https://customermates.com/api/v1/mcp`; on header auth re-check the key | | No "Add custom connector" option | Free plan | Custom connectors need a paid Claude or ChatGPT plan. See [the CLI method](/docs/connect-cli) | | Approved but nothing connects | You approved from a different account | Sign into the right Customermates account, then retry | | "Tool not found" | Stale connector cache | Remove and re-add the connector | | Server rejects a relationship update | Null-wipe guard fired | Ask the agent to use `manage_record_links` | | Enum field rejected | Value not in the column's option list | Ask the agent to call `get_record_schema` first | ## On a free plan or in a CLI? Custom connectors need a paid plan, and CLI and editor clients do not use the connector flow at all. Both authenticate with an API key instead: see [CLI & editors](/docs/connect-cli). Claude Desktop on a free plan can use the [config-file fallback](/docs/connect-cli#claude-desktop-config-file). ## Next - [MCP tool catalog](/docs/mcp#tool-catalog): every tool your AI can call. - [Webhooks](/docs/webhooks): the other half of the agentic loop. - [CLI & editors](/docs/connect-cli): API-key setup for Claude Code, Codex, Cursor, and Gemini CLI. --- # Connect CLI & editor clients (API key) Source: https://customermates.com/en/docs/connect-cli Customermates is the open-source, AI-native CRM. Once connected, your coding agent reads and writes contacts, deals, and notes without leaving the terminal. Every client on this page authenticates the same way: an API key sent in the `x-api-key` header against `https://customermates.com/api/v1/mcp`. If you self-host, replace the URL with your instance everywhere below. > Using Claude's or ChatGPT's **app** instead? Those connect with [the custom connector](/docs/connect-custom-connector) (OAuth, no key). ## Create an API key In Customermates: **Profile → API Keys → New key**. Name it for the client (e.g. `Claude Code`). The 64-character key is shown once, so copy it immediately. A key inherits the permissions of the user who created it; there is no per-key scoping. Create one key per client if you want audit-log granularity. ## Claude Code Claude Code adds MCP servers via its CLI. One command does the setup. Run this in any terminal, replacing `YOUR_KEY`: There is no restart or config-file edit. Claude Code connects automatically. Run `claude mcp list` to confirm `customermates` shows up. **Scope:** the command defaults to local scope. Add `--scope user` to make Customermates available everywhere on your machine, or `--scope project` to write it to a repo's `.mcp.json` so teammates pick it up. ## Codex OpenAI's Codex CLI uses a TOML config file. The `codex mcp add` subcommand only handles stdio servers, so for an HTTP MCP server like Customermates you add the block manually. Open `~/.codex/config.toml` (create it if missing) and append, replacing `YOUR_KEY`: To pull the key from your environment instead of storing it in the file: ```toml [mcp_servers.customermates] url = "https://customermates.com/api/v1/mcp" env_http_headers = { "x-api-key" = "CUSTOMERMATES_API_KEY" } ``` Then export `CUSTOMERMATES_API_KEY` in your shell profile. New sessions pick the server up, so end the current one and start fresh. ## Cursor Open **Settings → Tools & MCP → Add new MCP server** and paste this, replacing `YOUR_KEY`: Cursor hot-reloads, so no restart is needed. The tools appear in Composer right away. To edit the file directly, the same block goes in `~/.cursor/mcp.json` (global) or `/.cursor/mcp.json` (per-project), wrapped under `mcpServers`. ## Gemini CLI The Gemini CLI loads MCP servers from `~/.gemini/settings.json`. Merge this in, replacing `YOUR_KEY`: If `mcpServers` already has entries, add `customermates` alongside the existing ones rather than overwriting the object. ## Claude Desktop (config file) The recommended path for Claude Desktop is the [connector path](/docs/connect-custom-connector#claude): OAuth, no key, synced across your devices. On a free plan, or if you prefer a static key, the config file below works too. Open **Claude → Settings → Developer → Edit Config**, or the file directly: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` Merge this in, replacing `YOUR_KEY`: `mcp-remote` is a shim that lets Claude Desktop talk to a remote HTTP MCP server over stdio. It downloads on first run via `npx`, so you need Node 18+ on `PATH`. Then restart: fully quit (⌘Q on macOS, not just close the window) and reopen. Customermates appears in the tools panel with the CRM tools listed. ## Try your first prompt The server briefs your agent at connect, so it already knows the tool surface and the safety rules. In clients that support MCP prompts (like Claude Code), you can run the built-in `get-started` prompt for a guided kickoff: it interviews you, then summarizes your workspace. - *"Set the Acme deal's Status column to 'Won' and add a note that the contract was signed today."* - *"Pull the last ten contacts I created. Any without an email address?"* - *"Create a contact for Jane Doe at Initech, link it to the Initech organization, and start a deal for 12 hours of consulting."* The available record types are contact, organization, deal, service, and task. Deals and tasks have no fixed pipeline field. Attributes like a deal's status or a task's priority are configurable custom columns, so the values in a prompt depend on how your workspace is set up. Call `get_record_schema` to see the columns a record type currently has. ## Troubleshooting | Client | Symptom | Cause | Fix | |---|---|---|---| | Claude Desktop | `npx: command not found` | No Node on `PATH` | Install Node 18+ from [nodejs.org](https://nodejs.org) | | Claude Desktop | Tools panel empty after a config edit | Claude Desktop does not hot-reload MCP configs | Fully quit (⌘Q) and reopen | | Codex | TOML parse error | Inline-table values use `=`, not `:` | Check the `env_http_headers = { ... }` line | | All | "Invalid API key" | Wrong or truncated key | Regenerate in Profile → API Keys; the key must be the full 64 characters | | All | Relation update rejected | `update_*` tools do not accept relation arrays | Ask the agent to use `manage_record_links` to add or remove links | ## Next - [Custom connector](/docs/connect-custom-connector): the OAuth path for Claude and ChatGPT apps. - [MCP tool catalog](/docs/mcp#tool-catalog): every tool the agent can call. - [Webhooks](/docs/webhooks): subscribe other systems to CRM changes. --- # Messaging rate limits Source: https://customermates.com/en/docs/messaging-rate-limits When you connect a channel (LinkedIn, WhatsApp, email, and so on), Customermates talks to that provider through Unipile. Every provider enforces its own request limits, and exceeding them can get the underlying account throttled or suspended. To stay within safe bounds, Customermates applies a conservative request budget per channel and pauses before a provider would push back. A paused sync is expected behavior: it keeps the connected account within provider limits and resumes on its own. ## How the limits work Limits are configured in the Unipile dashboard and enforced by Unipile. They apply **per connected account** at three levels at once: - **Per endpoint:** a cap on each individual operation (listing chats, reading messages) over a rolling minute and day. These are the read limits below. - **Per sensitive method:** a stricter cap on riskier operations (profile lookups, sending a message or a connection request). These are the sensitive limits below. - **Per account:** an overall cap across every call for the account. When a window is full, further calls are rejected until it rolls over. ## Read limits Read operations sync history. They are set higher than writes because reads rarely trigger a provider ban. | Provider | Reads / minute | Reads / day | | --- | --- | --- | | LinkedIn | 20 | 300 | | WhatsApp | 30 | 510 | | Instagram | 50 | 510 | | Telegram | 50 | none | | Gmail / Google | 100 | none | | Outlook | 100 | none | | IMAP | 100 | none | LinkedIn stays the most conservative because its automated-activity detection is the strictest. Some values are shaped by the provider's own ceilings: social channels cap the per-minute read limit at 50, email channels at 100, and the daily limit moves in steps of 30 (so a target of 500 is applied as 510). Telegram and the email providers do not expose a separate daily read limit. ## Sensitive limits The riskier operations stay conservative regardless of the read budget: profile lookups (which providers treat as scraping-sensitive) and any send action. | Provider | Action | Limit | | --- | --- | --- | | LinkedIn | Get company profile | 1 / second | | LinkedIn | Get user profile | 1 / second | | LinkedIn | Send connection request | 50 / day, 1 / 10 seconds | | WhatsApp | Start a chat | 3 / minute, 30 / day | | Telegram | Start a chat | 3 / minute, 30 / day | | Instagram | Start a chat | 3 / minute, 30 / day | | Gmail / Google | Send an email | 300 / day | | Outlook | Send an email | 300 / day | | IMAP | Send an email | 300 / day | ## What you see when a limit is reached The action returns a message with the time to wait, for example: > This channel hit its rate limit. You can try again in 2 minutes. You can retry once the indicated window passes. Nothing is lost in the meantime. ## History syncs itself Importing a full message history can be large, roughly one request per conversation. If a limit is reached mid-import, the background importer **pauses and resumes automatically** when the window frees up, with no action needed on your part. Opening a conversation also loads its messages on demand, and new activity arrives in real time over webhooks. --- # CRM MCP Server Reference: Endpoint and Tools Source: https://customermates.com/en/docs/mcp Customermates exposes one MCP endpoint at `https://customermates.com/api/v1/mcp`. A connected client discovers all 46 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](/docs/connect-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](/docs/connect-custom-connector#claude), [ChatGPT](/docs/connect-custom-connector#chatgpt), [Claude Code](/docs/connect-cli#claude-code), [Codex](/docs/connect-cli#codex), [Cursor](/docs/connect-cli#cursor), or [Gemini](/docs/connect-cli#gemini-cli). 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](/docs/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: ``` 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](/docs/connect-custom-connector). ## Connect a client Getting an AI client onto your workspace is the same three moves every time: **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. 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. Ask the client to list its tools. All 46 should appear, and `get_workspace_context` is the natural first call: it returns your user, company, roles, and connected accounts in one go. | Client | Method | Guide | |---|---|---| | Claude web & mobile | Custom connector (OAuth) | [Connect with a custom connector](/docs/connect-custom-connector) | | Claude Desktop | Connector (OAuth) or config key | [Connect Claude Desktop](/docs/connect-custom-connector#claude) | | ChatGPT | Connector (OAuth) or key header | [Connect ChatGPT](/docs/connect-custom-connector#chatgpt) | | Claude Code | API key | [Connect Claude Code](/docs/connect-cli#claude-code) | | Codex | API key | [Connect Codex](/docs/connect-cli#codex) | | Cursor | API key | [Connect Cursor](/docs/connect-cli#cursor) | | Gemini CLI | API key | [Connect Gemini](/docs/connect-cli#gemini-cli) | | Any MCP client | Key header | Use 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 46-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=](/docs/mcp#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 46 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. {/* mcp-catalog:records */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `get_record_schema` | ✓ | | Schema and custom-column metadata, never record data. One entity type, or all five when `entity` is omitted. Call before any create or update. | | `list_records` | ✓ | | Search, filter, sort, paginate one entity type. Always returns the total. Deals include totalValue and totalQuantity, services include amount. | | `search_records` | ✓ | | Free-text search across one or more entity types in one call. | | `get_records` | ✓ | | Full 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_contacts` | | | Create up to 100 contacts, custom-column values and relation ids inline. | | `create_organizations` | | | Create up to 100 organizations, custom-column values and contact/user/deal/task ids inline. | | `create_deals` | | | Create up to 100 deals, services as an inline array. | | `create_services` | | | Create up to 100 services, custom-column values and user/deal/task ids inline. | | `create_tasks` | | | Create up to 100 tasks, custom-column values and relation ids inline. | | `update_contacts` | | | Partial 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_organizations` | | | Partial update by id. Never touches relations. | | `update_deals` | | | Partial 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_services` | | | Partial update by id. | | `update_tasks` | | | Partial update by id, including singleSelect custom-column values. Never touches relations. | | `update_record_notes` | | | Replace or append markdown notes on 1 to 100 records, selected by `mode`. | | `manage_record_links` | | | Add or remove ids on one relation (`action` add or remove). The only way to change relations. | | `delete_records` | | ✓ | IRREVERSIBLE hard-delete of 1 to 100 records by id (contacts also by email, phone, or provider:value). | {/* /mcp-catalog */} All create and update tools take custom-column values via customFieldValues; call `get_record_schema` first for the column ids. ### Workspace {/* mcp-catalog:workspace */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `get_workspace_context` | ✓ | | Your 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_users` | ✓ | | Team members with id, name, email, roleId, and status. | {/* /mcp-catalog */} ### 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. {/* mcp-catalog:messaging */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `get_messaging_threads` | ✓ | | Two modes: without threadId lists inbox threads with filters and sorting (threads with no message yet are hidden unless they hold a draft); with threadId returns one thread plus a page of its messages (default 25, newest first, drafts included). | | `get_activities` | ✓ | | Activity 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_calendars` | ✓ | | Three 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_message` | | | Delivers immediately. With threadId replies in an existing chat; with connectedAccountId plus attendeeIdentifiers starts a new one (optional chatName names a group). 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_email` | | | Delivers immediately. Send or reply from a connected email account; can send a saved draft via draftMessageId. | | `save_message_draft` | | | Prepare a reply for review: the draft shows up in the inbox compose box and the user sends it. Drafts are thread-bound, one per thread, saving again updates it. | | `discard_message_draft` | | ✓ | Delete a draft by its draft message id. | | `update_messaging_thread` | | | Set the thread state: unread, open, closed, or spam. | | `connect_messaging_account` | | | Generate 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. | {/* /mcp-catalog */} ### Social posts {/* mcp-catalog:social */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `get_social_posts` | ✓ | | Posts 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_engagement` | ✓ | | Engagement on a post: kind=comments (default) lists comments, kind=reactions lists who reacted; with commentId it returns the reactions on that comment. | | `get_social_profile` | ✓ | | A 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_relations` | | | Connection 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_leads` | ✓ | | Finds 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_companies` | ✓ | | Finds 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_parameters` | ✓ | | Resolves 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_lists` | | | LinkedIn 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. | {/* /mcp-catalog */} #### 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: ```json { "connectedAccountId": "00000000-0000-4000-8000-000000000001", "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: ```json { "connectedAccountId": "00000000-0000-4000-8000-000000000001", "authorIdentifier": "", "limit": 10 } ``` If next_cursor is non-null, repeat the same `connectedAccountId`, `authorIdentifier`, and `limit`, set `cursor` to that value, and omit `offset`: ```json { "connectedAccountId": "00000000-0000-4000-8000-000000000001", "authorIdentifier": "", "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 {/* mcp-catalog:docs */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `search_docs` | ✓ | | Full-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_page` | ✓ | | One documentation page as markdown with its canonical URL. Lists valid slugs on a miss. | | `search` | ✓ | | Required by ChatGPT deep research connectors; federates CRM records and docs. Interactive agents should prefer `search_records` or `search_docs`. | | `fetch` | ✓ | | Deep-research companion to `search`: fetches one result by its id. | {/* /mcp-catalog */} ### Custom columns {/* mcp-catalog:custom-columns */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `manage_custom_columns` | | ✓ | One 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. | {/* /mcp-catalog */} ### Widgets {/* mcp-catalog:widgets */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `manage_widgets` | | ✓ | One 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. | {/* /mcp-catalog */} ### Webhooks {/* mcp-catalog:webhooks */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `manage_webhooks` | | ✓ | One 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). | {/* /mcp-catalog */} ### Admin and team {/* mcp-catalog:admin */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `update_workspace_settings` | | | `target` profile updates your own name, country, and avatar; `target` company updates the workspace currency (admin only). | | `manage_team` | | | Invite members by email (action invite, up to 20, sends real invitation emails) or change a member's role and status (action update_member). | {/* /mcp-catalog */} ### Support {/* mcp-catalog:support */} | Tool | Read | Destructive | Purpose | |---|---|---|---| | `request_support` | | | Open 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. | {/* /mcp-catalog */} ### Narrowing with ?toolsets= All 46 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, 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: ```json { "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. Drafts are thread-bound; a brand-new outbound message cannot be drafted. - **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 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. 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. 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. 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. 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. 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 - [Custom connector](/docs/connect-custom-connector): end-to-end setup on one page. - [Filter syntax](/docs/filter-syntax): every operator, with examples. - [Webhooks](/docs/webhooks): the other half of the agentic loop. --- # Webhooks: Signatures, Retries, and Event Catalog Source: https://customermates.com/en/docs/webhooks Customermates sends a JSON POST to your HTTPS endpoint whenever subscribed data changes. You choose the events and provide the URL. Failed deliveries can be re-sent from the UI or through MCP. Deliveries are signed with HMAC-SHA256 over the raw request body. Every event and its payload shape is listed in the catalog at the end of this page. ## Uses Webhooks let other systems react to CRM changes without polling. - Push new contacts into an email sequencer. - Open a ticket in a support tool when a deal changes state. - Trigger a finance workflow when a deal is marked as closed. - Sync records into a data warehouse in near real time. ## Create a webhook **UI:** Company, then Webhooks, then New. **MCP:** `manage_webhooks` with `action: "create"`, plus `url`, `events[]`, and optional `description`, `secret`, `enabled`. ```json { "url": "https://hooks.example.com/customermates", "events": ["contact.created", "contact.updated", "deal.updated"], "secret": "use-a-random-string-from-a-password-manager", "enabled": true } ``` The URL must be HTTPS. HTTP is rejected. ## What you receive Every delivery is a `POST` with `Content-Type: application/json` and the same envelope: ```json { "event": "", "data": { "userId": "", "companyId": "", "entityId": "", "payload": { /* event-specific */ } }, "timestamp": "" } ``` `userId` is the user who caused the write, including a user acting through an API key. For messaging events, which are triggered by inbound provider activity rather than a user action, `userId` is `null`. For `*.updated` events, `payload` wraps the full record together with a `changes` object. For `*.created` and `*.deleted` events, `payload` is the record itself. Example, `contact.updated`: ```json { "event": "contact.updated", "data": { "userId": "u_123", "companyId": "c_abc", "entityId": "ct_xyz", "payload": { "contact": { "id": "ct_xyz", "firstName": "Max", "lastName": "Mustermann", "notes": { /* Tiptap JSON */ }, "organizations": [{ "id": "org_1", "name": "Example GmbH" }], "users": [], "deals": [{ "id": "deal_1" }, { "id": "deal_2" }], "customFieldValues": [ { "columnId": "col_abc", "value": "Won" } ] }, "changes": { "organizations": { "previous": [], "current": [{ "id": "org_1", "name": "Example GmbH" }] } } } }, "timestamp": "2026-04-22T10:00:00.000Z" } ``` The record fields depend on the entity type. Custom columns appear under `customFieldValues` as `columnId` and `value` pairs. Use `get_record_schema` to see the columns configured for a given entity in your workspace. ## The `changes` object `changes` is present only on `*.updated` events. Each key is a record field whose value moved. `previous` is the value before the write, `current` is the value after. Arrays of related records compare by id. Objects compare by deep equality. Scalar fields compare by value. `createdAt` and `updatedAt` are never reported as changes. If a write does not change any field, the event is suppressed. No-op `*.updated` events are not delivered. ## Signature verification If you set a `secret`, every request includes: ``` X-Webhook-Signature: ``` `` is `HMAC-SHA256(secret, rawRequestBody)`, lowercase hex, no prefix. Recompute it on your side and compare in constant time. Node.js example: ```ts import crypto from "crypto"; function verify(rawBody: string, received: string, secret: string) { const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } ``` ## Retries and failed deliveries Every delivery is recorded. Review deliveries in the UI (Company, then Webhook Deliveries) or through `manage_webhooks` with `action: "list_deliveries"`. A delivery is marked failed if the HTTP status is outside the 2xx range or the request times out. The request timeout is 5 seconds. Customermates automatically retries failed deliveries: up to 5 retries for timeouts, 5xx responses, and 408, 425, and 429. Permanent 4xx responses (for example 400, 401, 403, 404) are not retried, because a repeat would fail the same way. Retries happen inside the delivery run, so they do not create extra delivery records; each record shows the final outcome. You can also resend any delivery yourself from the UI or with `manage_webhooks` and `action: "resend_delivery"` plus the delivery id. A manual resend creates a new delivery record; the original is unchanged. ## Ordering and concurrency Deliveries are not strictly ordered across events. Two `contact.updated` events milliseconds apart can arrive out of order. Use the `timestamp` in the payload to resolve order, or treat deliveries as idempotent messages keyed by `entityId`. ## Notes are JSON, not markdown The `notes` field in payloads is Tiptap JSON, the same structure the editor stores. To render it as markdown on your side, run it through a Tiptap-compatible serializer. In MCP, `get_records` with `include: "withNotes"` returns notes already serialized to markdown. ## Delete payloads Delete events carry the full record as it was before deletion, under `payload`, not just the id. The affected record id is also available as `data.entityId`. ## Debugging - Use [webhook.site](https://webhook.site) as a throwaway receiver during integration testing. - The delivery log (`manage_webhooks`, `action: "list_deliveries"`) supports `searchTerm` on url and event name. - If your receiver returns an error status, Customermates records the response so you can inspect what happened. ## Event catalog Twenty-six events are available to subscribe to: fifteen record events across the five entity types, and eleven messaging events. All use the envelope above. ### Record events | Event | When it fires | `payload` | |---|---|---| | `contact.created` | A contact is created via UI, API, MCP, or import | full contact | | `contact.updated` | Any contact field changes | `contact`, `changes` | | `contact.deleted` | A contact is deleted | full contact | | `organization.created` | An organization is created | full organization | | `organization.updated` | Any organization field changes | `organization`, `changes` | | `organization.deleted` | An organization is deleted | full organization | | `deal.created` | A deal is created | full deal | | `deal.updated` | Any deal field changes | `deal`, `changes` | | `deal.deleted` | A deal is deleted | full deal | | `service.created` | A service is created | full service | | `service.updated` | Any service field changes | `service`, `changes` | | `service.deleted` | A service is deleted | full service | | `task.created` | A task is created | full task | | `task.updated` | Any task field changes | `task`, `changes` | | `task.deleted` | A task is deleted | full task | ### Messaging events Messaging events fire on inbound provider activity on connected accounts. `userId` is `null`. Payloads reference provider records by id (`connectedAccountId`, `provider`, `providerMessageId`, `threadId`, and similar) rather than embedding CRM records. | Event | When it fires | |---|---| | `messaging.message.received` | A chat message or email is ingested | | `messaging.message.updated` | An ingested message is updated | | `messaging.message.deleted` | An ingested message is deleted | | `messaging.message.reaction` | A reaction is added to a message | | `messaging.email.received` | An email is ingested | | `messaging.email.deleted` | An ingested email is deleted | | `messaging.chat.updated` | A chat thread is updated | | `messaging.chat.deleted` | A chat thread is deleted | | `messaging.calendar.changed` | A connected calendar changes | | `messaging.calendar_event.changed` | A calendar event changes | | `messaging.relation.created` | A new provider relation is created | ## Next - [MCP tool catalog](/docs/mcp#tool-catalog): programmatic webhook management. - [n8n integration](/docs/n8n): drop webhooks into visual workflows. --- # Connect n8n to Customermates: Webhooks, REST API Source: https://customermates.com/en/docs/n8n Connect n8n to Customermates by subscribing a Webhook node to CRM events and calling the REST API from HTTP Request nodes. No custom node is required for this; the REST and MCP surfaces work directly. A Customermates community node is also available at [github.com/customermates/n8n-nodes-crm](https://github.com/customermates/n8n-nodes-crm) if you prefer dedicated CRM nodes over generic HTTP Request nodes. ## When n8n fits - Your team prefers visual flows over writing code. - You want to branch and transform events before routing them to another system, such as Slack, HubSpot, Google Sheets, or a data warehouse. - You already run n8n for other automations. If you want an AI to drive the CRM in conversation, use [MCP](/docs/mcp) instead or alongside. ## Pattern 1: trigger n8n from CRM changes Copy the URL it generates. Go to **Company → Webhooks → New**, paste the URL, and select the events you want. Add steps after the Webhook node to transform and forward it. The payload shape is documented at [Webhooks](/docs/webhooks). Common next steps: - **Slack**: post a message to a channel when a deal is created. - **Google Sheets**: append a row every time a contact is created. - **HubSpot**: mirror the contact. ## Pattern 2: push to Customermates from n8n Use the **HTTP Request** node: - Method: `POST` for creates, `PATCH` for updates. - URL: `https://customermates.com/api/v1/`, where `` is `contacts`, `organizations`, `deals`, `services`, or `tasks`. - Headers: `x-api-key` set to your API key, stored as an n8n credential. - Body: JSON matching the [OpenAPI spec](/docs/openapi). To make MCP tool calls from n8n, send a request to `/api/v1/mcp` with `method: "tools/call"`. This routes writes through the same guardrails the MCP layer adds, such as rejecting a `null` on a relationship array. ## Pattern 3: scheduled jobs n8n's Cron trigger plus the Customermates REST API covers recurring work: - Daily: find contacts without an organization and post a reminder. - Weekly: export records matching a filter to a Google Sheet. - Monthly: archive records whose custom status column holds a given value. Deal and task lifecycle values such as status or priority are workspace-configurable singleSelect custom columns, not fixed fields. Call `get_record_schema` or read the OpenAPI spec to see which columns and options exist in your workspace before filtering on them. ## Authentication Store the API key as an n8n **Header Auth** credential with header name `x-api-key`. Reuse it across nodes so you rotate it in one place. Keys are 64-character base62 strings and inherit the owning user's permissions; there is no per-key scoping. ## Common gotchas - **Relationship arrays**: passing `null` on a relationship array is rejected. Send `[]` to clear, or use the `manage_record_links` tool to add or remove links. - **Rate limits**: high-frequency loops can hit API limits. Batch where possible. - **Webhook ordering**: deliveries are not strictly ordered. Treat them as idempotent messages keyed by `entityId`. ## Next - [Webhooks](/docs/webhooks): the trigger side in detail. - [OpenAPI](/docs/openapi): full REST reference for the action side. - [MCP](/docs/mcp): if you want an AI in the loop. --- # Self-Hosting Customermates with Docker Compose Source: https://customermates.com/en/docs/self-hosting Self-hosting requires two files (`docker-compose.yml` and `.env`) plus `docker compose up -d`. Both files live in the [Customermates repo](https://github.com/customermates/customermates) and you fetch them with `curl`. No `git clone` and no build step are needed. The published image at `ghcr.io/customermates/customermates:latest` applies database migrations on first boot. ## Self-host vs cloud Self-hosting uses the Starter entitlement baseline. It includes the core CRM records, views, REST, webhooks and MCP for an external AI client that you select and fund. It does not include connected messaging accounts, the unified inbox, connected calendar view, hosted Mate or hosted AI credits. Paying for another cloud plan does not expand the self-hosted entitlement set. | Decision factor | Managed cloud | Self-hosted | | ------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Application and database operations | Customermates operates them | You operate Docker, PostgreSQL, network access, TLS, updates and backups | | Core CRM records and views | Included by plan | Starter baseline | | Unified inbox and connected calendar | Available on entitled cloud plans | Not included | | AI access | External MCP; hosted Mate capability remains release-gated | External MCP with your own client and provider | | Connected providers | Configured within the managed product | You configure and assess every external provider | | Pricing | Per-user cloud subscription from €12 per month | Your infrastructure, providers and operator time | Self-hosting lets you choose where the application and database run. It does not create a security, compliance, privacy or air-gap guarantee by itself. Your organization remains responsible for configuration, providers, contracts, retention and operating controls. Compare total operating cost rather than assuming one model is cheaper. Include infrastructure, backups, restore testing, monitoring, updates, incident response and external providers as well as any subscription price. Customermates exports and imports CRM records as Excel workbooks, one entity type at a time, so a working set can move between systems. A whole-platform migration is still a separately designed and validated project. ## Install ### Prerequisites - Docker and Docker Compose v2. - A domain name if you want TLS (optional for local). ```bash mkdir customermates && cd customermates curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/docker-compose.yml -o docker-compose.yml curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/.env.selfhost.template -o .env ``` Then edit `.env` with real values: - `BETTER_AUTH_SECRET`: a long random string (`openssl rand -hex 32`). - `POSTGRES_PASSWORD`: change the default. - `BASE_URL`: your public URL (e.g. `https://crm.example.com`). Defaults to `http://localhost:4000` for local. - `RESEND_API_KEY` and `RESEND_OPERATOR_EMAIL`: a configured [Resend](https://resend.com) project. Required for signup verification, password reset, and invitation emails. ```bash docker compose up -d ``` First boot takes a minute while Prisma applies migrations. Watch the logs: ```bash docker compose logs -f app ``` When the app is ready, open `http://localhost:4000` (or your custom `APP_PORT`). Open the URL. Sign up with your email, click the verification link from the inbox, then choose a workspace name. Manage roles for additional users from Company → Users and Company → Roles. Point your reverse proxy (Caddy, nginx, Traefik) at the app port (4000 by default, or your custom `APP_PORT`). Caddy example: ```text crm.example.com { reverse_proxy localhost:4000 } ``` Customermates sets secure cookies when `BASE_URL` uses `https://`. Make sure the proxy forwards `X-Forwarded-Proto` correctly. Profile → API Keys → New key. Same flow as cloud. See [API keys](/docs/api-keys). ## Day-to-day operations ### Update ```bash docker compose pull docker compose up -d ``` Pulls the latest app image and restarts the affected services. Migrations run automatically on container boot. Verify with `docker compose ps`. ### Apply configuration changes ```bash docker compose up -d ``` Reconciles the stack and recreates affected containers when configuration changed. Use it after `.env` changes. For a simple restart with unchanged configuration, `docker compose restart` is sufficient. ### Logs and troubleshooting ```bash docker compose logs -f app docker compose logs -f postgres docker compose ps docker compose exec app sh ``` Enabled background jobs run in-process through the Postgres-backed worker that starts with the application. There is no separate worker service in the Compose file. Inspect application logs with `docker compose logs -f app`. ### Reset all data ```bash docker compose down -v docker compose up -d ``` `-v` deletes the Postgres volume. IRREVERSIBLE. Take a backup first if you need the data. ## Backups Back up Postgres with `pg_dump` on a schedule. The app container is stateless. ```bash mkdir -p backups docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \ | gzip > backups/customermates-$(date +%Y%m%d).sql.gz ``` For production: - Schedule daily dumps to a separate volume or off-site storage. - Test restore procedures in a non-production environment. - Keep `.env` and secrets out of source control. ## Edition boundary Every self-hosted deployment uses Starter entitlements. Connected messaging accounts, the unified inbox, connected calendar view, hosted Mate and hosted AI credits are cloud-only capabilities and are not unlocked in self-hosting by a paid cloud plan. External AI clients remain available through MCP. Review `LICENSE` and `ee/LICENSE.md` in the repository for the exact code-license boundary. The hosted in-app Assistant is cloud-only and remains release-gated. External MCP uses your own AI provider. Enterprise SSO and white-labeling are not implemented in either deployment model. ## Next - [Architecture and security](/docs/architecture-security): what you're running. - [API keys](/docs/api-keys): key hygiene. - [Connect your AI](/docs/connect-custom-connector): once installed, point an MCP client at your instance. --- # CRM Security: Tenant Isolation and Audit Logs Source: https://customermates.com/en/docs/architecture-security Customermates enforces a single-tenant-per-company data model at every layer. Every record is scoped to a company, API keys carry the owning user's identity, and agents see only what that user can see. ## Stack - **Next.js 16** (App Router, Turbopack): web app and API. - **PostgreSQL**: primary datastore. JSONB for custom column values and webhook payloads. - **Prisma**: ORM and migrations. - **better-auth**: sessions, social login, API keys, MCP OAuth. - **mcp-handler**: Model Context Protocol endpoint. - **TypeScript** end to end, Zod-validated at every boundary. ## Tenancy Every record belongs to a **Company**. The company id is enforced in three places: 1. **App layer**: every interactor resolves the current user's company and filters by it. 2. **Prisma layer**: queries include `companyId` in every `where`. 3. **Decorator layer**: `@TenantInteractor` makes interactors that fail to scope throw at runtime. Cross-tenant reads are not possible through the public surface. There is no admin panel that bypasses scope. ## Authentication Three ways to authenticate: - **Session cookie** (UI): signed, http-only, `Secure` under HTTPS. - **API key**: 64-character base62 token (a-z, A-Z, 0-9), issued by the better-auth apiKey plugin, hashed at rest, tied to a user. Sent in the `x-api-key` header. - **OAuth 2.1** (MCP): remote MCP clients can authorize via better-auth and send a Bearer token instead of an API key. All methods resolve to the same user and company context. ## Authorization Per-user and role-driven. Roles carry permissions on resources (contacts, organizations, deals, services, tasks) and actions (read, create, update, delete). Every interactor calls `userService.hasPermissionOrThrow(resource, action)` before acting. API keys inherit the permissions of their owning user. There is no per-key scoping. ## External AI access MCP requests use the same authentication and authorization path. When an agent acts through MCP: - It calls `/api/v1/mcp` with an API key or an OAuth Bearer token. - The server resolves user and company from the credential. - Every tool call runs inside a tenant-scoped context. - Validation errors return structured messages with remediation hints and do not leak internal schema details. The MCP surface adds input guardrails to keep tool calls safe: - Passing `null` on a relation array is rejected before it reaches the database. - Update tools targeting the wrong record type are rejected with a pointer to the correct tool. - Destructive tools require an explicit list of record ids. There is no delete-by-filter shortcut. See [MCP](/docs/mcp#tool-catalog) for the full tool catalog. ## Data at rest - Postgres encryption depends on your provider. On the managed cloud, data lives in an EU region with disk-level encryption at rest. - Secrets in `.env` are never logged. The logger redacts anything that looks like a key, token, or password. - Webhook secrets are stored in Postgres because they must be retrievable to sign outgoing requests. To keep them elsewhere, self-host with your own secret manager. ## Data in transit - HTTPS everywhere on the managed cloud. - On self-host, you provide the TLS. See [self-hosting](/docs/self-hosting) for the Caddy example. - Webhook deliveries only go to HTTPS URLs. HTTP is rejected at the schema layer. ## Audit logging Every write is logged with user, action, entity, and before and after values. It is queryable from the UI and exportable as JSON. Audit logging is included on every cloud plan. The self-hosted community edition does not include audit logging. ## Reporting vulnerabilities Please disclose responsibly to `security@customermates.com`. The PGP key is in the repository. We aim to acknowledge within 24 hours. ## Next - [Self-hosting](/docs/self-hosting): run Customermates yourself. - [API keys](/docs/api-keys): key hygiene rules. - [MCP](/docs/mcp): how the AI surface is shaped. --- # Assistant Source: https://customermates.com/en/docs/app-assistant Customermates Cloud contains the cloud-only **Mate** in-app assistant capability. Whether Mate is live in a hosted environment depends on that environment's current configuration. This page documents the panel when Mate is enabled, for humans and for AI agents that drive the UI. Self-hosted deployments retain full MCP access for external AI clients with your own provider instead. ## Purpose When Mate is enabled, it appears in the sidebar as **Ask AI** and opens with Cmd/Ctrl+J from anywhere in the app. Ask it questions about your workspace or the product, tell it to change data, have it operate the interface, or let it walk you through the app. It answers in your interface language. Mate remembers whether each user left the panel open or closed and restores that choice after a refresh. On a completely empty page, it opens automatically until the user explicitly closes it. Demo embeds can choose their initial presentation independently by adding the exact query parameter `agentChat=open` or `agentChat=closed` to the iframe URL. That parameter applies only to the current embedded page and does not overwrite the user's saved choice. ## What runs immediately and what asks first Ordinary CRM work runs as soon as you ask: creating and updating records, notes, record links, message drafts, inbox triage, workspace settings, custom fields, widget and webhook setup, team member role or status changes, and generating secure account-connection links. Mate says what it is about to change and reports exactly what changed. To connect a provider account, you open the generated link and complete its QR code or sign-in flow yourself; creating the link does not mean the account is connected yet. Mate makes the complete MCP tool inventory discoverable on every turn: all five record types and their create/update paths, workspace setup, custom fields, widgets, webhooks, team administration, messaging, connected social profiles and posts, Sales Navigator searches and lists, documentation, and the connector-compatible search and fetch tools. It loads only the definitions needed for the current task to keep token use and cost down. The current page and the wording of an earlier message never remove a capability. Discovery does not grant access: every call still enforces your current workspace role, tenant, plan entitlements, connected-account prerequisites, and the approvals below. The hosted assistant keeps the same `request_support` capability but routes it through its transcript-aware confirmation and email flow. Only actions that cannot be taken back or that leave your workspace stop for your approval, every single time: | Always asks | Why | | -------------------------------------------------- | -------------------------------------------- | | Deleting records | Hard delete, cannot be undone | | Deleting a custom field, widget, or webhook | Destroys configuration and stored values | | Discarding a message draft | Removes written content | | Sending an email or chat message | Reaches a real person outside the workspace | | Sending a team invitation | Sends a real invitation email | | Resending a webhook delivery | Sends a new external webhook request | | Changing a social relation or Sales Navigator list | Changes data on a connected external account | | Sending a support request | Emails the Customermates team | An approval card names the action and its consequence. Decline it or let it time out and nothing has changed. There is no standing permission: each of these actions asks again next time. ## Operating the interface Mate navigates by stable interface ids and activates a narrow allowlist of reversible display controls in the DOM. It can open display options and switch a list between table, cards, and kanban, with success reported only after the selected or expanded DOM state is visible. It can highlight the stable search and filter controls, or open display options for sorting and grouping, but it does not type into them or bind directly to page stores. Kanban needs a single-select field to group by; if the layout control is disabled, Mate explains that prerequisite. When you ask Mate to change data, it uses the matching MCP-backed tool directly and tells you what changed. When you would rather enter or refine something in the interface, it opens the right control and points at what you need, and **you** type, select, and save. Mate never sends arbitrary selectors or clicks save, send, or delete controls, so your own unsaved edits are never overwritten. ## Guided tours Ask for a tour of anything: a single page, a workflow, or the whole app. Mate composes each tour for you on the spot from what you asked, navigates between pages as the tour progresses, and explains each stop in your language. Close the overlay and the tour ends. ## Getting started on an empty workspace Ask Mate to set up your workspace and it uses the same complete tool catalog it has for any other request: terminology, settings, custom fields, linked records, team access, connected-account links, webhooks, and dashboard widgets. It asks only for decisions that materially change the result; otherwise it can proceed from the goal and constraints you already gave. It lists what it made, so you can adjust anything or ask it to delete records again, which asks for your approval like every delete. Longer requests continue automatically through a bounded sequence of steps. Mate keeps the original request, carries forward a compact record of completed work, retains the latest exact tool results, and re-reads workspace state when it needs details that were compacted. This makes longer setup and audit tasks finish without a manual “continue” in the normal case while retaining hard cost, context, output, error, and runtime limits. If a hard limit is reached, completed changes remain in place, the activity list shows what actually ran, and the next request re-reads the workspace before continuing. ## Credits Hosted AI processing uses credits from your plan's monthly allowance per active user. The meter above the composer shows the share used and when it resets; paid allowances reset monthly, and unused credits do not roll over. A successful simple request often settles at one credit and multi-step work costs more. Before a request starts, Mate must be able to reserve a conservative multi-step safety envelope; if too few credits remain for that envelope, new requests pause even though the meter can still show a small remainder. Unused reserved credits are released after complete provider usage is measured. External MCP clients use your own AI provider and never consume these credits. ## Chat history Chats are titled automatically. The history view lists active and archived chats. Archiving is reversible; deleting a chat permanently removes its messages and approvals while CRM records and billing entries remain. ## Support Tell Mate when something does not work out and it offers to email a support request. After your confirmation, the recent conversation is included in an email to the Customermates team. Mate confirms only after the email provider accepts the message. The team replies to the email address on your account, not in the chat. If sending fails, Mate does not claim that the team was notified. ## Key actions | Action | Where | Anchor id | What happens | | ------------------ | ------------------ | ------------------------------------------------------- | ---------------------------------------- | | Open the assistant | Sidebar, above Add | `#nav-assistant` | Opens the panel, same as Cmd/Ctrl+J | | The panel itself | Floating card | `#agent-panel-dialog` | Non-modal dialog over the page | | Write to Mate | Panel footer | `#agent-composer` | Enter sends, Shift+Enter breaks the line | | Credit meter | Above the composer | `#agent-usage` | Shows usage and the reset date | | Suggested prompts | Fresh chat | `#agent-suggestions` | Prefills the composer, does not send | | Back from history | History view | `#agent-history-back` | Returns to the current chat | | Archived chats | History view | `#agent-archive-summary` | Expands the archived list | | Load more chats | History view | `#agent-load-more-active` / `#agent-load-more-archived` | Next page of chats | ## Availability Every cloud plan includes the Mate entitlement. Whether the cloud-only capability is live depends on the hosted environment's current configuration. When enabled, Mate supports every interface language (English, German, Spanish, French, Italian). Self-hosted deployments do not include the hosted assistant; they connect external AI clients through the [MCP server](/docs/mcp) instead. --- # Dashboard Widgets and the Activity Timeline Source: https://customermates.com/en/docs/app-dashboard Customermates is an open-source, AI-native CRM. This page documents the **Dashboard** screen of the web app for humans and for AI agents that drive the UI. ## Purpose The Dashboard is the landing screen after sign-in. It shows configurable widgets over your CRM data: pipelines, tasks, recent activity, whatever you set up. From here you jump off to every other screen in the app. ## Widget types Use **Add widget** and choose **Chart** for aggregated CRM data or **Activity timeline** for a newest-first history feed. An activity timeline keeps its filters on that widget. Different activity widgets can therefore show different views without changing each other or the timeline on a record page. ### Sources and filters The **Changes** category contains audit-log changes. **Messages** contains Inbox messages. **Activities** contains connected-account activity, such as accepted LinkedIn connections, and calendar events. All accessible records are the implicit baseline. Filter an activity timeline by category, provider, connected account, conversation, or related contacts, organizations, deals, services, and tasks. Each field can be configured once. Relationship filters use the same operators as record lists: **in** and **not in** accept one or more records, while **has some** and **has none** need no value. Different filter fields are AND-combined, and multiple values within one **in** filter are OR-combined. Unlinked entries fail positive relationship filters and pass negative ones. Filters become persistent when you save the widget. ### Reading the timeline Entries are shown newest first. Use **Load older** for the next bounded page. A row can show one primary record, up to three related records, and a remaining-record count. Permitted record links open the relevant record, and message details can open the permitted Inbox thread. Rows without a CRM association remain visible without a record link. Message previews and details use the same safe rendering as record timelines. ### Permissions | Access | Visible content | | ------------------------------------------------------------- | --------------------------------------------------------------------- | | Audit log only | Permitted changes | | Inbox messages only, with the cloud Pro messaging entitlement | Permitted messages, connected-account activities, and calendar events | | Both permissions, with the cloud Pro messaging entitlement | One merged timeline containing all permitted sources | | Neither | A non-sensitive unavailable state | Audit-log changes are available independently of the messaging entitlement. Messages, connected-account activities, and calendar events require Inbox access and, in cloud mode, the Pro messaging entitlement. A missing connected messaging account removes messaging-based sources but does not hide permitted audit changes. Deleted or newly inaccessible filter values stop matching, are shown as **Unavailable value** for safe removal, and never expose their raw ids. ## URL Route: `/dashboard`. All app routes are locale-prefixed, for example `https://customermates.com/en/dashboard`. ## Key actions | Action | Where | Anchor id | What happens | |--------|-------|-----------|--------------| | Go to Dashboard | App sidebar | `#nav-dashboard` | Opens `/dashboard` | | Add widget | Page header | `#dashboard-add-widget` | Opens the widget modal | | Save / reset widget | Widget modal footer | `#widget-modal-save` / `#widget-modal-reset` | Creates or updates the widget (reset appears with unsaved changes) | | Global search | Top of sidebar | `#nav-search` | Opens the search modal (also Cmd/Ctrl+K) | | Create a record | Top of sidebar | `#nav-add` | Opens the record type picker | | Search anything | Search modal | `#global-search-input` | Jump to any contact, organization, deal, service, or task | | Open documentation | Sidebar footer | `#nav-documentation` | Opens `/docs` | | Send feedback | Sidebar footer | `#nav-feedback` | Opens the feedback dialog | ## The widget editor The widget modal is organized in three tabs: `#widget-tab-data` chooses the source (record type or activity timeline) and the aggregation, `#widget-tab-filters` narrows what counts (`#widget-entity-filters-heading` marks the record filter block), and `#widget-tab-appearance` sets the display type, axes, and colors. A live preview under `#widget-preview-heading` reflects every change before you save, and `#widget-template-heading` offers ready-made starting points. Save or discard with `#widget-modal-save` / `#widget-modal-reset`. ## Global search `#nav-search` (or Cmd/Ctrl+K anywhere) opens the search modal; `#global-search-input` takes the query. It matches contacts, organizations, deals, services, and tasks by their names and jumps straight to the record you pick. It searches records, not messages; the Inbox has its own search. ## First run New accounts land in the onboarding wizard at `/onboarding/wizard` before they see the Dashboard. It walks through profile, company, entities, demo data, AI setup, and team invites. Navigate it with the anchor ids `#onboarding-next` and `#onboarding-back`. ## Tips for agents - Locate controls via the stable ids above, not by visible text. CSS selector example: `#dashboard-add-widget`. - Rows never carry ids. Open records via their URL or the drawer deep-link `?open=:` (entity pages only). Stack multiple entries with commas, for example `?open=contact:abc,deal:xyz`. - Prefer [MCP](/docs/mcp) tools over clicking when a tool exists. Reading and writing CRM data is faster and safer through tools than through the UI. ## Related - [Quickstart](/docs/quickstart) - [App guide, Records](/docs/app-records) --- # Customermates CRM Inbox: Threads and Replies Source: https://customermates.com/en/docs/app-inbox This page documents the managed-cloud **Inbox** screen for humans and for authorized clients that drive the UI. ## Purpose The Inbox is unified messaging. Email, LinkedIn, WhatsApp, Instagram, and Telegram threads from all connected accounts appear in one list. From here you reply on the thread's original channel, set the thread state, and link participants to CRM contacts. It requires at least one connected account. See [App guide, Profile](/docs/app-profile). ## URL The route is `/inbox`. All app routes are locale-prefixed, for example `https://customermates.com/en/inbox`. ## Key actions | Action | Where | Anchor id | What happens | | --------------------------- | ----------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------- | | Go to Inbox | App sidebar | `#nav-inbox` | Opens /inbox | | Open thread | Row (button) inside the thread list | `#inbox-thread-list` | The list container has the id; thread rows inside it are buttons | | Reply | Composer at the bottom of a thread | `#inbox-reply-input` / `#inbox-reply-send` | Type, then send on the thread channel | | Set thread state | Thread header state select | `#inbox-thread-state` | Choose unread, open, closed, or spam | | Share thread with the team | Thread settings panel | `#thread-shared` | Expose this individual thread to CRM teammates on a messaging-enabled plan | | Link participant to contact | Thread settings panel | none | Open thread settings, then link per participant | ## Thread settings Open a thread's settings to manage everything that belongs to the conversation rather than a single message. Each participant can be linked to a CRM contact, which makes the thread and its messages appear on that contact's timeline; unlinked participants stay visible in the thread only. On any messaging-enabled plan, `#thread-shared` exposes that individual thread to CRM teammates. Sharing the complete connected account is a separate entitlement available on Business and Enterprise. ## Drafts and attachments Replies are drafted per thread and survive navigation: leave the thread and come back, and your text is still there. Discarding a draft removes the written content and cannot be undone. Supported incoming media opens inline, ordinary files download, and unsupported items are shown as unsupported. LinkedIn-post attachments open at their external source. Outgoing attachments go through the reply composer on channels that support them. ## Thread states A thread is **unread** until someone opens it, **open** while it needs attention, **closed** when it is done, and **spam** to keep a sender out of the way. Setting a state never sends anything to the other side; it only organizes your inbox. The hosted Mate capability is release-gated and is not required for this workflow. ## Tips for agents - Locate controls via the stable ids above, for example with the CSS selector `#inbox-reply-send`. - Rows never carry ids. Open records via their URL, or (on entity pages) via the drawer deep-link `?open=:{id}`. In the Inbox, thread rows are buttons inside `#inbox-thread-list`. - Prefer [MCP](/docs/mcp) tools over clicking when a tool exists. - Starting a fresh conversation (new email, new chat) is available through the MCP tools `send_email` and `send_chat_message`. Prefer those over the UI. In the app, the send icon on a contact's channel opens a compose popover for a quick message. ## Related - [App guide, Profile](/docs/app-profile): connect the email, LinkedIn, WhatsApp, Instagram, or Telegram accounts that feed the Inbox. - [Rate limits](/docs/messaging-rate-limits): the per-provider budgets that govern reads and sends. --- # CRM Records: Contacts, Deals and Custom Columns Source: https://customermates.com/en/docs/app-records Customermates has five record types: Contacts, Organizations, Deals, Services, and Tasks. They all use the same screen layout, so this page documents them together for people and for AI agents that drive the UI. ## The shared layout Every record type has the same three surfaces: - **List** at `/{type}` (for example `/contacts`), locale-prefixed like `https://customermates.com/en/contacts`. Search, filter, sort, and bulk actions live here. - **Drawer**: append `?open={entityType}:{id}` to the list URL (for example `/contacts?open=contact:{id}`) to edit a record over the list. - **Detail page** at `/{type}/{id}` (for example `/contacts/42`) for the full record, custom fields, and delete. It opens with an overview of the fields that matter most, and every field carries a pin control that adds it to or removes it from that overview. Pinning is per person, so two colleagues can read the same record through different summaries. Rows never carry a DOM id. Open a record by its URL or the drawer deep-link, never by clicking a row you located by id. ## Lists and toolbars Each record type has its own route and its own toolbar ids, prefixed with the type. The four toolbar controls behave the same everywhere: **Add** opens the create drawer, **Search** live-filters the list, **Filter** opens the filter popover ([filter syntax](/docs/filter-syntax)), and **Display options** controls columns, sorting, and view mode. | Record type | Route | entityType | Sidebar | Toolbar: add, search, filter, display options | |---|---|---|---|---| | Contacts | `/contacts` | `contact` | `#nav-contacts` | `#contacts-add`, `#contacts-search`, `#contacts-filter`, `#contacts-display-options` | | Organizations | `/organizations` | `organization` | `#nav-organizations` | `#organizations-add`, `#organizations-search`, `#organizations-filter`, `#organizations-display-options` | | Deals | `/deals` | `deal` | `#nav-deals` | `#deals-add`, `#deals-search`, `#deals-filter`, `#deals-display-options` | | Services | `/services` | `service` | `#nav-services` | `#services-add`, `#services-search`, `#services-filter`, `#services-display-options` | | Tasks | `/tasks` | `task` | `#nav-tasks` | `#tasks-add`, `#tasks-search`, `#tasks-filter`, `#tasks-display-options` | Inside each display-options popover, the stable layout controls are: - Contacts: `#contacts-layout-table`, `#contacts-layout-cards`, `#contacts-layout-kanban` - Organizations: `#organizations-layout-table`, `#organizations-layout-cards`, `#organizations-layout-kanban` - Deals: `#deals-layout-table`, `#deals-layout-cards`, `#deals-layout-kanban` - Services: `#services-layout-table`, `#services-layout-cards`, `#services-layout-kanban` - Tasks: `#tasks-layout-table`, `#tasks-layout-cards`, `#tasks-layout-kanban` ## Drawer and detail actions These ids are shared by every record type. | Action | Where | Anchor id | What happens | |---|---|---|---| | Save (drawer) | Drawer footer | `#drawer-save` | Persists edits made in the drawer | | Save / Reset (detail) | Detail topbar | `#entity-save` / `#entity-reset` | Persists or discards edits | | Edit custom fields | Detail topbar | `#entity-edit-fields` | Toggles editing of the custom fields, not the built-in ones | | Add custom field | Custom fields section, while editing fields or when the section is empty | `#entity-add-custom-field` | Creates a custom column | | Delete | Detail topbar | `#entity-delete` | Opens the confirmation modal | | Confirm / cancel delete | Confirmation modal | `#confirm-delete` / `#confirm-delete-cancel` | Irreversible delete or abort | | Bulk delete | Selection bar after checkbox-select | `#mass-delete` | Deletes every selected row | Notes are a labeled field inside the form. Edit them there, then save. ## Each record type in brief **Contacts** are people, with channel identifiers (email address, LinkedIn, WhatsApp, Instagram, Telegram) that connect them to Inbox threads and power the send icons on their detail page. **Organizations** group contacts and carry the company-level relations that deals and services hang off. **Deals** track opportunities and carry linked services with per-deal quantities; the deal's value follows from them. **Services** are what you sell, linkable to deals and organizations. **Tasks** carry due dates and link to any of the other types. All five share the same machinery: the same list views, the same drawer and detail page, the same custom columns, the same filters, and the same relation editing. Anything this page says about one type holds for the others. ## Switching views Every list offers a table, a card grid, and a board. `#{type}-display-options` opens the switcher; the board additionally needs a `singleSelect` column to group by, and the group-by picker lists exactly those. Sorting, search, and filters live in the same toolbar (`#{type}-search`, `#{type}-filter`). View choices persist per user and survive reloads. Mate can open the display options and activate the stable table, cards, or kanban target. Dynamic grouping, sorting, search, and filter values remain under the user's direct control; Mate can highlight search and filter, or open display options for sorting and grouping. ## Custom columns and the board view Any record type can carry user-defined custom columns. There are no fixed workflow fields baked into a type, so there is no product-level deal stage or task status. `get_record_schema` returns whatever columns a workspace has configured for each type. A `singleSelect` custom column can drive a board (kanban) view. `#{type}-display-options` switches the list between a table and a board grouped by a chosen `singleSelect` column. On the board, drag a record between columns to change that column's value; the equivalent in the drawer or detail form is to set the field and save. This applies to every record type that has at least one `singleSelect` column, not only to deals. The demo workspace ships example columns to illustrate the pattern, for instance a "Status" column on deals and "Status" and "Priority" columns on tasks. These are seeded examples, not fixed behavior. Rename them, change their options, add more, or remove them per workspace. Records link to other records through labeled relation fields inside the form. Pick the records, then save. ## Tips for agents - Locate controls by the stable ids above, for example `document.querySelector('#deals-add')`. Ids are identical across locales and viewports; visible labels are not. - To open a record, build the URL yourself: `/{type}/{id}` for the full page, or `/{type}?open={entityType}:{id}` for the drawer. Do not try to click a row. - Prefer [MCP](/docs/mcp) tools over driving the UI when a tool exists. Creating or updating a record, or changing a `singleSelect` column value, is one tool call instead of a click sequence. ## Related - [Core concepts](/docs/concepts): entities, relationships, custom columns. - [MCP tool catalog](/docs/mcp#tool-catalog): every tool your AI can call. - [App guide, Dashboard](/docs/app-dashboard) --- # User Profile Settings in the Customermates CRM Source: https://customermates.com/en/docs/app-profile Customermates is an open-source, AI-native CRM. This page documents the **Profile** screen of the web app for humans and for AI agents that drive the UI. ## Purpose Profile holds your personal workspace settings: everything scoped to you as a user rather than to the company. Three tabs cover your settings (one form for identity and preferences: name, country, avatar, theme, language), API keys for MCP and REST clients, and the messaging accounts that feed the Inbox. Changes here affect only your own account. ## URL `/profile/settings` URLs are locale-prefixed. For example `https://customermates.com/en/profile/settings`. Each tab has its own route: `/profile/settings`, `/profile/api-keys`, `/profile/connected-accounts`. ## Key actions | Action | Where | Anchor id | What happens | |---|---|---|---| | Go to Profile | App sidebar | `#nav-profile` | Opens /profile/settings | | Settings tab | Sidebar sub-item | `#nav-profile-settings` | Identity (name, country, avatar) and preferences (theme, language) in one form | | Save / reset settings | Form footer | `#profile-settings-save` / `#profile-settings-reset` | Persists or discards (reset appears with unsaved changes) | | API keys tab | Sidebar sub-item | `#nav-profile-api-keys` | Manage keys for MCP/REST clients | | Add an API key | API keys card | `#profile-api-keys-generate` | Opens a wizard for a standard key or a guided AI connection | | Create a standard key | Add-key wizard | `#api-key-save` | Opens after Standard API key; the key is shown once, so copy it immediately | | Connect an AI client | Add-key wizard | none | Choose Claude, ChatGPT, Codex, Cursor, or Gemini for client-specific setup | | Revoke a key | Key row | none | Row button labeled Revoke | | Connected accounts tab | Sidebar sub-item | `#nav-profile-connected-accounts` | Messaging accounts for the Inbox | | Connect an account | Card header | `#profile-connected-accounts-connect` | Opens the hosted-auth window (email, LinkedIn, WhatsApp, Instagram, Telegram) | | Connect an account from an empty profile | Centered empty state | `#profile-connected-accounts-connect-empty` | Opens the same hosted-auth window when no account exists | | Manage account / folders | Account row click | none | Opens the account modal (folder selection, resync) | ## Tips for agents - Locate controls via the stable ids above. For example, the CSS selector `#profile-api-keys-generate` finds the generate button. The ids are stable; visible labels and layout are not. - Rows never carry ids. Open records via their URL, or via the drawer deep-link `?open=:{id}` on entity pages. The Profile tabs are plain routes, so navigate by tab URL and act on rows via their visible buttons (e.g. **Revoke**) or by clicking the row. - Prefer MCP tools over clicking when a tool exists. See [MCP](/docs/mcp). Driving the UI is for the few actions with no tool, like connecting a messaging account. - The Add modal first offers a standard named/expiring key and five guided AI connections. Claude and ChatGPT can use their custom-connector path; local Claude, Codex, Cursor, and Gemini create a key and show only that client's setup snippet. - A generated API key is shown exactly once, whether created through `#api-key-save` or a quick connection. Copy the displayed key or snippet in the same step; there is no way to display the plaintext key again. - Connecting an account opens a separate hosted-auth window: expect a popup, not in-page navigation. ## Related - [API keys](/docs/api-keys): create, rotate, and revoke keys, plus hygiene rules. - [App guide, Inbox](/docs/app-inbox): the screen your connected accounts feed. - [CLI & editors](/docs/connect-cli): where a generated key goes for Claude Code, Codex, and Cursor. --- # Global Search Source: https://customermates.com/en/docs/app-search Global search is a modal, not a page: it opens from anywhere in the app with **Cmd/Ctrl+K** or by activating `#nav-search` in the sidebar. This page documents it for people and for AI agents that drive the UI. ## What it searches The modal searches **contacts, organizations, deals, and services** by name and related fields, and shows a **Recently opened** group before you type. Tasks are not part of global search: find them on `/tasks` with its list search and filters, or with the `list_records` MCP tool. Selecting a result opens the record's drawer over the current page, so the context you were in stays underneath. The deep-link equivalent is the `?open={entityType}:{id}` pattern documented in [CRM Records](/docs/app-records). ## Stable ids | Control | Id | |---|---| | Sidebar trigger | `#nav-search` | | Search input inside the modal | `#global-search-input` | For an agent, typing happens in `#global-search-input` after opening the modal; results are regular options selected by their visible name. For filtered, sorted, or counted lookups prefer the list pages or `list_records`, which return totals. ## Frequently asked questions Tasks are not part of global search. Find them on the tasks list with its search and filters, or through the `list_records` MCP tool. Cmd+K on macOS, Ctrl+K elsewhere, from anywhere in the app. The sidebar trigger `#nav-search` opens the same modal. In the record's drawer over the current page, so your context stays underneath. The same drawer is reachable directly via the `?open={entityType}:{id}` deep link documented in [CRM Records](/docs/app-records). --- # Onboarding Wizard Source: https://customermates.com/en/docs/app-onboarding A fresh workspace opens the onboarding wizard at `/onboarding/wizard` right after sign-up. It runs once; finishing it lands on the dashboard, where the assistant panel opens on its own. This page documents the wizard for people and for AI agents that drive the UI. ## The three steps Your name and country. The fields are ordinary form inputs addressed by their visible labels. Invite teammates by email, or copy the shareable link of the form `/invitation/{token}`. Anyone who registers through that link joins the workspace as **Waiting for Approval** until an admin assigns a role and activates them on the [Company page](/docs/app-company). This step can be skipped and repeated later from the Company page. Connect an external AI client (Claude, OpenAI, Cursor, or Gemini) to the workspace's [MCP endpoint](/docs/mcp). The flow creates an API key for the chosen client. This step can be skipped; the same flow stays available from the [Profile page](/docs/app-profile), and the in-app [Assistant](/docs/app-assistant) works without it. ## Stable ids | Control | Id | |---|---| | Primary action (next step, or finish on the last step) | `#onboarding-back` and `#onboarding-next` | `#onboarding-next` advances and, on the last step, completes the wizard; `#onboarding-back` returns to the previous step. Everything inside a step is a regular form: drive it by label, then activate `#onboarding-next`. ## Frequently asked questions The invite and AI steps are skippable. Invitations can be sent later from the [Company page](/docs/app-company), and the AI-client flow stays available on the [Profile page](/docs/app-profile). The profile step is required. They join the workspace as Waiting for Approval. An admin assigns a role and activates them on the Company page; until then they cannot work in the workspace. No. The built-in [Assistant](/docs/app-assistant) works without any external client. The AI step connects external tools such as Claude or Cursor to your workspace through [MCP](/docs/mcp). --- # Company Settings: Roles, Audit Log, Team Access Source: https://customermates.com/en/docs/app-company Customermates is an open-source CRM with a built-in MCP server. This page documents the **Company** screen of the web app for humans and for AI agents that drive the UI. ## Purpose The Company screen is workspace-wide administration. It holds the subscription (Cloud only), the team and its permissions, outbound webhooks with their delivery history, and the audit trail. Everything that affects the whole workspace, not a single record, lives here. ## URL All app routes are locale-prefixed (example: https://customermates.com/en/company/members). Each tab has its own route: `/company/subscription` (Cloud only), `/company/settings`, `/company/members`, `/company/roles`, `/company/webhooks`, `/company/webhook-deliveries`, and `/company/audit-logs`. ## Key actions | Action | Where | Anchor id | What happens | |---|---|---|---| | Go to Company | App sidebar | `#nav-company` | Opens /company/subscription (Cloud) or /company/settings (self-hosted) | | Subscription tab | Sidebar sub-item | `#nav-company-subscription` | Plan, trial status, and the plan picker (Cloud only) | | Settings tab | Sidebar sub-item | `#nav-company-settings` | Workspace currency for all money values | | Save / reset settings | Top bar | `#company-settings-save` / `#company-settings-reset` | Persists or discards | | Members tab | Sidebar sub-item | `#nav-company-members` | Team management | | Invite member | Members toolbar | `#company-members-add` | Opens the invite modal | | Send invite | Invite modal footer | `#member-modal-save` / `#member-modal-reset` | Sends the invitation | | Search / filter members | Members toolbar | `#company-members-search` / `#company-members-filter` / `#company-members-display-options` | List controls | | Roles tab | Sidebar sub-item | `#nav-company-roles` | Permission sets | | Add role | Roles toolbar | `#company-roles-add` | Opens the role editor | | Roles list controls | Roles toolbar | `#company-roles-search` / `#company-roles-filter` / `#company-roles-display-options` | List controls | | Webhooks tab | Sidebar sub-item | `#nav-company-webhooks` | Outbound event subscriptions | | Create webhook | Webhooks toolbar | `#company-webhooks-add` | Opens the webhook modal | | Save webhook | Webhook modal footer | `#webhook-modal-save` / `#webhook-modal-reset` | Subscribes the URL to events | | Webhooks list controls | Webhooks toolbar | `#company-webhooks-search` / `#company-webhooks-filter` / `#company-webhooks-display-options` | List controls | | Deliveries tab | Sidebar sub-item | `#nav-company-webhook-deliveries` | Per-delivery status + payload; resend from a delivery row | | Deliveries list controls | Deliveries toolbar | `#company-webhook-deliveries-search` / `#company-webhook-deliveries-filter` / `#company-webhook-deliveries-display-options` | List controls | | Audit logs tab | Sidebar sub-item | `#nav-company-audit-logs` | Who changed what, when | | Audit list controls | Audit toolbar | `#company-audit-logs-search` / `#company-audit-logs-filter` / `#company-audit-logs-display-options` | List controls | The display-options popovers expose these stable layout controls: - Members: `#company-members-layout-table`, `#company-members-layout-cards`, `#company-members-layout-kanban` - Roles: `#company-roles-layout-table`, `#company-roles-layout-cards`, `#company-roles-layout-kanban` - Webhooks: `#company-webhooks-layout-table`, `#company-webhooks-layout-cards`, `#company-webhooks-layout-kanban` - Deliveries: `#company-webhook-deliveries-layout-table`, `#company-webhook-deliveries-layout-cards`, `#company-webhook-deliveries-layout-kanban` - Audit logs: `#company-audit-logs-layout-table`, `#company-audit-logs-layout-cards`, `#company-audit-logs-layout-kanban` ## Tips for agents - Locate controls via the stable ids above, not by visible text. Text changes with the locale, ids do not. Example CSS selector: `#company-webhooks-add`. - Rows never carry ids. Navigate to a tab via its URL (for example `/company/webhooks`) and open a row by clicking it. Edits happen in modals, not in a drawer. - Prefer [MCP](/docs/mcp) tools over clicking when a tool exists: managing records via MCP is faster and more reliable than driving the UI. ## Related - [Webhooks](/docs/webhooks) - [App guide, Profile](/docs/app-profile) --- # Create and Rotate API Keys for MCP and REST Source: https://customermates.com/en/docs/api-keys API keys authenticate Model Context Protocol (MCP) and REST clients against your Customermates CRM. Create one key per client (Claude, ChatGPT, Zapier, etc.), store it in an env var, rotate when a device is compromised or when a teammate leaves. ## Create a key **Profile → API Keys → New key**. Give it a name that identifies where it will be used (e.g. `Claude Desktop personal laptop`). The 64-character key is shown **once**. Copy it immediately. ## Use a key Send it in the `x-api-key` header on every request: ```bash curl -H "x-api-key: $CUSTOMERMATES_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ https://customermates.com/api/v1/mcp \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` The `Accept: application/json, text/event-stream` header is required by the MCP streamable HTTP transport; without it the endpoint returns `406 Not Acceptable`. For MCP clients, the key goes into the MCP config: - [Claude Desktop](/docs/connect-cli#claude-desktop-config-file) - [ChatGPT](/docs/connect-custom-connector#chatgpt) - [Cursor](/docs/connect-cli#cursor) ## Hygiene - **One key per client.** Makes audit logs readable and lets you revoke a single client without breaking the others. - **Store in an env var**, not in a config file. `~/.zshrc`, 1Password, Bitwarden CLI, or your OS keychain. - **Rotate when**: - A device that held the key is lost or compromised. - A teammate with access leaves. - You suspect the key leaked (check your git history, CI logs, screenshots). ## Rotate Profile → API Keys → click the key → **Rotate**. A new key is generated and the old one is invalidated immediately. Update every client that had the old key. ## Revoke Same place → **Revoke**. Invalidates the key without generating a new one. Use when you don't plan to replace it. ## Key format Customermates keys are 64 characters, base62 (`a-z A-Z 0-9`), generated by the API key plugin. The full key is shown only at creation time and is never retrievable afterward. ## Permissions Every key inherits the permissions of the user it belongs to. If a user is demoted from Admin to Member, their keys lose admin capabilities on the next call. There is no separate permission scoping per key today. ## Next - [Quickstart](/docs/quickstart): first end-to-end run. - [MCP overview](/docs/mcp): what keys unlock. --- # Filter Syntax: Operators for MCP and REST Queries Source: https://customermates.com/en/docs/filter-syntax Customermates filters (used by the Model Context Protocol (MCP) tools, the REST API, and the app UI) are arrays of field-operator-value rules. Sixteen operators cover equality, comparison, set membership, range, recency, null checks, and relationship membership. ## Where filters apply - The `list_records` MCP tool and the messaging filters on `get_messaging_threads` and `get_activities`. - `entityFilters` and `dealFilters` on widgets. - `timelineFilters` on activity widgets. Activity filters allow at most one rule per field; put OR alternatives in one `in` value array. - Saved views in the UI (converted to the same shape under the hood). ## The shape ```json [ { "field": "firstName", "operator": "contains", "value": "acme" }, { "field": "createdAt", "operator": "gte", "value": "2024-01-01" } ] ``` Rules are AND-combined. For OR logic, run two queries and merge client-side, or use the `in` operator when comparing against a list. ## Operators | Operator | Expects | Works on | Example value | |---|---|---|---| | `equals` | single | scalars, ids | `"active"` | | `contains` | single | strings | `"acme"` | | `gt` | single | numbers, dates | `"2024-01-01"` | | `gte` | single | numbers, dates | `100` | | `lt` | single | numbers, dates | `"2024-12-31"` | | `lte` | single | numbers, dates | `"2024-12-31"` | | `in` | array | any | `["id1", "id2"]` | | `notIn` | array | any | `["id1"]` | | `between` | array of 2 | numbers, dates | `["2024-01-01", "2024-12-31"]` | | `inLastDays` | single (integer) | dates | `30` | | `isNull` | no value | any | (none) | | `isNotNull` | no value | any | (none) | | `hasNone` | no value | relationship arrays | (none) | | `hasSome` | no value | relationship arrays | (none) | | `hasUnset` | no value | messaging-thread participants | (none) | | `allSet` | no value | messaging-thread participants | (none) | ### Date operators Date fields such as `createdAt` and `updatedAt` accept `gt`, `gte`, `lt`, `lte`, `between`, and `inLastDays`. Use `inLastDays` with an integer number of days for a rolling recency window, for example `{ "field": "createdAt", "operator": "inLastDays", "value": 7 }` for records created in the last week. ### Relationship and participant operators Relationship arrays (`organizationIds`, `dealIds`, `userIds`, `contactIds`, `serviceIds`, `taskIds`) accept `in`, `notIn`, `hasNone`, and `hasSome`. The participants link-status field on `get_messaging_threads` uses `hasUnset` and `allSet` to filter threads by whether their participants are linked to records. ## Field names The `field` value is whatever `get_record_schema` returns under `filterableFields` for that entity. It includes: - Default scalar fields (e.g. `createdAt`, `updatedAt`). - Relationship arrays (`organizationIds`, `dealIds`, `userIds`, `contactIds`). Pair these with `in`, `notIn`, `hasNone`, `hasSome`. - Custom column ids. Use the column's UUID as the field. Always call `get_record_schema` first if you're not sure what's filterable. The error when a field isn't recognized lists every available field with its allowed operators. ## Examples Contacts in any of three organizations: ```json { "field": "organizationIds", "operator": "in", "value": ["org_1","org_2","org_3"] } ``` Deals created in 2024 whose custom singleSelect column (identified by its UUID) equals a chosen option value: ```json [ { "field": "createdAt", "operator": "between", "value": ["2024-01-01","2024-12-31"] }, { "field": "col_uuid", "operator": "equals", "value": "option_value" } ] ``` Contacts without any organization linked: ```json { "field": "organizationIds", "operator": "hasNone" } ``` Records created in the last 30 days: ```json { "field": "createdAt", "operator": "inLastDays", "value": 30 } ``` ## Free-text vs filters `list_records` also accepts `searchTerm`, which runs a free-text search against the entity's name fields (firstName + lastName for contacts, name for the rest). If you want "contacts whose name contains acme", that's `searchTerm`, not a filter rule on `firstName`. Filter rules on `firstName` specifically are not supported. ## Next - [Core concepts](/docs/concepts): field types and relationships. - [MCP tool catalog](/docs/mcp#tool-catalog): where filters appear.