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

# Sync a CRM Into Brew Over MCP

> Pull CRM fields into Brew contacts with an MCP assistant: create custom fields, map properties, run the sync, and verify counts before you send.

Your sales team is working a deal. Your marketing email goes to that person anyway.

Fixing that means Brew needs to know what your CRM knows. If you have both connected to an MCP assistant, you can sync the fields you filter on, with no code.

This page covers a one-directional sync, CRM into Brew. It works with any MCP-connected CRM, including Attio and HubSpot.

Using Attio? See [Sync Brew and Attio Over MCP](/api-reference/mcp/attio) for its list-entry model and the engagement writeback loop.

<Warning>
  Every marketing recipient must have opted in to hear from you. Importing or syncing contact data into Brew is not permission to email it. See [What You Can Send](/get-started/what-you-can-send).
</Warning>

This syncs CRM **context** onto contacts who already opted in. It is not a way to import a prospecting list. A CRM contact from a sales conversation has not consented.

## Before You Start

You need:

* A Brew brand connected to your assistant, with opted-in contacts already in it
* A CRM connected to the same assistant, with read access to contacts
* Contacts that exist in **both** systems under the same email address

Matching is on email. A contact in one system and not the other won't sync.

## 1. Check What Brew Already Has

Some fields are native. Don't create custom fields that duplicate them.

| Native field              | Type   | Holds                  |
| ------------------------- | ------ | ---------------------- |
| `email`                   | string | Contact identifier     |
| `firstName` / `lastName`  | string | Name                   |
| `subscribed`              | bool   | Consent state          |
| `validationStatus`        | string | Deliverability verdict |
| `lastValidatedAt`         | date   | When that was checked  |
| `createdAt` / `updatedAt` | date   | Record timestamps      |
| `importId`                | string | Source import          |

Ask your assistant:

> List the custom fields in my Brew brand.

## 2. Create the Fields You're Missing

Name each field after its source so nobody has to guess where the data came from.

> Create these custom fields in Brew: crm\_lifecycle\_stage (string), crm\_owner (string), crm\_last\_contacted (date), crm\_record\_id (string).

**Brew normalizes names to camelCase.** You declare the underscored name and get the camelCase one back:

```json theme={null}
{
  "fieldName": "hubspotLifecycleStage",
  "fieldType": "string",
  "label": "Hubspot Lifecycle Stage",
  "isCore": false,
  "isFilterable": true,
  "isSortable": true,
  "isSearchable": false
}
```

The returned `fieldName` is what your audience filters use, not the name you typed.

Field types are `string`, `number`, `date`, or `bool`. Declare anything date-like as `date` so comparisons work.

## 3. Pick Which CRM Properties to Sync

Only sync what you'll actually filter on. Four fields cover most exclusion cases:

| What you want           | Why it earns a field                                     |
| ----------------------- | -------------------------------------------------------- |
| Lifecycle or deal stage | The main suppression signal                              |
| Record owner            | Separates rep-owned contacts from the open pool          |
| Last contacted date     | Catches someone a rep emailed who hasn't moved stage yet |
| CRM record ID           | Stable join key. Emails change, IDs don't                |

**Verify the internal property names before you build anything.** Display labels and internal names differ in most CRMs.

> Read the property definitions for contacts in \[CRM] and show me the internal names and stored values for \[your properties].

Some CRMs store dropdown values as internal codes or numeric IDs rather than the label you see. A filter written against the label matches nothing, so check the real stored values first.

## 4. Run the Sync

> Get \[your four properties] from \[CRM] for every contact in \[list or segment]. Update each matching Brew contact with those values. Tell me how many you updated and list any you couldn't match.

**Always ask for the count and the unmatched list.** An assistant working a long list can skip rows without error. The count is your only proof it finished.

Ask for the report in this shape so gaps are obvious:

```
Updated: 47 of 52
Unmatched (5):
  jane@oldcompany.com   no Brew contact found
  sam@example.com       no lifecycle stage set in CRM
  ...
```

Run a full sync across the whole group once before you trust any filter. After that, re-runs only need to cover what changed.

## 5. Build the Audience

Add filter conditions in the Brew UI, or ask your assistant:

> Create a Brew audience called "Q3 nurture" where hubspotLifecycleStage is not opportunity, hubspotOwner is empty, and hubspotLastContacted is before 30 days ago.

Use the camelCase field names from step 2. Available operators:

| Type    | Operators                                                                                                                          |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| String  | `equals`, `not_equals`, `contains`, `not_contains`, `contains_any`, `not_contains_any`, `starts_with`, `ends_with`, `in`, `not_in` |
| Number  | `gt`, `gte`, `lt`, `lte`, `between`                                                                                                |
| Date    | `before`, `after`, `on_date`, `between`                                                                                            |
| Boolean | `is_true`, `is_false`                                                                                                              |
| Any     | `is_empty`, `is_not_empty`, `exists`, `not_exists`, `is_set`                                                                       |

