Skip to main content

Quick Navigation


Sync & Update Endpoints

Sync endpoints find existing records in your CRM and optionally return properties to sync back. Update endpoints push changes to records that were previously synced.
Key Principle: All matching logic lives in YOUR CRM. Enginy sends data, and your CRM decides how to match records (by email, domain, LinkedIn URL, etc.).

Quick Reference

EndpointMethodPurpose
/contacts/syncPOSTFind existing contacts, return crmId + properties
/contactsPUTUpdate contacts that have crmId
/companies/syncPOSTFind existing companies, return crmId + properties
/companiesPUTUpdate companies that have crmId

Matching Strategy Summary

EntityPrimary MatchFallbackNormalization
Contactsemail (lowercase)linkedinUrlRemove trailing slashes
CompaniesdomainlinkedinUrl, nameStrip www., protocol, path

Export (Create) Endpoints

These endpoints create new records that don’t exist in your CRM yet.

Complete Implementation Examples

Below are full working examples of a Custom CRM API implementation in popular frameworks, including all endpoints for export (contacts, companies, associations), sync, update, tasks, and activities.

GitHub Repository

Clone our reference implementation - a complete working Node.js/Express server with SQLite database, including a web dashboard for viewing contacts, companies, tasks, and activities.
The reference implementation includes: - All required endpoints (health, contacts, companies, associations, tasks, activities) - Optional users endpoint for owner assignment - SQLite database with Prisma ORM - Web dashboard at / for viewing data - Contact/company detail pages showing associated activities - Full activity logging with direction indicators (inbound/outbound)
const express = require('express');
const { v4: uuidv4 } = require('uuid');

const app = express();
app.use(express.json());

// In-memory storage (replace with your database)
const contacts = new Map();
const companies = new Map();
const tasks = new Map();
const activities = new Map();
const users = new Map([
  ['user-1', { id: 'user-1', name: 'John Smith', email: '[email protected]' }],
  ['user-2', { id: 'user-2', name: 'Jane Doe', email: '[email protected]' }],
]);

// Middleware to validate API key
const validateApiKey = (req, res, next) => {
  const apiKey = req.headers['x-api-key'];
  if (apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Invalid API key' });
  }
  next();
};

app.use(validateApiKey);

// Health endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// ============================================
// USERS ENDPOINT (Optional - for owner assignment)
// ============================================

// Get users (optional - return 404 to disable owner selection)
app.get('/users', (req, res) => {
  res.json(Array.from(users.values()));
});

// ============================================
// CONTACTS ENDPOINTS (Export)
// ============================================

// Create contacts (batch)
app.post('/contacts', async (req, res) => {
  const { contacts: contactsToCreate } = req.body;
  const results = [];
  const errors = [];

  for (const contact of contactsToCreate) {
    try {
      const crmId = uuidv4();
      contacts.set(crmId, {
        id: crmId,
        externalId: contact.externalId,
        email: contact.email,
        firstName: contact.firstName,
        lastName: contact.lastName,
        phone: contact.phone,
        company: contact.company,
        title: contact.title,
        linkedinUrl: contact.linkedinUrl,
        // Store all additional fields
        ...contact,
        createdAt: new Date().toISOString(),
      });

      results.push({
        externalId: contact.externalId,
        crmId: crmId,
      });
    } catch (error) {
      errors.push({
        externalId: contact.externalId,
        error: error.message,
      });
    }
  }

  res.status(201).json({ results, errors: errors.length > 0 ? errors : undefined });
});

// ============================================
// COMPANIES ENDPOINTS (Export)
// ============================================

// Create companies (batch)
app.post('/companies', async (req, res) => {
  const { companies: companiesToCreate } = req.body;
  const results = [];
  const errors = [];

  for (const company of companiesToCreate) {
    try {
      const crmId = uuidv4();
      companies.set(crmId, {
        id: crmId,
        externalId: company.externalId,
        name: company.name,
        domain: company.domain,
        industry: company.industry,
        numberOfEmployees: company.numberOfEmployees,
        linkedinUrl: company.linkedinUrl,
        // Store all additional fields
        ...company,
        createdAt: new Date().toISOString(),
      });

      results.push({
        externalId: company.externalId,
        crmId: crmId,
      });
    } catch (error) {
      errors.push({
        externalId: company.externalId,
        error: error.message,
      });
    }
  }

  res.status(201).json({ results, errors: errors.length > 0 ? errors : undefined });
});

