Skip to content

CRM Syncs

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

ProviderURL
hubspothubspot.com
salesforcesalesforce.com
pipedrivepipedrive.com
zohozoho.com (CRM)
sugarcrmsugarcrm.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.

EndpointMethodDescription
/crm-syncsGETList configured CRM integrations
/crm-syncs/{provider}/contacts/pushPOSTPush contacts
/crm-syncs/{provider}/companies/pushPOSTPush companies
/crm-syncs/{provider}/lists/pushPOSTModify list memberships (add/remove)
/crm-syncs/{provider}/fieldsGETDiscover available fields for an entity type
/crm-syncs/{provider}/statusGETCheck connection health

For machine-readable schemas see the interactive API docs.


List CRM Syncs

GET /crm-syncs

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

Example

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

Response

{
"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

FieldTypeRequiredDescription
recordsobject[]YesContact payloads in the CRM’s native field format (e.g. email, firstname, lastname for HubSpot)
provider_fieldsobjectNoOptional field mapping overrides

Example

Terminal window
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

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

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

Errors

StatusMeaning
404No 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

Terminal window
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:

FieldTypeDescription
list_idstringCRM list identifier
add_contact_idsstring[]CRM contact IDs to add
remove_contact_idsstring[]CRM contact IDs to remove

Example

Terminal window
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

{
"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

ParameterTypeDefaultDescription
entity_typestringcontactsOne of contacts, companies, leads, deals

Example

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

Response

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

Errors

StatusMeaning
502The 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

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

Response

{
"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)

ProviderContact upsert keyCompany upsert key
HubSpotemaildomain
SalesforceEmailWebsite (used as domain)
Pipedriveemail + namename (no domain field)
ZohoEmailWebsite
SugarCRMemailwebsite

Contact field map (canonical → provider)

graph8HubSpotSalesforcePipedriveZohoSugarCRM
work_emailemailEmailemail[]Emailemail1
first_namefirstnameFirstNamename (derived)First_Namefirst_name
last_namelastnameLastName (default -)name (derived)Last_Name (fallback first_name)last_name
job_titlejobtitleTitleorg.titleTitletitle
seniority_levelseniority
direct_phonephonePhonephone[]Phonephone_work
mobile_phonemobilephoneMobilePhonephone[]Mobilephone_mobile
linkedin_urlhs_linkedin_urlTwitter (custom)(custom)(custom)linkedin_c
citycityMailingCity(custom)Cityprimary_address_city
countrycountryMailingCountry(custom)Countryprimary_address_country
sourcehs_lead_sourceLeadSource(custom)Lead_Sourcelead_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)

graph8HubSpotSalesforcePipedriveZohoSugarCRM
namenameNamenameAccount_Namename
domaindomainWebsite(custom)Websitewebsite
industryindustryIndustry(custom)Industryindustry
employee_countnumberofemployees (int)NumberOfEmployees(custom)Employeesemployees
revenueannualrevenueAnnualRevenue(custom)Annual_Revenueannual_revenue
countrycountryBillingCountry(custom)Billing_Countrybilling_address_country
founded_yearfounded_year(custom)(custom)(custom)(custom)

Direction support

ProviderPush (g8 → CRM)Pull (CRM → g8)Deals syncLifecycle / status owned by
HubSpot✅ full✅ fullCRM (lifecyclestage, hs_lead_status overwrite g8 on pull)
Salesforce✅ contacts/companies⚠️ minimal (email + first/last only by default)CRM
Pipedrive✅ contacts/companies (push-only)⚠️ partialCRM
Zoho✅ contacts/companies✅ partial⚠️ partialCRM
SugarCRM✅ full✅ fullCRM (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

ProviderQuirk
Allemail, phone, domain are always strings. ZIP / postal codes are also strings (Salesforce + Zoho will 400 on integers).
SalesforceRequires LastName — graph8 defaults to "-".
ZohoRequires Last_Name — graph8 falls back to First_Name if missing.
PipedriveEmployee count ranges (50-200) are converted to a midpoint integer.
HubSpotnumberofemployees is integer, NOT the bucket string used elsewhere.
HubSpot ambiguous fieldsphone, 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.