# CRM Syncs

> Push contacts, companies, and list memberships to HubSpot, Salesforce, Pipedrive, Zoho, or SugarCRM

Source: https://docs.graph8.com/developers/crm-syncs/

Push graph8 records to a connected CRM. Five providers are supported:

| Provider | URL |
|----------|-----|
| `hubspot` | hubspot.com |
| `salesforce` | salesforce.com |
| `pipedrive` | pipedrive.com |
| `zoho` | zoho.com (CRM) |
| `sugarcrm` | sugarcrm.com |

You must connect the CRM in **Settings -> Integrations** first - the `{provider}` path parameter resolves to the existing Nango connection for the org. If no connection exists you'll get `404 Not Found`.

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/crm-syncs` | GET | List configured CRM integrations |
| `/crm-syncs/{provider}/contacts/push` | POST | Push contacts |
| `/crm-syncs/{provider}/companies/push` | POST | Push companies |
| `/crm-syncs/{provider}/lists/push` | POST | Modify list memberships (add/remove) |
| `/crm-syncs/{provider}/fields` | GET | Discover available fields for an entity type |
| `/crm-syncs/{provider}/status` | GET | Check connection health |

For machine-readable schemas see the [interactive API docs](https://be.graph8.com/api/v1/docs).

---

## List CRM Syncs

`GET /crm-syncs`

Returns connected CRM providers for the org. Disconnected providers are not returned.

### Example

**cURL**

```bash
curl "https://be.graph8.com/api/v1/crm-syncs" \
  -H "Authorization: Bearer $API_KEY"
```

### Response

```json
{
  "data": [
    {
      "provider": "hubspot",
      "status": "connected",
      "connection_id": "nango-conn-abc123",
      "last_sync_at": null,
      "connected_at": null
    }
  ]
}
```

---

## Push Contacts

`POST /crm-syncs/{provider}/contacts/push`

Bulk-create contacts on the CRM. The endpoint iterates each record and reports per-record errors in the response - one bad record does not fail the whole batch.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `records` | object[] | **Yes** | Contact payloads in the CRM's native field format (e.g. `email`, `firstname`, `lastname` for HubSpot) |
| `provider_fields` | object | No | Optional field mapping overrides |

### Example

**cURL**

```bash
curl -X POST "https://be.graph8.com/api/v1/crm-syncs/hubspot/contacts/push" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {"email": "jane@acme.com", "firstname": "Jane", "lastname": "Smith"},
      {"email": "bob@acme.com", "firstname": "Bob", "lastname": "Jones"}
    ]
  }'
```

### Response

```json
{
  "data": {
    "pushed_count": 2,
    "failed_count": 0,
    "errors": []
  }
}
```

When a record fails, `errors[]` contains `{ "record": "<email>", "error": "<message>" }`.

### Errors

| Status | Meaning |
|--------|---------|
| `404` | No active `{provider}` connection. Connect in Integrations first |

---

## Push Companies

`POST /crm-syncs/{provider}/companies/push`

Same shape as contact push. Each record is a company payload (e.g. `name`, `domain`, `industry` for HubSpot).

### Example

**cURL**

```bash
curl -X POST "https://be.graph8.com/api/v1/crm-syncs/salesforce/companies/push" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {"Name": "Acme Inc", "Website": "acme.com", "Industry": "Technology"}
    ]
  }'
```

---

## Push List Memberships

`POST /crm-syncs/{provider}/lists/push`

Add or remove contacts from CRM lists. Each `records[]` entry targets one list.

### Request Body

Each item in `records[]` should look like:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `list_id` | string | **Yes** | CRM list identifier |
| `add_contact_ids` | string[] | No | CRM contact IDs to add |
| `remove_contact_ids` | string[] | No | CRM contact IDs to remove |

### Example

**cURL**

```bash
curl -X POST "https://be.graph8.com/api/v1/crm-syncs/hubspot/lists/push" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [{
      "list_id": "42",
      "add_contact_ids": ["101", "102"],
      "remove_contact_ids": ["999"]
    }]
  }'
```

### Response

```json
{
  "data": {
    "added": 2,
    "removed": 1,
    "errors": []
  }
}
```

---

## Discover Fields

`GET /crm-syncs/{provider}/fields`

Return the schema (custom + standard fields) for an entity type on the CRM. Useful before a push so you can validate field names dynamically.

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `entity_type` | string | `contacts` | One of `contacts`, `companies`, `leads`, `deals` |

### Example

**cURL**

```bash
curl "https://be.graph8.com/api/v1/crm-syncs/hubspot/fields?entity_type=contacts" \
  -H "Authorization: Bearer $API_KEY"
```

### Response

```json
{
  "data": {
    "fields": [
      {
        "name": "First Name",
        "slug": "firstname",
        "type": "string",
        "required": false,
        "label": "First Name"
      }
    ]
  }
}
```

### Errors

| Status | Meaning |
|--------|---------|
| `502` | The CRM rejected the discovery request - check `message` for the upstream error |

---

## Get Status

`GET /crm-syncs/{provider}/status`

Health-check the connection by issuing a small read. Returns `connected: false` (with a `message`) if the connection is broken or the OAuth token expired - the endpoint never raises 5xx for connection failures.

### Example

**cURL**

```bash
curl "https://be.graph8.com/api/v1/crm-syncs/hubspot/status" \
  -H "Authorization: Bearer $API_KEY"
