> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chatsailer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CRM

> Contacts, schema, campaigns. Call describe_schema before you guess a field key.

URL: `https://mcp.chatsailer.com/mcp/crm`

Read and manage a Sailer workspace. Record tools take a `resource` argument so
they can grow without eating the client's tool budget. Today the only value is
`"contact"`. Campaigns have their own tools. Organizations, deals, and
messages are not tools yet.

Every workspace defines its own custom fields. The JSON Schema a client caches
does **not** include them — clients cache by server, not by tenant. That is
why `describe_schema` exists, and why you call it first.

<Note>
  First connection is [read-only](/mcp/auth). Write tools will not even appear
  until you reconnect with `contacts:write`.
</Note>

## Orient

### `whoami`

No arguments, no scope. Which workspace this connection is bound to, who
authorized it, `oauth` vs `api_token`, whether it reads company-wide, and the
scopes you actually have.

> Which Sailer workspace am I connected to?

A Sailer account can belong to several workspaces. The connection is bound to
exactly one, chosen at consent. No tool argument can change it.

### `describe_schema`

Scope: `contacts:read`. Optional `resource` (`"contact"`); omit it to describe
everything this server can touch.

Returns built-in and custom fields, which are required or read-only, select
options, and which keys are filterable, sortable, or expandable.

> What custom fields does this workspace have?

Use the `key` it returns inside `custom_fields` on create and update. Guessing
from another workspace — or from these docs — is how you get an unknown-field
error.

## Read contacts

Ids look like `con_8f3a…`. That prefix is the resource; a deal id sent here is
a 400, not a mysterious 404.

### `search_records`

Scope: `contacts:read`. `resource` defaults to `"contact"`.

| Argument | What it does                                                       |
| -------- | ------------------------------------------------------------------ |
| `q`      | Free text across name, email, phone                                |
| `filter` | Nested and/or condition tree                                       |
| `sort`   | Same syntax as the REST `sort` query                               |
| `expand` | Comma-separated relations, e.g. `owner`                            |
| `limit`  | Page size                                                          |
| `cursor` | Opaque. Pass `next_cursor` from the previous page back as `cursor` |

If `has_more` is true, keep paging. Do not count a page and call it the
population — that is what `count_records` is for. Cursors are opaque, same
contract as [REST pagination](/guides/pagination).

> Find contacts created in the last 7 days.

### `get_record`

Scope: `contacts:read`. `resource`, `id`, optional `expand`.

> Show me contact `con_…`

Missing, deleted, or outside what you can see: not found. Same next step in
all three cases — search again.

### `count_records`

Scope: `contacts:read`. Same `filter` / `q` as search, no paging.

> How many contacts are in this workspace?

## Write contacts

Scope on every tool here: `contacts:write`. Call `describe_schema` first.
Unknown or read-only custom-field keys are rejected, not ignored.

`first_name` and `phone` are required on create. Phone is unique per workspace
and stored as E.164.

### `create_record`

New row. Prefer `upsert_contact` if the person may already exist — a duplicate
is not automatically reversible.

> Create a contact named Ana with phone +5511999999999.

### `update_record`

Destructive: it overwrites what you send. Omit a field to leave it; send
`null` to clear it. Read first if you meant to append.

> Set Ana's email to [ana@example.com](mailto:ana@example.com).

### `upsert_contact`

Match on phone, after canonicalization — `(11) 98765-4321` and
`+55 11 98765-4321` are the same person. Default `on_match` is `update`;
`ignore` returns the existing row untouched.

The result tells you which branch ran (`created` vs `matched_existing`). The
record alone does not.

> Add or update the contact with this phone number.

## Campaigns

### `list_campaigns`

Scope: `campaigns:read`. Newest first. Optional `status`, `limit` 1–100
(default 25). Use this to get a `campaign_id`.

> List this workspace's campaigns.

### `get_campaign_analytics`

Scope: `campaigns:read`. One campaign, one metric family.

| `metric`          | What you get                        |
| ----------------- | ----------------------------------- |
| `big_numbers`     | Headline counts and rates (default) |
| `status_funnel`   | Lifecycle funnel                    |
| `daily_metrics`   | Per-day series                      |
| `error_breakdown` | Where sends failed                  |

Optional `window_start` / `window_end` (UTC). Defaults to the last 30 days.

These families are **not interchangeable**. `big_numbers` and `daily_metrics`
use a strict "answered" (a qualifying inbound, classified as a reply).
`status_funnel` uses the lifecycle status, which is usually a larger number.
Rates on `big_numbers` are 0–100, not fractions.

Every response carries `_meta.caveats`. Read them before comparing two
numbers. A difference between two families is not a change over time.

> Show me campaign performance. Read the caveats before you summarize.
