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

# Sync & Update Guide

> Sync companies and receive updates from your CRM

<Card title="Your Implementation Required" icon="triangle-exclamation" color="#f59e0b">
  These endpoints must be implemented on **your server**
</Card>

## Overview

The sync and update endpoints allow bidirectional data flow between Enginy and your CRM:

| Endpoint               | Purpose                                                            |
| ---------------------- | ------------------------------------------------------------------ |
| `POST /companies/sync` | Find existing companies and optionally return properties to Enginy |
| `PUT /companies`       | Update companies that already exist in your CRM                    |

<Info>
  **Key Principle**: All matching logic lives in **your CRM**, not Enginy. You decide how to identify
  companies (by domain, name, LinkedIn URL, etc.) and what data to sync back.
</Info>

***

## How Matching Works

When Enginy sends companies to sync, your CRM should:

1. **Look up each company** using your preferred identifier(s)
2. **Return matches** with your CRM's internal ID (`crmId`)
3. **Optionally return properties** you want synced back to Enginy

### Common Matching Strategies

<CardGroup cols={2}>
  <Card title="Domain (Most Common)" icon="globe">
    Match by `domain` field - the most reliable identifier for companies
  </Card>

  <Card title="LinkedIn URL" icon="linkedin">
    Match by `linkedinUrl` - useful for B2B companies
  </Card>

  <Card title="Company Name" icon="building">
    Match by `name` - use fuzzy matching for variations
  </Card>

  <Card title="Composite Key" icon="key">
    Combine domain + name for higher accuracy
  </Card>
</CardGroup>

### Domain Normalization

Always normalize domains before matching:

```javascript theme={null}
function normalizeDomain(domain) {
  if (!domain) return null
  return domain
    .toLowerCase()
    .replace(/^(https?:\/\/)?(www\.)?/, '') // Remove protocol and www
    .replace(/\/.*$/, '') // Remove path
    .trim()
}

// Examples:
// "https://www.acme.com/about" → "acme.com"
// "WWW.ACME.COM" → "acme.com"
// "acme.com" → "acme.com"
```

***

## Sync Companies

### Request

```http theme={null}
POST /companies/sync
Content-Type: application/json
X-API-Key: your-api-key
```

### Request Body

Enginy sends all companies it wants to sync:

```json theme={null}
{
  "companies": [
    {
      "externalId": "company-123",
      "name": "Acme Corporation",
      "domain": "acme.com",
      "industry": "Technology",
      "employeeCount": 500,
      "location": "San Francisco, CA",
      "linkedinUrl": "https://linkedin.com/company/acme"
    },
    {
      "externalId": "company-456",
      "name": "TechStartup Inc",
      "domain": "techstartup.io",
      "industry": "Software",
      "employeeCount": 25
    },
    {
      "externalId": "company-789",
      "name": "Unknown Corp",
      "domain": "unknown-company.com"
    }
  ]
}
```

### Response

Return **only companies that exist** in your CRM:

```json theme={null}
{
  "results": [
    {
      "externalId": "company-123",
      "crmId": "account_4k8j2m",
      "properties": {
        "accountTier": "Enterprise",
        "totalDeals": 5,
        "lifetimeValue": 250000,
        "accountOwner": "sales-team-west",
        "industry": "Enterprise Software"
      }
    },
    {
      "externalId": "company-456",
      "crmId": "account_7n3x9p",
      "properties": {
        "accountTier": "Startup",
        "totalDeals": 1,
        "lifetimeValue": 15000
      }
    }
  ]
}
```

<Note>
  Company `company-789` is not in the response because it doesn't exist in your CRM. Only return matches.
</Note>

### Example Implementation

Here's how to implement the matching logic:

<CodeGroup>
  ```javascript Node.js theme={null}
  app.post('/companies/sync', async (req, res) => {
    const { companies } = req.body
    const results = []

    for (const company of companies) {
      // Strategy 1: Match by domain (most common)
      const normalizedDomain = normalizeDomain(company.domain)
      let existing = normalizedDomain ? await db.accounts.findOne({ domain: normalizedDomain }) : null

      // Strategy 2: Fallback to LinkedIn URL
      if (!existing && company.linkedinUrl) {
        existing = await db.accounts.findOne({
          linkedinUrl: normalizeLinkedInUrl(company.linkedinUrl),
        })
      }

      // Strategy 3: Fallback to company name (fuzzy match)
      if (!existing && company.name) {
        existing = await db.accounts.findOne({
          name: { $regex: new RegExp(`^${escapeRegex(company.name)}$`, 'i') },
        })
      }

      // If found, return the match with properties to sync back
      if (existing) {
        results.push({
          externalId: company.externalId,
          crmId: existing.id,
          properties: {
            accountTier: existing.tier,
            totalDeals: existing.dealCount,
            lifetimeValue: existing.ltv,
            accountOwner: existing.ownerId,
            industry: existing.industry, // Your CRM's industry classification
          },
        })
      }
      // If not found, don't include in results
    }

    res.json({ results })
  })

  function normalizeDomain(domain) {
    if (!domain) return null
    return domain
      .toLowerCase()
      .replace(/^(https?:\/\/)?(www\.)?/, '')
      .replace(/\/.*$/, '')
      .trim()
  }

  function normalizeLinkedInUrl(url) {
    if (!url) return null
    return url.toLowerCase().replace(/\/$/, '').replace('www.linkedin.com', 'linkedin.com')
  }
  ```

  ```python Python theme={null}
  @app.post("/companies/sync")
  async def sync_companies(data: CompaniesSync):
      results = []

      for company in data.companies:
          # Strategy 1: Match by domain
          normalized_domain = normalize_domain(company.domain)
          existing = None

          if normalized_domain:
              existing = await db.accounts.find_one({
                  "domain": normalized_domain
              })

          # Strategy 2: Fallback to LinkedIn URL
          if not existing and company.linkedinUrl:
              existing = await db.accounts.find_one({
                  "linkedinUrl": normalize_linkedin_url(company.linkedinUrl)
              })

          # Strategy 3: Fallback to company name
          if not existing and company.name:
              existing = await db.accounts.find_one({
                  "name": {"$regex": f"^{re.escape(company.name)}$", "$options": "i"}
              })

          # If found, return the match
          if existing:
              results.append({
                  "externalId": company.externalId,
                  "crmId": existing["id"],
                  "properties": {
                      "accountTier": existing.get("tier"),
                      "totalDeals": existing.get("dealCount"),
                      "lifetimeValue": existing.get("ltv"),
                      "accountOwner": existing.get("ownerId"),
                  }
              })

      return {"results": results}


  def normalize_domain(domain: str) -> str:
      if not domain:
          return None
      import re
      domain = domain.lower()
      domain = re.sub(r'^(https?://)?(www\.)?', '', domain)
      domain = re.sub(r'/.*$', '', domain)
      return domain.strip()
  ```
</CodeGroup>

***

## Update Companies

After syncing, Enginy knows which companies have CRM IDs. The update endpoint receives **only companies that were previously matched**.

### Request

```http theme={null}
PUT /companies
Content-Type: application/json
X-API-Key: your-api-key
```

### Request Body

```json theme={null}
{
  "companies": [
    {
      "crmId": "account_4k8j2m",
      "name": "Acme Corporation (Rebranded)",
      "domain": "acme.com",
      "employeeCount": 750,
      "industry": "Enterprise Technology",
      "revenue": "100M-250M"
    }
  ]
}
```

<Tip>Every company in an update request has a `crmId` because it was returned during a previous sync.</Tip>

### Response

```json theme={null}
{
  "results": [
    {
      "crmId": "account_4k8j2m",
      "success": true,
      "properties": {
        "lastUpdated": "2024-01-20T10:30:00Z"
      }
    }
  ]
}
```

### Example Implementation