// ============================================
// ASSOCIATIONS ENDPOINTS (Export)
// ============================================

// Create associations (batch)
app.post('/associations', async (req, res) => {
  const { associations } = req.body;
  let created = 0;
  const errors = [];

  for (const assoc of associations) {
    const contact = contacts.get(assoc.contactCRMId);
    const company = companies.get(assoc.companyCRMId);

    if (!contact) {
      errors.push({
        contactCRMId: assoc.contactCRMId,
        companyCRMId: assoc.companyCRMId,
        error: 'Contact not found',
      });
      continue;
    }

    if (!company) {
      errors.push({
        contactCRMId: assoc.contactCRMId,
        companyCRMId: assoc.companyCRMId,
        error: 'Company not found',
      });
      continue;
    }

    // Link contact to company
    contact.companyCRMId = assoc.companyCRMId;
    contact.companyName = company.name;
    created++;
  }

  res.json({
    success: true,
    created,
    errors: errors.length > 0 ? errors : undefined,
  });
});

// ============================================
// TASKS ENDPOINTS (Sequences)
// ============================================

// Create task
app.post('/tasks', async (req, res) => {
  const { subject, description, type, ownerId, dueDate, contactId, companyId } = req.body;

  const task = {
    id: uuidv4(),
    subject,
    description,
    type,
    ownerId,
    dueDate: dueDate ? new Date(dueDate) : null,
    contactId,
    companyId,
    completed: false,
    completedAt: null,
    createdAt: new Date().toISOString(),
  };

  tasks.set(task.id, task);
  res.status(201).json(task);
});

// Get task
app.get('/tasks/:taskId', async (req, res) => {
  const task = tasks.get(req.params.taskId);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  res.json(task);
});

// Get tasks batch
app.post('/tasks/batch', async (req, res) => {
  const { ids } = req.body;
  const result = ids.map((id) => tasks.get(id)).filter(Boolean);
  res.json(result);
});

// Update task
app.patch('/tasks/:taskId', async (req, res) => {
  const task = tasks.get(req.params.taskId);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  const { completed } = req.body;
  task.completed = completed;
  task.completedAt = completed ? new Date().toISOString() : null;

  res.json(task);
});

// Complete tasks batch
app.patch('/tasks/batch', async (req, res) => {
  const { ids, completed } = req.body;

  const updated = ids
    .map((id) => {
      const task = tasks.get(id);
      if (task) {
        task.completed = completed;
        task.completedAt = completed ? new Date().toISOString() : null;
      }
      return task;
    })
    .filter(Boolean);

  res.json(updated);
});

// ============================================
// ACTIVITIES ENDPOINTS (Campaign Sync)
// ============================================

const activities = new Map();

// Create activity
app.post('/activities', async (req, res) => {
  const { type, subject, body, direction, ownerId, occurredAt, contactId, companyId, metadata } = req.body;

  const activity = {
    id: uuidv4(),
    type,        // EMAIL, LINKEDIN, LINKEDIN_CONNECTION, LINKEDIN_INMAIL
    subject,
    body,
    direction,   // INBOUND or OUTBOUND
    ownerId,
    occurredAt: occurredAt || new Date().toISOString(),
    contactId,
    companyId,
    metadata,
    createdAt: new Date().toISOString(),
  };

  activities.set(activity.id, activity);
  res.status(201).json(activity);
});

// ============================================
// SYNC & UPDATE ENDPOINTS
// ============================================