```

### Response

```json
{
  "data": {
    "provider": "hubspot",
    "connected": true,
    "connection_id": "nango-conn-abc123",
    "crm_type": "hubspot",
    "message": "Connection is healthy",
    "last_sync_at": null
  }
}
```

---

## Field Mapping Reference

Every provider has a canonical map from graph8 fields to the provider's native field names. Use `GET /crm-syncs/{provider}/fields` to introspect at runtime; the tables below are the maintained reference.

### Upsert keys (per provider)

| Provider | Contact upsert key | Company upsert key |
|---|---|---|
| HubSpot | `email` | `domain` |
| Salesforce | `Email` | `Website` (used as domain) |
| Pipedrive | `email` + `name` | `name` (no domain field) |
| Zoho | `Email` | `Website` |
| SugarCRM | `email` | `website` |

### Contact field map (canonical → provider)

| graph8 | HubSpot | Salesforce | Pipedrive | Zoho | SugarCRM |
|---|---|---|---|---|---|
| `work_email` | `email` | `Email` | `email[]` | `Email` | `email1` |
| `first_name` | `firstname` | `FirstName` | `name` (derived) | `First_Name` | `first_name` |
| `last_name` | `lastname` | `LastName` (default `-`) | `name` (derived) | `Last_Name` (fallback first_name) | `last_name` |
| `job_title` | `jobtitle` | `Title` | `org.title` | `Title` | `title` |
| `seniority_level` | `seniority` | — | — | — | — |
| `direct_phone` | `phone` | `Phone` | `phone[]` | `Phone` | `phone_work` |
| `mobile_phone` | `mobilephone` | `MobilePhone` | `phone[]` | `Mobile` | `phone_mobile` |
| `linkedin_url` | `hs_linkedin_url` | `Twitter` (custom) | (custom) | (custom) | `linkedin_c` |
| `city` | `city` | `MailingCity` | (custom) | `City` | `primary_address_city` |
| `country` | `country` | `MailingCountry` | (custom) | `Country` | `primary_address_country` |
| `source` | `hs_lead_source` | `LeadSource` | (custom) | `Lead_Source` | `lead_source` |

Notes:
- Salesforce requires `LastName` — graph8 defaults to `"-"` if missing.
- Pipedrive uses array shapes for email and phone (`[{ value, primary }]`).
- SugarCRM custom fields are suffixed `_c` (e.g. `linkedin_c`); HubSpot auto-discovers custom fields via the Properties API.

### Company field map (canonical → provider)

| graph8 | HubSpot | Salesforce | Pipedrive | Zoho | SugarCRM |
|---|---|---|---|---|---|
| `name` | `name` | `Name` | `name` | `Account_Name` | `name` |
| `domain` | `domain` | `Website` | (custom) | `Website` | `website` |
| `industry` | `industry` | `Industry` | (custom) | `Industry` | `industry` |
| `employee_count` | `numberofemployees` (int) | `NumberOfEmployees` | (custom) | `Employees` | `employees` |
| `revenue` | `annualrevenue` | `AnnualRevenue` | (custom) | `Annual_Revenue` | `annual_revenue` |
| `country` | `country` | `BillingCountry` | (custom) | `Billing_Country` | `billing_address_country` |
| `founded_year` | `founded_year` | (custom) | (custom) | (custom) | (custom) |

### Direction support

| Provider | Push (g8 → CRM) | Pull (CRM → g8) | Deals sync | Lifecycle / status owned by |
|---|---|---|---|---|
| HubSpot | ✅ full | ✅ full | ✅ | CRM (`lifecyclestage`, `hs_lead_status` overwrite g8 on pull) |
| Salesforce | ✅ contacts/companies | ⚠️ minimal (email + first/last only by default) | — | CRM |
| Pipedrive | ✅ contacts/companies (push-only) | — | ⚠️ partial | CRM |
| Zoho | ✅ contacts/companies | ✅ partial | ⚠️ partial | CRM |
| SugarCRM | ✅ full | ✅ full | ✅ | CRM (SugarHint enrichment fields pulled inbound) |

### Custom field handling

- **HubSpot**: auto-discovers all custom properties via the Properties API. Pass them as bare property names in `records[].properties`.
- **Salesforce**: pass-through. Custom fields use `__c` suffix natively.
- **Pipedrive**: custom fields are prefixed `org.` for company customs, `person.` for contact customs.
- **Zoho**: pass-through. Pre-sanitized (integers and currency parsed before push).
- **SugarCRM**: custom fields use `_c` suffix (e.g. `lead_score_c`).

### Type coercion + quirks

| Provider | Quirk |
|---|---|
| All | `email`, `phone`, `domain` are always strings. ZIP / postal codes are also strings (Salesforce + Zoho will 400 on integers). |
| Salesforce | Requires `LastName` — graph8 defaults to `"-"`. |
| Zoho | Requires `Last_Name` — graph8 falls back to `First_Name` if missing. |
| Pipedrive | Employee count ranges (`50-200`) are converted to a midpoint integer. |
| HubSpot | `numberofemployees` is integer, NOT the bucket string used elsewhere. |
| HubSpot ambiguous fields | `phone`, `address`, `city`, `state`, `country`, `zip`, `mobilephone` are personal contact fields. Don't include them on contact-→-company migration mappings (graph8 auto-filters). |

### Rate limits + quirks

- **HubSpot**: ~10 rps per integration; bulk endpoints accept 100 records per request.
- **Salesforce**: API request limits depend on the org's edition (typically 15k-1M / 24h). The connector uses bulk where available.
- **Pipedrive**: ~10 rps; no bulk upsert — graph8 paginates internally.
- **Zoho**: ~10 rps; require token refresh every hour.
- **SugarCRM**: instance-specific; check with your SugarCRM admin.

`GET /crm-syncs/{provider}/status` exposes the current rate-limit budget and last-sync timestamp.