Audiences are saved filters, not frozen snapshots. They re-evaluate at send time against whatever was last synced. Creating one returns the match count immediately:

```
Created audience "Q3 nurture", 3 contacts currently match.
audienceId: j971pkqx7yzfkvk02371yffqb58by8tq
```

If that count is zero and you expected more, check your filter first. The usual cause is a value written against a display label instead of the stored one. See [Create Audiences](/audience/create-audiences) for what else you can filter on.

## 6. Verify

Change one thing in your CRM and re-run the sync. Check that the audience count moves **by the number you expect**, not just that it moved.

| Change in the CRM                   | Expected in Brew                                         |
| ----------------------------------- | -------------------------------------------------------- |
| Stage set to your excluded value    | Contact drops out of the audience                        |
| Stage set back to an included value | Contact returns                                          |
| Assign an owner                     | Owner field populates, drops from unowned-only audiences |
| Log an activity                     | Last-contacted date updates                              |

Use two or three test contacts and confirm before you rely on it for a real send.

## From MCP

Your Brew connection is bound to one brand, picked when you connected. The CRM side may be workspace-scoped depending on the provider, so confirm you are pointed at the right brand before writing.

Brew tools accept an optional `brand_id`. Omit it to use the bound brand, since supplying a different one fails with `BRAND_SCOPE_MISMATCH` rather than writing to the wrong place. See [Authentication and Scoping](/api-reference/mcp/authentication-and-scoping).

* `list_custom_fields` shows what already exists. Fields with `isCore: true` are built in and can't be created or deleted. Call this before creating anything.
* `create_custom_field` takes `fieldName` and `fieldType`. Creating a field that already exists is safe, it returns the existing definition. Field names can't contain whitespace, path separators, or quotes.
* `update_contact` takes an `email` and a `fields` map, one contact per call. There's no batch write, so work through a large list in chunks and report the running count.
* `create_contact` upserts by email and returns `created: true|false`. Use it only for contacts who opted in through your own signup or checkout, not to pull a CRM list into Brew.

Pass an `idempotency_key`, a fresh UUID per intended action, so a retry replays the original result instead of acting twice. The same key with a different payload returns a 409. See [Idempotency](/api-reference/api/idempotency).

If a CRM property has no value for a contact, leave the Brew field unset. Don't write an empty string. An unset field reads as "not synced yet", while a blank one falsely reads as "synced, and empty."

The full tool catalog is in [MCP Tools](/api-reference/mcp/tools).

## When the Sync Fails

| What you see                      | What it means                                                      | What to do                                                                                                                                                     |
| --------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "requires additional permissions" | Scope missing, or a transient error                                | Retry once first, this appears transiently and resolves on its own. If it persists, disconnect and reconnect the CRM, since scopes are granted at connect time |
| Count lower than expected         | Assistant skipped rows                                             | Re-run on the remainder and ask for the unmatched list                                                                                                         |
| Contact not found                 | Email differs between systems, or the contact is missing from Brew | Upsert it, or reconcile the address                                                                                                                            |
| Filter matches nothing            | Filter written against a display label                             | Use the stored internal value                                                                                                                                  |
| `BRAND_SCOPE_MISMATCH`            | Wrong brand asserted                                               | Omit `brand_id` to use the bound brand                                                                                                                         |

## What This Does and Doesn't Do

**Syncs when asked, not automatically.** Nothing fires when a record changes in your CRM. Run the sync immediately before a send.

A daily sync leaves up to 24 hours of drift. That's long enough for a rep to open a deal at 9am and your 4pm send to email them anyway.

**One direction.** This pulls CRM data into Brew. To write engagement back to the CRM, see [Sync Brew and Attio Over MCP](/api-reference/mcp/attio).

**Matches on email only.** If your CRM stores multiple addresses per contact, decide which one is authoritative before syncing. Otherwise one person becomes two Brew contacts.

**Stores values, not flags.** Write `crmLifecycleStage = "customer"`, not `isCustomer = true`. Keeping the real value means un-suppressing costs nothing and new filters don't need a re-sync.

**Empty fields may be invisible.** Some CRMs omit unset properties from API responses entirely rather than returning null. An absent value is ambiguous. It can mean "never happened" or "this CRM isn't tracking it."

Check that a real sample is populated before you filter on it. A recency filter built on an empty field won't error. It will silently fail to suppress, and you'll hear about it from a recipient.

## Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to explore it further with ChatGPT or Claude.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