// Sync contacts (find existing)
app.post('/contacts/sync', async (req, res) => {
  const { contacts: contactsToSync } = req.body;
  const results = [];

  for (const contact of contactsToSync) {
    // Match by email (your CRM decides matching logic)
    let existing = null;
    for (const [id, c] of contacts) {
      if (c.email?.toLowerCase() === contact.email?.toLowerCase()) {
        existing = { id, ...c };
        break;
      }
    }

    if (existing) {
      results.push({
        externalId: contact.externalId,
        crmId: existing.id,
        properties: {
          // Return any properties you want synced back to Enginy
          lastActivityDate: existing.updatedAt,
        }
      });
    }
  }

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

// Update contacts
app.put('/contacts', async (req, res) => {
  const { contacts: contactsToUpdate } = req.body;
  const results = [];

  for (const contact of contactsToUpdate) {
    const existing = contacts.get(contact.crmId);

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

    // Update all fields (including engagement fields)
    const { crmId, ...fieldsToUpdate } = contact;
    contacts.set(crmId, { ...existing, ...fieldsToUpdate, updatedAt: new Date().toISOString() });

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

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

// Sync companies (find existing)
app.post('/companies/sync', async (req, res) => {
  const { companies: companiesToSync } = req.body;
  const results = [];

  for (const company of companiesToSync) {
    // Match by domain (normalize first)
    const normalizedDomain = company.domain?.toLowerCase().replace(/^www\./, '');
    let existing = null;
    for (const [id, c] of companies) {
      if (c.domain?.toLowerCase().replace(/^www\./, '') === normalizedDomain) {
        existing = { id, ...c };
        break;
      }
    }

    if (existing) {
      results.push({
        externalId: company.externalId,
        crmId: existing.id,
        properties: {
          lastActivityDate: existing.updatedAt,
        }
      });
    }
  }

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

// Update companies
app.put('/companies', async (req, res) => {
  const { companies: companiesToUpdate } = req.body;
  const results = [];

  for (const company of companiesToUpdate) {
    const existing = companies.get(company.crmId);

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

    // Update all fields (including engagement fields)
    const { crmId, ...fieldsToUpdate } = company;
    companies.set(crmId, { ...existing, ...fieldsToUpdate, updatedAt: new Date().toISOString() });

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

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

app.listen(3000, () => console.log('Custom CRM API running on port 3000'));

Minimal Implementation

If you want to get started quickly, here’s a minimal implementation with just the export endpoints:
Node.js (Minimal Export)
const express = require('express')
const { v4: uuidv4 } = require('uuid')

const app = express()
app.use(express.json())

const API_KEY = process.env.API_KEY
const contacts = new Map()
const companies = new Map()

// Auth middleware
app.use((req, res, next) => {
  if (req.headers['x-api-key'] !== API_KEY) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  next()
})

// Health
app.get('/health', (req, res) => res.json({ status: 'ok' }))

// Create contacts
app.post('/contacts', (req, res) => {
  const results = req.body.contacts.map((contact) => {
    const crmId = uuidv4()
    contacts.set(crmId, { ...contact, id: crmId })
    return { externalId: contact.externalId, crmId }
  })
  res.status(201).json({ results })
})

// Create companies
app.post('/companies', (req, res) => {
  const results = req.body.companies.map((company) => {
    const crmId = uuidv4()
    companies.set(crmId, { ...company, id: crmId })
    return { externalId: company.externalId, crmId }
  })
  res.status(201).json({ results })
})

// Create associations
app.post('/associations', (req, res) => {
  let created = 0
  for (const { contactCRMId, companyCRMId } of req.body.associations) {
    const contact = contacts.get(contactCRMId)
    if (contact) {
      contact.companyCRMId = companyCRMId
      created++
    }
  }
  res.json({ success: true, created })
})

app.listen(3000, () => console.log('Server running on port 3000'))

Testing Checklist

Before connecting to Enginy, ensure all these tests pass:
curl -X GET https://your-api.com/health \
  -H "X-API-Key: your-secret-key"
Expected: 200 OK with {"status": "ok"}
curl -X GET https://your-api.com/users \
  -H "X-API-Key: your-secret-key"
Expected: 200 OK with array of users:
[
  {"id": "user-1", "name": "John Smith", "email": "[email protected]"},
  {"id": "user-2", "name": "Jane Doe", "email": "[email protected]"}
]
Note: This endpoint is optional. If you don’t want owner selection, return 404 Not Found.
curl -X POST https://your-api.com/contacts \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "externalId": "123",
        "email": "[email protected]",
        "firstName": "Test",
        "lastName": "User"
      }
    ]
  }'
Expected: 201 Created with {"results": [{"externalId": "123", "crmId": "..."}]}
curl -X POST https://your-api.com/companies \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": [
      {
        "externalId": "456",
        "name": "Test Company",
        "domain": "test.com"
      }
    ]
  }'
Expected: 201 Created with {"results": [{"externalId": "456", "crmId": "..."}]}
curl -X POST https://your-api.com/associations \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "associations": [
      {
        "contactCRMId": "CONTACT_CRM_ID",
        "companyCRMId": "COMPANY_CRM_ID"
      }
    ]
  }'
Expected: 200 OK with {"success": true, "created": 1}
curl -X POST https://your-api.com/tasks \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Test task"}'
Expected: 201 Created with task object containing id
curl -X POST https://your-api.com/activities \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"type": "EMAIL", "subject": "Test activity"}'
Expected: 201 Created with activity object containing id
curl -X POST https://your-api.com/activities \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"type": "LINKEDIN_INMAIL", "subject": "Test LinkedIn", "direction": "INBOUND"}'
Expected: 201 Created with activity object containing id
curl -X POST https://your-api.com/tasks/batch \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["TASK_ID_1", "TASK_ID_2"]}'
Expected: 200 OK with array of tasks
curl -X PATCH https://your-api.com/tasks/batch \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["TASK_ID_1"], "completed": true}'
Expected: 200 OK with array of updated tasks
curl -X PUT https://your-api.com/contacts \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "crmId": "CONTACT_CRM_ID",
        "last_engaged_at": "2024-12-31T10:00:00Z"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"crmId": "...", "success": true}]}
