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

# CRM Sync

> Push new users into HubSpot, Salesforce, Pipedrive or Zoho as contacts, and log every conversation as an activity.

Connect a CRM and your agent keeps it fed automatically:

1. **People become contacts.** Sign-ups are upserted by email (so re-syncing never duplicates); messaging senders — WhatsApp, Telegram, Slack, and every other connected chat app — are filed automatically from a name, number, or handle; and anonymous website visitors become contacts the moment they share any detail — a name, email, or phone. See [Where contacts come from](#where-contacts-come-from).
2. **Conversations become activities.** Each chat is written back as a note (HubSpot, Pipedrive, Zoho) or a completed task (Salesforce), attached to that contact.

Nothing is sent synchronously during a signup or a chat turn. Work goes onto a durable queue that survives restarts, retries with exponential backoff, and dead-letters anything permanently broken so you can see it and act.

<Warning>
  **Full transcripts are off by default.** Out of the box only conversation *metadata* leaves the platform — channel, contact, message count, last activity. Turn on **Include the full transcript** to also export the conversation text, which is scrubbed of API keys and bearer tokens first.
</Warning>

***

## The four connectors

| CRM            | Connect with                | Contact | Activity       | Upsert key                         |
| -------------- | --------------------------- | ------- | -------------- | ---------------------------------- |
| **HubSpot**    | Private-app token, or OAuth | Contact | Note           | `email` (native batch upsert)      |
| **Salesforce** | OAuth only                  | Contact | Task (`WhoId`) | External-id field, else email      |
| **Pipedrive**  | API token, or OAuth         | Person  | Note           | Email (search-then-write)          |
| **Zoho CRM**   | OAuth only                  | Contact | Note           | `Email` (`duplicate_check_fields`) |

Each connector is a full-width card on the CRM page. Click a card's header to expand it.

The **Upsert key** is how a contact *with an email* avoids duplicates. A contact that arrives with **no** email — a messaging sender known only by a handle — is created directly and de-duplicated by Agentheya instead (see [Messaging channels](#messaging-channels)).

***

## Connecting

### With a token (HubSpot, Pipedrive)

The quickest path. Paste a **HubSpot private-app access token** or a **Pipedrive API token**, and for Pipedrive also give your **company domain** (`https://yourco.pipedrive.com` — not the shared `api.pipedrive.com` proxy, which has no web UI and cannot deep-link back to a person).

The token is verified against the live API before it is saved, and stored encrypted at rest.

### With OAuth

If the platform has registered an app for that provider, **Connect with OAuth** appears and does everything for you.

### With your own OAuth app

Salesforce and Zoho have no static-token mode, so they need OAuth. If this server hasn't registered a platform-wide app for them, click **Use your own OAuth app**, register an app with the provider, and paste its **client ID** and **client secret**. They are stored encrypted against that one agent, and used for the authorization, the token exchange, and every later refresh.

Two providers also need a **login host**:

* **Salesforce** — sandboxes authenticate at `https://test.salesforce.com`, never at the production `login.salesforce.com`. Leave the field blank for production orgs.
* **Zoho** — accounts are data-center bound (`accounts.zoho.eu`, `.in`, `.com.au`, `.jp`, `.ca`, …). An authorization code minted in one data center cannot be redeemed in another. The callback detects your data center automatically, so you normally leave this blank.

Set your redirect URI on the provider's app to `https://<your-domain>/api/crm-oauth/callback`.

***

## What gets synced

Three toggles per connector. They save the moment you flip them.

| Toggle                              | Effect                                                                                                                                                                                          |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **New users become contacts**       | New people are queued for a contact upsert — on signup (password, Google, SSO, invite), and every messaging sender (any connected chat app) filed automatically from a name, number, or handle. |
| **Conversations become activities** | Each conversation is written back as a note/task, debounced (about ten minutes by default) so a chatty session produces at most one entry per window rather than one per message.               |
| **Include the full transcript**     | Off = metadata only. On = the redacted conversation text is appended to the activity body.                                                                                                      |

***

## Where contacts come from

Most CRM contacts are keyed on an **email address** — that is how connectors upsert without ever making a duplicate. Messaging channels are the exception: a chat-app sender is filed from whatever they arrive with — **any one of a name, a phone number, or a messaging handle is enough**, no email required. So for each place your agent talks to people, the question is simply: *what identifying detail arrives on its own, and does the agent still have to ask for more?*

That splits every surface into two kinds:

* **Known-identity surfaces** — the person has an account, arrived with their email attached, or messaged from a chat app (which always carries at least a handle). The contact is created **automatically**; the agent does nothing.
* **Cookie-only surfaces** — the person is just a browser cookie or an IP (a website chat or widget). There is nothing a CRM can file them under, so the agent must **collect one identifier first** — a name, an email, or a phone number — and save it with the `capture_contact` tool (or a pre-chat form collects it up front).

<Note>
  A contact is always filed in the CRM that owns the surface it came in on. A visitor on **your** widget lands in **your** CRM; a visitor on a [subscriber's](/sidebar-menu/users) hosted page or widget lands in **that subscriber's** CRM — never yours. The agent cannot choose the destination; the layer is fixed by the surface.
</Note>

### Surface by surface

| Surface                                                                                        | What the agent knows about the person                                                                             | How they become a contact                                                                                                                                                                                                                                                |
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Sign-up** (password, Google, SSO, invite)                                                    | Email + name                                                                                                      | **Automatically**, the moment they register                                                                                                                                                                                                                              |
| **Hosted page — signed-in customer**                                                           | Account email + name                                                                                              | **Automatically**, on their first conversation                                                                                                                                                                                                                           |
| **Hosted page — anonymous visitor**                                                            | IP only (plus anything a pre-chat form collected)                                                                 | Agent **asks for a name, email, or phone** (any one is enough) → `capture_contact` (or a pre-chat form)                                                                                                                                                                  |
| **Website widget** (embed)                                                                     | Browser cookie / IP; whatever the pre-chat form collected                                                         | Pre-chat form (name, email, or phone) → **automatic**; otherwise the agent **asks for any one** → `capture_contact`                                                                                                                                                      |
| **Inbound email**                                                                              | Sender's email + display name                                                                                     | Automatic **only when the sender is a registered account** — logged via the conversation sync (needs **Conversations become activities** on). An unrecognised sender is recorded in Contacts but not pushed to the CRM until the agent files them with `capture_contact` |
| **Messaging channels** (WhatsApp, Telegram, Discord, Slack, Messenger, Instagram, LINE, Viber) | At least one identifier, always — a name, a phone (WhatsApp DMs), and/or a messaging handle                       | **Automatically** — any single identifier files the sender, no email needed. See [Messaging channels](#messaging-channels)                                                                                                                                               |
| **API / OpenAI-compatible endpoint**                                                           | The API key. A user-scoped key is tied to a registered account (email + name); an integration key is not a person | User key → that account (already a contact if sign-up sync ran); integration key → nothing to add                                                                                                                                                                        |
| **Orders / checkout**                                                                          | Buyer's email + name from the order                                                                               | Buyer email → contact + an order note *(this hook is built but not yet wired up)*                                                                                                                                                                                        |
| **Webhooks · schedules · feeds · connector (MCP) events**                                      | An integration, a feed author, or a connector — not a person you can email                                        | No personal contact; `capture_contact` only if a real email appears in the content                                                                                                                                                                                       |
| **Owner preview / test chat**                                                                  | —                                                                                                                 | Never — preview sessions are deliberately kept out of your CRM                                                                                                                                                                                                           |

### Messaging channels

Every chat app your agent is connected to files its senders into the CRM **automatically** — you don't ask for anything. A CRM contact needs only one identifier, and a messaging sender always brings at least their platform handle, so a contact is always created. Whatever richer detail the channel exposes (a display name, a phone number) is added on top.

| Channel                | What we always get             | Also, when the platform sends it |
| ---------------------- | ------------------------------ | -------------------------------- |
| **WhatsApp**           | Phone number (direct messages) | Push name                        |
| **Telegram**           | Chat ID                        | First name                       |
| **Discord**            | User ID                        | Username                         |
| **Viber**              | User ID                        | Display name                     |
| **Slack**              | User ID                        | —                                |
| **Facebook Messenger** | Page-scoped ID                 | —                                |
| **Instagram**          | Instagram-scoped ID            | —                                |
| **LINE**               | User ID                        | —                                |

The contact is **labelled** with the best identifier available — the display name if there is one, otherwise the phone number, otherwise the handle — and de-duplicated on a stable per-sender key, so the same person messaging again updates the same record instead of creating a new one. It carries no email until someone provides one; use `capture_contact` to attach an email later.

<Note>
  `mqtt`, `redis` and `db` channels are machine-to-machine routes, not people — they never create contacts. An `imap` mailbox is treated as inbound email, where the sender's address is the contact.
</Note>

### The `capture_contact` tool

When a CRM is connected with **New users become contacts** turned on, your agent gains a `capture_contact` tool. It is how an anonymous visitor becomes a real contact: the agent calls it the moment someone shares **any** identifier — a name, an email, or a phone number.

| Field     | Required | Notes                                                                                                                                                                  |
| --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`    | —        | The person's name.                                                                                                                                                     |
| `email`   | —        | Their email address.                                                                                                                                                   |
| `phone`   | —        | Their phone number.                                                                                                                                                    |
| `company` | No       | Company / organisation.                                                                                                                                                |
| `note`    | No       | A one-line summary of what the person wants, logged as the first activity (e.g. *"Asked about enterprise pricing; wants a demo next week"*). Not the whole transcript. |

**At least one of `name`, `email`, or `phone` is required** — any single one is enough to create the contact. The agent is instructed to ask for whichever the visitor will share, and never to invent details.

The tool files the contact into the correct CRM by itself — the agent never chooses which one — and it is hidden on preview/test chats so testing never pollutes your CRM.

### Getting an identifier in the first place

On anonymous surfaces the agent can only file a contact **after** it has at least one identifier — a name, an email, or a phone number — so the job is to reliably collect one. Three ways, strongest first:

1. **Tell the agent to ask.** Add an instruction on the **Instructions / Purpose** tab so the agent collects contact details at the right moment — for example:

   > When a visitor wants a quote, a demo, a callback, or to be contacted, ask for their name, email, or phone number (any one is fine) before continuing, then call `capture_contact` to save them. Ask once, politely — don't nag.

   The agent then asks naturally in the conversation and files the contact itself. Use it to *add richer details* to a messaging contact (chat-app senders are already saved automatically from their handle / name / number), and as the only way to capture a contact on a one-off, cookie-only web chat.

2. **Add a pre-chat form** *(widgets)*. Give your widget's pre-chat form a **name, email, or phone** field (any of them — an email dedupes best, but any one creates the contact). Whatever a visitor types there is filed as a contact automatically on their first message — no asking required.

3. **Require sign-in.** Gate a hosted page so a visitor must register or log in before chatting. Every conversation then belongs to an account with a known email, and contacts are created automatically — at the cost of a higher barrier to starting a chat.

<Note>
  **Messaging senders are saved automatically** from whatever they arrive with — a handle alone is enough, so no email is required and you don't need to do anything. (A messaging contact has no email until someone provides one, so it won't merge with an email-based record for the same person; `capture_contact` is how you attach that email later.) Everywhere else — website chat, widgets — a browser cookie or IP is not something a CRM can file, so there the agent still has to obtain one identifier — a name, email, or phone — first.
</Note>

***

## Configuration

**API host** — the per-tenant host the provider handed us at connect time (Salesforce `instance_url`, Zoho `api_domain`, your Pipedrive company domain). Captured automatically; change it only if your tenant moved.

**External-id field** *(Salesforce only)* — the name of a custom external-id field on `Contact`, for example `AgentheyaUserId__c`. Set it and every sync becomes a true `PATCH`-by-external-id upsert keyed on our stable id, which survives a user changing their email address. It also gives email-less messaging-sender contacts a native Salesforce dedup key, instead of relying on Agentheya's own de-duplication. Leave it blank and Salesforce is matched on email instead.

***

## Testing before you trust it

Every connector card has an **Add a test user** panel. Fill in an email and a name, then:

* **Preview payload** — renders the exact HTTP request that would be sent: method, URL, headers (with the token redacted) and the full JSON body. Nothing leaves the server. This works *before* you connect, so you can see precisely which fields would be exported.
* **Send test contact** — pushes that one synthetic contact straight to your CRM, bypassing the queue so you see the result immediately: the record id, a deep link into the CRM (when the provider returns one — Salesforce always, Pipedrive on your company domain), the round-trip time, and the body that was actually uploaded.
* **Send test conversation** — attaches a sample conversation note to that test contact, so you can check how a real transcript will look.
* **Remove test contact** — deletes it again.

**Test connection** does a cheap authenticated read and reports the account it reached and how long it took.

***

## Keeping it healthy

The **Recent sync activity** table at the bottom of the page lists the newest jobs across every connector: what was synced, its status, how many attempts it took, and the error if it failed.

| Control                          | What it does                                                                   |
| -------------------------------- | ------------------------------------------------------------------------------ |
| **Sync now**                     | Drains the queue immediately instead of waiting for the next automatic sweep.  |
| **Resync all users**             | Re-queues a contact upsert for every existing user of this agent (up to 1000). |
| **Retry N failed**               | Re-arms dead-lettered jobs after you've fixed whatever broke.                  |
| **Discard failed**               | Clears the dead-letter.                                                        |
| **Clear** *(on an error banner)* | Dismisses a stale "Needs attention" without reconnecting.                      |

A connector shows **Needs attention** after a permanent failure — a revoked token, a missing scope, a wrong API host. The next successful sync clears it on its own.

Completed jobs are pruned after seven days. Dead-lettered jobs are kept, because they are the only record that something never made it.

***

## Removing a user

Deleting a subscriber from the [Users](/sidebar-menu/users) page queues a delete against every connected CRM, and drops the local mapping.

<Note>
  This **archives** the CRM record (HubSpot's recycle bin, Salesforce's, and so on) rather than irreversibly erasing it. That is deliberate: HubSpot's permanent GDPR delete also blocklists the email address, so the person could never be re-added to your CRM by any means. If you owe a data-subject erasure, perform it in the CRM directly.
</Note>

***

## Subscriber CRM sync

Everything above is *your* (the agent creator's) CRM connection. [Subscribers](/sidebar-menu/users) who resell or embed your agent get their **own**, completely separate CRM sync on their personal dashboard — with the same connectors, toggles, and test panel described here.

Their page states it plainly:

> Connect your own CRM and your customers are added to it automatically, with their conversations logged as activities. This connection is yours alone — it is separate from anything the agent's creator has configured, syncs only *your* customers, and the creator cannot see it.

So a subscriber's CRM connection:

* Is **scoped to their own downstream customers** — the people who chat on *their* hosted page or widget — and never touches yours.
* Is **invisible to you, the creator**: you can't view, edit, or read their connection, and they can't see yours.
* Runs the **same machinery** — durable queue, retries, dead-letter, the `capture_contact` tool, and the metadata-only default — independently, per subscriber.

A contact is always filed in the CRM that owns the surface it arrived on; the agent never chooses the layer. See [Where contacts come from](#where-contacts-come-from) for how each surface maps to a CRM.

***

## Scopes for your own OAuth app

If you register your own app, grant it exactly these:

| CRM            | Scopes to grant                                                                 |
| -------------- | ------------------------------------------------------------------------------- |
| **HubSpot**    | `crm.objects.contacts.read`, `crm.objects.contacts.write`                       |
| **Salesforce** | `api`, `refresh_token`                                                          |
| **Zoho CRM**   | `ZohoCRM.modules.contacts.ALL`, `ZohoCRM.modules.notes.ALL`, `ZohoCRM.org.READ` |
| **Pipedrive**  | Persons and Notes, enabled on the app in the Developer Hub                      |

<Note>
  Zoho's `ZohoCRM.org.READ` is what backs **Test connection** — without it the test reports a failure even though syncing works.

  Pipedrive ignores scopes passed at authorization time and takes them from the app's own configuration, so granting them in the Developer Hub is the only thing that matters.
</Note>

If a provider's card says the server has no OAuth app for it, bringing your own is the supported path — nothing else is required from an administrator.
