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

# Close CRM

> Two-way sync between RevyOps and Close CRM — push qualified replies out, pull leads and activity in.

Close CRM is a **bidirectional** integration. Unlike single-direction integrations, it moves data both ways:

* **Outbound** — when a contact sends a qualifying "interested" reply to an outbound campaign, RevyOps finds or creates the matching Lead/Contact in Close and logs the reply as an email Activity.
* **Inbound** — RevyOps registers a webhook subscription with Close and mirrors Leads, Contacts, Opportunities, Tasks, and Activities back into RevyOps for reporting and attribution.

Both directions are configured from the same panel, and inbound sync is optional — you can run outbound-only if you just want qualified replies logged in Close.

## Prerequisites

* A Close CRM account with API access
* A Close API key (found in Close under **Settings → Developer → API Keys**)
* Admin role on the RevyOps client you're configuring — the setup and settings endpoints require the `admin` role, and Close CRM must be enabled as an available integration on your plan

## Connecting Close CRM

Unlike webhook-based integrations where you paste a URL into the other platform's UI, Close CRM setup runs entirely from RevyOps: you paste in an API key, and RevyOps calls the Close API to verify it, provision custom fields, and (if you enable inbound sync) register the webhook subscription. There's nothing to configure on the Close side.

<Steps>
  <Step title="Open the Integrations panel">
    In the RevyOps client dashboard, go to **Integrations** and select **Close CRM**.
  </Step>

  <Step title="Enter your API key">
    Paste your Close API key and click **Verify & Connect**. RevyOps calls Close's `GET /me/` endpoint to confirm the key is valid, then creates two custom contact fields in Close if they don't already exist: `Cold Email Accepted` (a choice field with `TRUE`/`FALSE`) and `Cold Email Reply Type` (a text field). If a field with either name already exists in Close but doesn't match the expected type, setup fails with an error asking you to rename or remove it.
  </Step>

  <Step title="Choose which reply statuses sync to Close">
    On the **Configure** screen, RevyOps lists the "interested" reply labels pulled from your connected sequencers (Instantly, SmartLead, Lemlist, EmailBison, PlusVibe). Toggle which ones should push to Close CRM. Leaving this unset syncs every interested reply, regardless of label.
  </Step>

  <Step title="Decide whether to import CRM data into RevyOps">
    The same screen has an **Import CRM Data Into RevyOps** toggle. This is optional and off doesn't stop outbound sync — it only controls whether Close data flows back into RevyOps. Turning it on:

    <br />

    1. Provisions a Close webhook subscription pointed at your client's webhook URL.

    <br />

    2. Kicks off a one-time full backfill of existing Close data (see [Initial import](#initial-import) below).
  </Step>

  <Step title="Confirm">
    Click **Confirm Sync Settings**. If you enabled inbound sync and webhook provisioning succeeds, the panel moves to an import progress screen. If provisioning fails (e.g. Close API error), inbound sync is left disabled and the error is shown — outbound sync is unaffected either way.
  </Step>
</Steps>

## Outbound sync

Outbound sync is **always on** once Close CRM is connected — it isn't a toggle. It fires whenever a reply is marked interested (from any connected sequencer's webhook), for every disposition you selected during setup. For each qualifying reply, RevyOps:

1. Finds or creates a Close **Contact** by searching Close for the contact's email address. If no contact exists, RevyOps also finds or creates a Close **Lead** by matching the contact's email domain against existing Lead URLs (`http://domain`, `https://domain`, and `www.` variants), falling back to a name match, and finally creating a new Lead if nothing matches.
2. Sets `Cold Email Accepted = TRUE` on the Close contact.
3. Sets `Cold Email Reply Type` to the reply's disposition (e.g. `Interested`), if one is set.
4. Pushes any per-email custom field on the reply whose name starts with `tracking_` as an additional custom field on the Close contact, creating the field in Close if it doesn't exist yet.
5. Creates a Close **Email Activity** (`activity/email/`, direction `incoming`) recording the reply — sender, recipient, subject, and body (HTML preferred, falling back to plain text; either is truncated to 100 KB with a `... [truncated]` suffix). This only happens once per email: a `stored_in_close` flag on the email record prevents duplicate activity creation if the reply is reprocessed.

Any failure in this chain (custom field name collisions, Close API errors) is recorded in the **Sync Log** in the integration settings panel rather than surfaced to the end user.

## Inbound sync

Inbound sync is optional and controlled by the **Import CRM Data Into RevyOps** toggle in the settings panel. When on, RevyOps:

1. Registers a real webhook subscription with Close (`POST /webhook/` or `PUT /webhook/<id>/`) pointed at a per-client URL, subscribed to lead, contact, opportunity, task, and activity events (see [Real-time sync](#real-time-sync) below).
2. Keeps mirrored copies of Close data in RevyOps as new events arrive.

Turning the toggle off pauses ongoing sync (new Close webhook events are accepted but ignored) without deleting anything already imported. Turning it back on re-provisions the webhook and starts a fresh full backfill.

### Real-time sync

Close delivers webhook events to `https://api.revyops.com/api/public/closecrm-webhook/{client_id}`. RevyOps subscribes to:

* **Lead**: created, updated, deleted, merged
* **Contact**: created, updated, deleted
* **Opportunity**: created, updated, deleted
* **Activities**: email, call, SMS, WhatsApp message, note, meeting, form submission, lead status change, opportunity status change, task completed, and custom activities
* **Tasks**: lead, incoming email, email follow-up, missed call, answered detached call, voicemail, opportunity due, incoming SMS (`task.outgoing_call` is excluded — Close's webhook API doesn't support subscribing to it)
* **Custom field / custom activity type schema changes**: lead, contact, activity, opportunity, and shared fields, plus custom activity type definitions

Every incoming event is HMAC-verified using `close-sig-hash` and `close-sig-timestamp` headers against a signing key Close returns when the webhook is created, then queued in a database table (rather than processed inline in the request). A background sync worker polls that queue in batches of 50, verifies each event's signature again before applying it, and deletes the row once processed. If Close returns a rate-limit (429) response mid-batch, the worker stops, reschedules itself using the `Retry-After`/`RateLimit` response headers (falling back to 30 seconds), and resumes from where it left off.

Deletes are handled explicitly: a deleted Lead archives the matching Company, a deleted Contact archives the matching Contacts (and re-syncs downstream reporting), and deleted Tasks/Opportunities/Activities remove the corresponding RevyOps Activity, Email, Call, SMS, or WhatsApp record.

## Initial import

When you first turn on inbound sync, RevyOps runs a one-time full backfill (this also re-runs if you disable and re-enable inbound sync later). The flow, visible as a progress screen in the panel:

1. **Export** — RevyOps calls Close's asynchronous lead export API (`POST /export/lead/`) requesting all leads with activities included, then polls every 10 seconds until the export is `done`.
2. **Download** — the completed export is downloaded and archived to Google Cloud Storage under `{agency_id}/{client_id}/closecrm-imports/{run_id}/close-export.json.gz`, for troubleshooting.
3. **Import** — RevyOps fetches your Close users, custom field schemas, custom activity type schemas, and call/meeting outcome mappings, then walks every exported lead, creating or updating the matching Company, Contacts, and Activities (tasks, activities, opportunities) in batches. Progress (companies/contacts/activities processed vs. total) updates live in the panel — you can safely close it and the import continues in the background.

Matching during import follows the same identity rules as real-time sync: contacts are matched by email, phone, or LinkedIn URL, and multiple Close contacts resolving to the same person are merged. If the import fails partway through, the panel shows a "Close CRM import failed" state with a retry option; if it's already running, starting it again is rejected until the current run finishes or fails.

## Do Not Contact (DNC) sync

Independent of the CRM sync toggle, Close CRM has its own DNC sync switch in the settings panel. When enabled:

* **Automatic**: whenever a contact is newly marked Do Not Contact in RevyOps (manually, in bulk, or pulled in from another platform's DNC list), RevyOps immediately pushes it to every other configured integration with DNC sync enabled, including Close CRM.
* **Manual**: the **Push DNC Now** button runs a full backfill push of every contact currently flagged `do_not_contact` for the client, in batches, with retry-on-rate-limit.

Either path finds or creates the Close Contact (and Lead, by domain) the same way outbound sync does, then sets a custom contact field named `Outbound Status` to `Do Not Contact`, creating the field (and adding the choice value if the field exists but is missing it) as needed. The settings panel shows the last sync status and timestamp, or an error if the push failed.

## Managing the integration

Once configured, the panel switches to a **Settings** view showing:

* The two outbound custom fields (`Cold Email Accepted`, `Cold Email Reply Type`) it manages in Close
* The current CRM sync statuses, editable in place
* DNC sync toggle and **Push DNC Now**
* The **Import CRM Data Into RevyOps** toggle — turning it on re-triggers a full backfill; turning it off requires confirming you understand ongoing sync will stop (it warns about losing reporting, attribution, and AI-workflow context)
* A **Reconfigure** action that resets the integration back to the API key step

### Sync Log

The settings panel includes a paginated Sync Log of issues recorded during outbound push or inbound import — for example, a custom field name collision in Close, or a failed activity creation. Issues can be cleared from the log with **Clear all**.

## Data mapped

| Close object                                                                                               | RevyOps model                                                                                                                                                                                          | Notes                                                                                                                                     |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Lead                                                                                                       | Company                                                                                                                                                                                                | Matched by `closecrm_id`, then by domain (parsed from the Lead's `url`)                                                                   |
| Contact                                                                                                    | Contact / Person                                                                                                                                                                                       | Matched by email, phone, or LinkedIn URL; multiple Close contacts on the same identity are merged into one Person                         |
| Opportunity                                                                                                | Activity (`closecrm.opportunity`)                                                                                                                                                                      |                                                                                                                                           |
| Task (`task.lead`, `task.incoming_email`, etc.)                                                            | Activity (`closecrm.task`)                                                                                                                                                                             |                                                                                                                                           |
| Activity — Email                                                                                           | Email                                                                                                                                                                                                  | `stored_in_close = True`; body truncated to 100 KB                                                                                        |
| Activity — Call                                                                                            | Call                                                                                                                                                                                                   |                                                                                                                                           |
| Activity — SMS                                                                                             | SMS Message                                                                                                                                                                                            |                                                                                                                                           |
| Activity — WhatsApp Message                                                                                | WhatsApp Message                                                                                                                                                                                       |                                                                                                                                           |
| Activity — Note, Meeting, Form Submission, Lead/Opportunity Status Change, Task Completed, Custom Activity | Activity (`closecrm.note`, `closecrm.meeting`, `closecrm.form_submission`, `closecrm.lead_status_change`, `closecrm.opportunity_status_change`, `closecrm.task_completed`, `closecrm.custom_activity`) | Requires a matching custom field/activity schema fetched from Close; events without a known schema are skipped and logged as a sync issue |
| Close custom fields (lead/contact/activity/opportunity/shared)                                             | Company/Contact/Activity custom fields                                                                                                                                                                 | Schema is re-fetched automatically whenever Close sends a `custom_fields.*` or `custom_activity_type` webhook event                       |
| Close user (owner)                                                                                         | Owner                                                                                                                                                                                                  | Created lazily the first time a record references that user                                                                               |

## Limitations

* There's no agency-wide "global webhook" option for Close CRM the way some sequencer integrations have — each client gets its own webhook subscription, provisioned individually when you enable inbound sync for that client.
* Email activity bodies (both HTML and plain text) are truncated at 100 KB when pushed to Close.
* `task.outgoing_call` events aren't imported — Close's webhook API doesn't support subscribing to that task type.
* Reply-to-Close sync only fires for dispositions you've selected in **CRM Sync Statuses**; if that list is empty, no replies sync out (leaving it in its default, unconfigured state syncs everything).