curl -X PUT https://your-api.com/companies \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": [
      {
        "crmId": "COMPANY_CRM_ID",
        "last_engaged_at": "2024-12-31T10:00:00Z"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"crmId": "...", "success": true}]}
curl -X POST https://your-api.com/contacts/sync \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "externalId": "123",
        "email": "[email protected]",
        "firstName": "Existing",
        "lastName": "Contact"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"externalId": "123", "crmId": "...", "properties": {...}}]} Note: Only returns contacts that exist in your CRM
curl -X PUT https://your-api.com/contacts \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "crmId": "YOUR_CRM_CONTACT_ID",
        "email": "[email protected]",
        "firstName": "Updated",
        "lastName": "Name"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"crmId": "...", "success": true}]}
curl -X POST https://your-api.com/companies/sync \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": [
      {
        "externalId": "456",
        "name": "Existing Corp",
        "domain": "existing.com"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"externalId": "456", "crmId": "...", "properties": {...}}]} Note: Only returns companies that exist in your CRM
curl -X PUT https://your-api.com/companies \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": [
      {
        "crmId": "YOUR_CRM_COMPANY_ID",
        "name": "Updated Corp Name",
        "employeeCount": 100
      }
    ]
  }'
Expected: 200 OK with {"results": [{"crmId": "...", "success": true}]}
curl -X POST https://your-api.com/activities \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "LINKEDIN",
    "subject": "[1/3] [FROM Acme Corp] John Smith",
    "body": "Hi, I noticed your company is expanding...",
    "direction": "OUTBOUND",
    "contactId": "YOUR_CONTACT_CRM_ID",
    "metadata": {
      "messageId": "msg-123",
      "sequenceIndex": 1,
      "sequenceMessageCount": 3
    }
  }'
Expected: 201 Created with activity object containing id
curl -X POST https://your-api.com/activities \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "EMAIL",
    "subject": "[1/3] Re: Partnership Opportunity",
    "body": "<div>Hi Jane, Thanks for your interest...</div>",
    "direction": "OUTBOUND",
    "contactId": "YOUR_CONTACT_CRM_ID",
    "occurredAt": "2024-12-31T10:00:00Z"
  }'
Expected: 201 Created with activity object containing id
curl -X PUT https://your-api.com/contacts \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "crmId": "YOUR_CRM_CONTACT_ID",
        "genesy_engagement_status": "Message Replied (2/3) - LINKEDIN",
        "genesy_sequence_status": "Replied",
        "genesy_last_activity": "2024-12-31T10:00:00Z"
      }
    ]
  }'
Expected: 200 OK with {"results": [{"crmId": "...", "success": true}]} Note: Your CRM should store these custom engagement fields

Next Steps

Connect Your CRM

Once your implementation is complete, connect your CRM to Enginy