Skip to main content

Your Implementation Required

These endpoints must be implemented on your server

Overview

The sync and update endpoints allow bidirectional data flow between Enginy and your CRM:
EndpointPurpose
POST /companies/syncFind existing companies and optionally return properties to Enginy
PUT /companiesUpdate companies that already exist in your CRM
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.

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

Domain (Most Common)

Match by domain field - the most reliable identifier for companies

LinkedIn URL

Match by linkedinUrl - useful for B2B companies

Company Name

Match by name - use fuzzy matching for variations

Composite Key

Combine domain + name for higher accuracy

Domain Normalization

Always normalize domains before matching:
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

POST /companies/sync
Content-Type: application/json
X-API-Key: your-api-key

Request Body

Enginy sends all companies it wants to sync:
{
  "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:
{
  "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
      }
    }
  ]
}
Company company-789 is not in the response because it doesn’t exist in your CRM. Only return matches.

Example Implementation

Here’s how to implement the matching logic:
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')
}

Update Companies

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

Request

PUT /companies
Content-Type: application/json
X-API-Key: your-api-key

Request Body

{
  "companies": [
    {
      "crmId": "account_4k8j2m",
      "name": "Acme Corporation (Rebranded)",
      "domain": "acme.com",
      "employeeCount": 750,
      "industry": "Enterprise Technology",
      "revenue": "100M-250M"
    }
  ]
}
Every company in an update request has a crmId because it was returned during a previous sync.

Response

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

Example Implementation

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 })
})

Properties You Can Sync Back

When you return properties in your response, Enginy will store these values on the company. Common properties to sync:
PropertyTypeDescription
domainstringCompany’s domain
phonestringCompany’s phone number
accountTierstringYour account classification (Enterprise, SMB, Startup)
totalDealsnumberNumber of deals/opportunities with this company
lifetimeValuenumberTotal revenue from this account
accountOwnerstringAssigned account manager ID or name
industrystringYour CRM’s industry classification
tagsarrayCRM tags or labels
lastActivityDatestringISO 8601 date of last activity
customField_*anyAny custom fields you want to sync
You can return any properties you want. Enginy will store them as custom fields on the company.

Handling Null Values

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.
To prevent accidentally clearing data in Genesy, follow these guidelines:
Your CRM returnsResult in Genesy
"domain": "acme.com"Domain is updated to acme.com
"domain": nullDomain is cleared (set to null)
Field not included in responseDomain 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.
// 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):
FieldTypeDescription
externalIdstringEnginy’s internal company ID (use for mapping)
namestringCompany name
domainstringPrimary domain (normalize before matching!)
websitestringWebsite URL
industrystringIndustry classification
employeeCountnumberNumber of employees
employeeRangestringEmployee range (e.g., “50-100”)
revenuestringRevenue range
foundedYearnumberYear founded
descriptionstringCompany description
locationstringHeadquarters location
citystringCity
statestringState/region
countrystringCountry
phonestringCompany phone
linkedinUrlstringLinkedIn company page
twitterUrlstringTwitter/X profile
facebookUrlstringFacebook page
technologiesarrayTechnologies used
tagsarrayTags/labels
customFieldsobjectAny 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

{
  "companies": [
    {
      "crmId": "account_4k8j2m",
      "genesy_last_engaged_at": "2024-12-31T10:00:00Z",
      "genesy_active_contacts": 3
    }
  ]
}
Field names are user-configurable. Your CRM should accept any field name and store it appropriately.

Error Handling

Partial Success

If some companies fail, return successful results and errors separately:
{
  "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:
{
  "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:
// 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:
// 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

{
  "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 FieldTypeExample ValueDescription
campaignEngagementStatusstringMessage Replied (2/3) - LINKEDINCurrent engagement status
campaignSequenceStatusstringOngoingSequence status
campaignOpensstring5Number of email opens
campaignClicksstring2Number of link clicks
activitiesstringConnection Request Sent, Message SentActivity log
sendersstringJohn Smith, Jane DoeCampaign senders
campaignsstringQ1 Outreach, Product LaunchCampaign names
Field names are user-configurable. Your CRM should accept any field name and store it appropriately.

Implementation Tip

Make sure your PUT /companies handler stores unknown fields:
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

GitHub Repository

See a complete working implementation of companies sync and update endpoints, including engagement field handling.