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:
Endpoint Purpose 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:
Look up each company using your preferred identifier(s)
Return matches with your CRM’s internal ID (crmId)
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:
Property Type Description domainstring Company’s domain phonestring Company’s phone number accountTierstring Your account classification (Enterprise, SMB, Startup) totalDealsnumber Number of deals/opportunities with this company lifetimeValuenumber Total revenue from this account accountOwnerstring Assigned account manager ID or name industrystring Your CRM’s industry classification tagsarray CRM tags or labels lastActivityDatestring ISO 8601 date of last activity customField_*any Any 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 returns Result in Genesy "domain": "acme.com"Domain is updated to acme.com "domain": nullDomain 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.
// 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 externalIdstring Enginy’s internal company ID (use for mapping) namestring Company name domainstring Primary domain (normalize before matching!) websitestring Website URL industrystring Industry classification employeeCountnumber Number of employees employeeRangestring Employee range (e.g., “50-100”) revenuestring Revenue range foundedYearnumber Year founded descriptionstring Company description locationstring Headquarters location citystring City statestring State/region countrystring Country phonestring Company phone linkedinUrlstring LinkedIn company page twitterUrlstring Twitter/X profile facebookUrlstring Facebook page technologiesarray Technologies used tagsarray Tags/labels customFieldsobject 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
{
"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 Field Type Example Value Description campaignEngagementStatusstring Message Replied (2/3) - LINKEDINCurrent engagement status campaignSequenceStatusstring OngoingSequence status campaignOpensstring 5Number of email opens campaignClicksstring 2Number of link clicks activitiesstring Connection Request Sent, Message SentActivity log sendersstring John Smith, Jane DoeCampaign senders campaignsstring Q1 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.