<CodeGroup>
  ```javascript Node.js theme={null}
  app.put('/companies', async (req, res) => {
    const { companies } = req.body
    const results = []

    for (const company of companies) {
      try {
        // Find by crmId (always present in update requests)
        const existing = await db.accounts.findById(company.crmId)

        if (!existing) {
          results.push({
            crmId: company.crmId,
            success: false,
            error: 'Company not found',
          })
          continue
        }

        // Update the company with new data
        // Your CRM decides which fields to update
        await db.accounts.updateOne(
          { _id: company.crmId },
          {
            $set: {
              name: company.name,
              domain: normalizeDomain(company.domain),
              employeeCount: company.employeeCount,
              industry: company.industry,
              revenue: company.revenue,
              updatedAt: new Date(),
              updatedBy: 'enginy-sync',
            },
          },
        )

        results.push({
          crmId: company.crmId,
          success: true,
          properties: {
            lastUpdated: new Date().toISOString(),
          },
        })
      } catch (error) {
        results.push({
          crmId: company.crmId,
          success: false,
          error: error.message,
        })
      }
    }

    res.json({ results })
  })
  ```

  ```python Python theme={null}
  @app.put("/companies")
  async def update_companies(data: CompaniesUpdate):
      results = []

      for company in data.companies:
          try:
              existing = await db.accounts.find_one({"_id": company.crmId})

              if not existing:
                  results.append({
                      "crmId": company.crmId,
                      "success": False,
                      "error": "Company not found"
                  })
                  continue

              await db.accounts.update_one(
                  {"_id": company.crmId},
                  {"$set": {
                      "name": company.name,
                      "domain": normalize_domain(company.domain),
                      "employeeCount": company.employeeCount,
                      "industry": company.industry,
                      "revenue": company.revenue,
                      "updatedAt": datetime.utcnow(),
                  }}
              )

              results.append({
                  "crmId": company.crmId,
                  "success": True,
              })
          except Exception as e:
              results.append({
                  "crmId": company.crmId,
                  "success": False,
                  "error": str(e)
              })

      return {"results": results}
  ```
</CodeGroup>

***

## Properties You Can Sync Back

When you return `properties` in your response, Enginy will store these values on the company. Common properties to sync:

| Property           | Type   | Description                                            |
| ------------------ | ------ | ------------------------------------------------------ |
| `domain`           | string | Company's domain                                       |
| `phone`            | string | Company's phone number                                 |
| `accountTier`      | string | Your account classification (Enterprise, SMB, Startup) |
| `totalDeals`       | number | Number of deals/opportunities with this company        |
| `lifetimeValue`    | number | Total revenue from this account                        |
| `accountOwner`     | string | Assigned account manager ID or name                    |
| `industry`         | string | Your CRM's industry classification                     |
| `tags`             | array  | CRM tags or labels                                     |
| `lastActivityDate` | string | ISO 8601 date of last activity                         |
| `customField_*`    | any    | Any custom fields you want to sync                     |

<Info>You can return any properties you want. Enginy will store them as custom fields on the company.</Info>

### Handling Null Values

<Warning>
  **Important**: When Genesy syncs properties back from your CRM, fields that are returned as `null` (or not
  returned at all) will **clear the existing value** in Genesy. This includes core fields like `domain` and
  `phone`.
</Warning>

To prevent accidentally clearing data in Genesy, follow these guidelines:

| Your CRM returns               | Result in Genesy                    |
| ------------------------------ | ----------------------------------- |
| `"domain": "acme.com"`         | Domain is updated to `acme.com`     |
| `"domain": null`               | Domain is **cleared** (set to null) |
| Field not included in response | Domain is **cleared** (set to null) |

**Best Practice**: Only include fields in `properties` that you explicitly want to sync back. If you don't want to modify a field, **don't include it** in the properties object.

```javascript theme={null}
// Good: Only return fields you want to update
results.push({
  externalId: company.externalId,
  crmId: existing.id,
  properties: {
    accountTier: existing.tier,
    lifetimeValue: existing.ltv,
    // Don't include domain/phone if you don't want to overwrite Genesy's values
  },
})

// Bad: Including null values will clear data in Genesy
results.push({
  externalId: company.externalId,
  crmId: existing.id,
  properties: {
    domain: existing.domain, // If null, will clear domain in Genesy!
    phone: existing.phone, // If null, will clear phone in Genesy!
  },
})
```

***

## Available Company Fields

Enginy sends these fields (when available):

| Field           | Type   | Description                                    |
| --------------- | ------ | ---------------------------------------------- |
| `externalId`    | string | Enginy's internal company ID (use for mapping) |
| `name`          | string | Company name                                   |
| `domain`        | string | Primary domain (normalize before matching!)    |
| `website`       | string | Website URL                                    |
| `industry`      | string | Industry classification                        |
| `employeeCount` | number | Number of employees                            |
| `employeeRange` | string | Employee range (e.g., "50-100")                |
| `revenue`       | string | Revenue range                                  |
| `foundedYear`   | number | Year founded                                   |
| `description`   | string | Company description                            |
| `location`      | string | Headquarters location                          |
| `city`          | string | City                                           |
| `state`         | string | State/region                                   |
| `country`       | string | Country                                        |
| `phone`         | string | Company phone                                  |
| `linkedinUrl`   | string | LinkedIn company page                          |
| `twitterUrl`    | string | Twitter/X profile                              |
| `facebookUrl`   | string | Facebook page                                  |
| `technologies`  | array  | Technologies used                              |
| `tags`          | array  | Tags/labels                                    |
| `customFields`  | object | Any custom fields                              |

***

## Engagement Field Updates

When contacts at a company engage with campaigns, Enginy can also send engagement updates to companies through the `PUT /companies` endpoint. These updates contain **custom field names** configured by the user.

### Example Engagement Update

```json theme={null}
{
  "companies": [
    {
      "crmId": "account_4k8j2m",
      "genesy_last_engaged_at": "2024-12-31T10:00:00Z",
      "genesy_active_contacts": 3
    }
  ]
}
```

<Info>
  Field names are user-configurable. Your CRM should accept **any** field name and store it appropriately.
</Info>

***

## Error Handling

### Partial Success

If some companies fail, return successful results and errors separately:

```json theme={null}
{
  "results": [
    {
      "externalId": "company-123",
      "crmId": "account_4k8j2m"
    }
  ],
  "errors": [
    {
      "externalId": "company-456",
      "error": "Multiple accounts match domain techstartup.io"
    }
  ]
}
```

### Update Failures

For updates, indicate failure in the result:

```json theme={null}
{
  "results": [
    {
      "crmId": "account_4k8j2m",
      "success": true
    },
    {
      "crmId": "account_invalid",
      "success": false,
      "error": "Company not found"
    }
  ]
}
```

***

## Handling Edge Cases

### Subsidiaries & Parent Companies

If your CRM tracks parent/subsidiary relationships, decide how to handle:

```javascript theme={null}
// Option 1: Match to parent company only
if (existing.parentAccountId) {
  existing = await db.accounts.findById(existing.parentAccountId)
}

// Option 2: Match to subsidiary, return both IDs
results.push({
  externalId: company.externalId,
  crmId: existing.id,
  properties: {
    parentCompanyId: existing.parentAccountId,
    isSubsidiary: true,
  },
})
```

### Duplicate Domains

If multiple accounts share a domain, pick the most relevant:

```javascript theme={null}
// Find all matches
const matches = await db.accounts.find({ domain: normalizedDomain })

if (matches.length > 1) {
  // Pick the primary account (e.g., highest revenue, most recent activity)
  existing = matches.sort((a, b) => b.ltv - a.ltv)[0]
}
```

***

## Engagement Field Updates

When contacts at a company engage with campaigns, Enginy can also update company records through the `PUT /companies` endpoint. These updates contain **custom field names** configured by the user.

### Example Engagement Update

```json theme={null}
{
  "companies": [
    {
      "crmId": "account_4k8j2m",
      "genesy_engagement_status": "Message Replied (2/3) - LINKEDIN",
      "genesy_last_activity": "2024-12-31T10:00:00Z",
      "genesy_active_contacts": "3"
    }
  ]
}
```

### Available Engagement Fields

The same engagement fields available for contacts can be configured for companies:

| Enginy Field               | Type   | Example Value                           | Description               |
| -------------------------- | ------ | --------------------------------------- | ------------------------- |
| `campaignEngagementStatus` | string | `Message Replied (2/3) - LINKEDIN`      | Current engagement status |
| `campaignSequenceStatus`   | string | `Ongoing`                               | Sequence status           |
| `campaignOpens`            | string | `5`                                     | Number of email opens     |
| `campaignClicks`           | string | `2`                                     | Number of link clicks     |
| `activities`               | string | `Connection Request Sent, Message Sent` | Activity log              |
| `senders`                  | string | `John Smith, Jane Doe`                  | Campaign senders          |
| `campaigns`                | string | `Q1 Outreach, Product Launch`           | Campaign names            |

<Info>
  Field names are user-configurable. Your CRM should accept **any** field name and store it appropriately.
</Info>

### Implementation Tip

Make sure your `PUT /companies` handler stores unknown fields:

```javascript theme={null}
app.put('/companies', async (req, res) => {
  const { companies } = req.body
  const results = []

  for (const company of companies) {
    const { crmId, ...fieldsToUpdate } = company

    // Store ALL fields, not just known ones
    await db.accounts.updateOne({ _id: crmId }, { $set: fieldsToUpdate })

    results.push({ crmId, success: true })
  }

  res.json({ results })
})
```

***

## Reference Implementation

<Card title="GitHub Repository" icon="github" href="https://github.com/Genesy-AI/custom-crm-example">
  See a complete working implementation of companies sync and update endpoints, including engagement field
  handling.
</Card>
