Quick Navigation
Export (Create)
Create new contacts and companies
Sync & Update
Sync existing records and push updates
Full Implementation
Complete working examples
Testing
Verify your implementation
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.).
Contacts Sync
Match contacts by email, LinkedIn URL, or phone. Detailed examples and matching strategies.
Companies Sync
Match companies by domain, name, or LinkedIn URL. Includes domain normalization examples.
Quick Reference
| Endpoint | Method | Purpose |
|---|---|---|
/contacts/sync | POST | Find existing contacts, return crmId + properties |
/contacts | PUT | Update contacts that have crmId |
/companies/sync | POST | Find existing companies, return crmId + properties |
/companies | PUT | Update companies that have crmId |
Matching Strategy Summary
| Entity | Primary Match | Fallback | Normalization |
|---|---|---|---|
| Contacts | email (lowercase) | linkedinUrl | Remove trailing slashes |
| Companies | domain | linkedinUrl, name | Strip 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: 'john@example.com' }],
['user-2', { id: 'user-2', name: 'Jane Doe', email: 'jane@example.com' }],
]);
// 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'));
from fastapi import FastAPI, Header, HTTPException, Depends
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, List, Dict, Any
import uuid
app = FastAPI()
# In-memory storage (replace with your database)
contacts: Dict[str, dict] = {}
companies: Dict[str, dict] = {}
tasks: Dict[str, dict] = {}
activities: Dict[str, dict] = {}
users: Dict[str, dict] = {
"user-1": {"id": "user-1", "name": "John Smith", "email": "john@example.com"},
"user-2": {"id": "user-2", "name": "Jane Doe", "email": "jane@example.com"},
}
def validate_api_key(x_api_key: str = Header()):
if x_api_key != "your-secret-key":
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
# ============================================
# CONTACTS MODELS & ENDPOINTS
# ============================================
class Contact(BaseModel):
externalId: str
email: Optional[str] = None
firstName: Optional[str] = None
lastName: Optional[str] = None
phone: Optional[str] = None
company: Optional[str] = None
title: Optional[str] = None
linkedinUrl: Optional[str] = None
# Accept any additional fields
class Config:
extra = "allow"
class ContactsCreate(BaseModel):
contacts: List[Contact]
class ContactResult(BaseModel):
externalId: str
crmId: str
@app.post("/contacts")
async def create_contacts(data: ContactsCreate, api_key: str = Depends(validate_api_key)):
results = []
errors = []
for contact in data.contacts:
try:
crm_id = str(uuid.uuid4())
contacts[crm_id] = {
"id": crm_id,
**contact.dict(),
"createdAt": datetime.utcnow().isoformat(),
}
results.append({"externalId": contact.externalId, "crmId": crm_id})
except Exception as e:
errors.append({"externalId": contact.externalId, "error": str(e)})
response = {"results": results}
if errors:
response["errors"] = errors
return response
# ============================================
# COMPANIES MODELS & ENDPOINTS
# ============================================
class Company(BaseModel):
externalId: str
name: Optional[str] = None
domain: Optional[str] = None
industry: Optional[str] = None
numberOfEmployees: Optional[int] = None
linkedinUrl: Optional[str] = None
class Config:
extra = "allow"
class CompaniesCreate(BaseModel):
companies: List[Company]
@app.post("/companies")
async def create_companies(data: CompaniesCreate, api_key: str = Depends(validate_api_key)):
results = []
errors = []
for company in data.companies:
try:
crm_id = str(uuid.uuid4())
companies[crm_id] = {
"id": crm_id,
**company.dict(),
"createdAt": datetime.utcnow().isoformat(),
}
results.append({"externalId": company.externalId, "crmId": crm_id})
except Exception as e:
errors.append({"externalId": company.externalId, "error": str(e)})
response = {"results": results}
if errors:
response["errors"] = errors
return response
# ============================================
# ASSOCIATIONS MODELS & ENDPOINTS
# ============================================
class Association(BaseModel):
contactCRMId: str
companyCRMId: str
class AssociationsCreate(BaseModel):
associations: List[Association]
@app.post("/associations")
async def create_associations(data: AssociationsCreate, api_key: str = Depends(validate_api_key)):
created = 0
errors = []
for assoc in data.associations:
contact = contacts.get(assoc.contactCRMId)
company = companies.get(assoc.companyCRMId)
if not contact:
errors.append({
"contactCRMId": assoc.contactCRMId,
"companyCRMId": assoc.companyCRMId,
"error": "Contact not found",
})
continue
if not company:
errors.append({
"contactCRMId": assoc.contactCRMId,
"companyCRMId": assoc.companyCRMId,
"error": "Company not found",
})
continue
# Link contact to company
contact["companyCRMId"] = assoc.companyCRMId
contact["companyName"] = company.get("name")
created += 1
response = {"success": True, "created": created}
if errors:
response["errors"] = errors
return response
# ============================================
# TASKS MODELS & ENDPOINTS
# ============================================
class TaskCreate(BaseModel):
subject: str
description: Optional[str] = None
type: Optional[str] = None
ownerId: Optional[str] = None
dueDate: Optional[str] = None
contactId: Optional[str] = None
companyId: Optional[str] = None
class TaskUpdate(BaseModel):
completed: Optional[bool] = None
class TaskBatchGet(BaseModel):
ids: List[str]
class TaskBatchUpdate(BaseModel):
ids: List[str]
completed: bool
@app.get("/health")
async def health(api_key: str = Depends(validate_api_key)):
return {"status": "ok"}
# ============================================
# USERS ENDPOINT (Optional - for owner assignment)
# ============================================
@app.get("/users")
async def get_users(api_key: str = Depends(validate_api_key)):
"""Optional endpoint - return 404 to disable owner selection"""
return list(users.values())
@app.post("/tasks")
async def create_task(task: TaskCreate, api_key: str = Depends(validate_api_key)):
task_id = str(uuid.uuid4())
tasks[task_id] = {
"id": task_id,
**task.dict(),
"completed": False,
"completedAt": None,
}
return tasks[task_id]
@app.get("/tasks/{task_id}")
async def get_task(task_id: str, api_key: str = Depends(validate_api_key)):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
return tasks[task_id]
@app.post("/tasks/batch")
async def get_tasks_batch(batch: TaskBatchGet, api_key: str = Depends(validate_api_key)):
return [tasks[tid] for tid in batch.ids if tid in tasks]
@app.patch("/tasks/{task_id}")
async def update_task(task_id: str, update: TaskUpdate, api_key: str = Depends(validate_api_key)):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
if update.completed is not None:
tasks[task_id]["completed"] = update.completed
tasks[task_id]["completedAt"] = (
datetime.utcnow().isoformat() if update.completed else None
)
return tasks[task_id]
@app.patch("/tasks/batch")
async def complete_tasks_batch(batch: TaskBatchUpdate, api_key: str = Depends(validate_api_key)):
updated = []
for tid in batch.ids:
if tid in tasks:
tasks[tid]["completed"] = batch.completed
tasks[tid]["completedAt"] = (
datetime.utcnow().isoformat() if batch.completed else None
)
updated.append(tasks[tid])
return updated
# ============================================
# ACTIVITIES ENDPOINTS (Campaign Sync)
# ============================================
activities: Dict[str, dict] = {}
class ActivityCreate(BaseModel):
type: str # EMAIL, LINKEDIN, LINKEDIN_CONNECTION, LINKEDIN_INMAIL
subject: Optional[str] = None
body: Optional[str] = None
direction: Optional[str] = None # INBOUND or OUTBOUND
ownerId: Optional[str] = None
occurredAt: Optional[str] = None
contactId: Optional[str] = None
companyId: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
@app.post("/activities")
async def create_activity(activity: ActivityCreate, api_key: str = Depends(validate_api_key)):
activity_id = str(uuid.uuid4())
activities[activity_id] = {
"id": activity_id,
**activity.dict(),
"createdAt": datetime.utcnow().isoformat(),
}
return activities[activity_id]
# ============================================
# SYNC & UPDATE ENDPOINTS
# ============================================
class ContactSync(BaseModel):
externalId: str
email: Optional[str] = None
linkedinUrl: Optional[str] = None
class Config:
extra = "allow"
class ContactsSync(BaseModel):
contacts: List[ContactSync]
class ContactUpdate(BaseModel):
crmId: str
class Config:
extra = "allow"
class ContactsUpdate(BaseModel):
contacts: List[ContactUpdate]
@app.post("/contacts/sync")
async def sync_contacts(data: ContactsSync, api_key: str = Depends(validate_api_key)):
results = []
for contact in data.contacts:
# Match by email (your CRM decides matching logic)
existing = None
for cid, c in contacts.items():
if c.get("email", "").lower() == (contact.email or "").lower():
existing = {"id": cid, **c}
break
if existing:
results.append({
"externalId": contact.externalId,
"crmId": existing["id"],
"properties": {
"lastActivityDate": existing.get("updatedAt"),
}
})
return {"results": results}
@app.put("/contacts")
async def update_contacts(data: ContactsUpdate, api_key: str = Depends(validate_api_key)):
results = []
for contact in data.contacts:
existing = contacts.get(contact.crmId)
if not existing:
results.append({"crmId": contact.crmId, "success": False, "error": "Contact not found"})
continue
# Update all fields (including engagement fields)
update_data = contact.dict(exclude={"crmId"})
contacts[contact.crmId] = {**existing, **update_data, "updatedAt": datetime.utcnow().isoformat()}
results.append({"crmId": contact.crmId, "success": True})
return {"results": results}
class CompanySync(BaseModel):
externalId: str
domain: Optional[str] = None
class Config:
extra = "allow"
class CompaniesSync(BaseModel):
companies: List[CompanySync]
class CompanyUpdate(BaseModel):
crmId: str
class Config:
extra = "allow"
class CompaniesUpdate(BaseModel):
companies: List[CompanyUpdate]
@app.post("/companies/sync")
async def sync_companies(data: CompaniesSync, api_key: str = Depends(validate_api_key)):
results = []
for company in data.companies:
normalized_domain = (company.domain or "").lower().replace("www.", "")
existing = None
for cid, c in companies.items():
if c.get("domain", "").lower().replace("www.", "") == normalized_domain:
existing = {"id": cid, **c}
break
if existing:
results.append({
"externalId": company.externalId,
"crmId": existing["id"],
"properties": {"lastActivityDate": existing.get("updatedAt")}
})
return {"results": results}
@app.put("/companies")
async def update_companies(data: CompaniesUpdate, api_key: str = Depends(validate_api_key)):
results = []
for company in data.companies:
existing = companies.get(company.crmId)
if not existing:
results.append({"crmId": company.crmId, "success": False, "error": "Company not found"})
continue
update_data = company.dict(exclude={"crmId"})
companies[company.crmId] = {**existing, **update_data, "updatedAt": datetime.utcnow().isoformat()}
results.append({"crmId": company.crmId, "success": True})
return {"results": results}
package main
import (
"encoding/json"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
var (
contacts = make(map[string]map[string]interface{})
companies = make(map[string]map[string]interface{})
tasks = make(map[string]map[string]interface{})
activities = make(map[string]map[string]interface{})
users = map[string]map[string]interface{}{
"user-1": {"id": "user-1", "name": "John Smith", "email": "john@example.com"},
"user-2": {"id": "user-2", "name": "Jane Doe", "email": "jane@example.com"},
}
mu sync.RWMutex
apiKey = "your-secret-key"
)
// ============================================
// REQUEST/RESPONSE TYPES
// ============================================
type ContactsRequest struct {
Contacts []map[string]interface{} `json:"contacts"`
}
type CompaniesRequest struct {
Companies []map[string]interface{} `json:"companies"`
}
type AssociationsRequest struct {
Associations []struct {
ContactCRMId string `json:"contactCRMId"`
CompanyCRMId string `json:"companyCRMId"`
} `json:"associations"`
}
type ResultItem struct {
ExternalId string `json:"externalId"`
CrmId string `json:"crmId"`
}
type ExportResponse struct {
Results []ResultItem `json:"results"`
Errors []map[string]interface{} `json:"errors,omitempty"`
}
type AssociationsResponse struct {
Success bool `json:"success"`
Created int `json:"created"`
Errors []map[string]interface{} `json:"errors,omitempty"`
}
// ============================================
// MIDDLEWARE
// ============================================
func validateAPIKey(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") != apiKey {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid API key"})
return
}
w.Header().Set("Content-Type", "application/json")
next(w, r)
}
}
// ============================================
// HANDLERS
// ============================================
func healthHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// Users handler (optional - return 404 to disable owner selection)
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
mu.RLock()
defer mu.RUnlock()
var usersList []map[string]interface{}
for _, user := range users {
usersList = append(usersList, user)
}
json.NewEncoder(w).Encode(usersList)
}
func createContactsHandler(w http.ResponseWriter, r *http.Request) {
var req ContactsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
mu.Lock()
defer mu.Unlock()
var results []ResultItem
for _, contact := range req.Contacts {
crmId := uuid.New().String()
contact["id"] = crmId
contact["createdAt"] = time.Now().UTC().Format(time.RFC3339)
contacts[crmId] = contact
externalId, _ := contact["externalId"].(string)
results = append(results, ResultItem{ExternalId: externalId, CrmId: crmId})
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(ExportResponse{Results: results})
}
func createCompaniesHandler(w http.ResponseWriter, r *http.Request) {
var req CompaniesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
mu.Lock()
defer mu.Unlock()
var results []ResultItem
for _, company := range req.Companies {
crmId := uuid.New().String()
company["id"] = crmId
company["createdAt"] = time.Now().UTC().Format(time.RFC3339)
companies[crmId] = company
externalId, _ := company["externalId"].(string)
results = append(results, ResultItem{ExternalId: externalId, CrmId: crmId})
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(ExportResponse{Results: results})
}
func createAssociationsHandler(w http.ResponseWriter, r *http.Request) {
var req AssociationsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
mu.Lock()
defer mu.Unlock()
created := 0
var errors []map[string]interface{}
for _, assoc := range req.Associations {
contact, contactExists := contacts[assoc.ContactCRMId]
company, companyExists := companies[assoc.CompanyCRMId]
if !contactExists {
errors = append(errors, map[string]interface{}{
"contactCRMId": assoc.ContactCRMId,
"companyCRMId": assoc.CompanyCRMId,
"error": "Contact not found",
})
continue
}
if !companyExists {
errors = append(errors, map[string]interface{}{
"contactCRMId": assoc.ContactCRMId,
"companyCRMId": assoc.CompanyCRMId,
"error": "Company not found",
})
continue
}
contact["companyCRMId"] = assoc.CompanyCRMId
contact["companyName"] = company["name"]
created++
}
response := AssociationsResponse{Success: true, Created: created}
if len(errors) > 0 {
response.Errors = errors
}
json.NewEncoder(w).Encode(response)
}
// ============================================
// ACTIVITIES & SYNC HANDLERS
// ============================================
var activities = make(map[string]map[string]interface{})
func createActivityHandler(w http.ResponseWriter, r *http.Request) {
var activity map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&activity); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
mu.Lock()
defer mu.Unlock()
activityId := uuid.New().String()
activity["id"] = activityId
activity["createdAt"] = time.Now().UTC().Format(time.RFC3339)
activities[activityId] = activity
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(activity)
}
func syncContactsHandler(w http.ResponseWriter, r *http.Request) {
var req ContactsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
mu.RLock()
defer mu.RUnlock()
var results []map[string]interface{}
for _, contact := range req.Contacts {
email, _ := contact["email"].(string)
for id, c := range contacts {
if existingEmail, _ := c["email"].(string); strings.EqualFold(existingEmail, email) {
results = append(results, map[string]interface{}{
"externalId": contact["externalId"],
"crmId": id,
"properties": map[string]interface{}{"lastActivityDate": c["updatedAt"]},
})
break
}
}
}
json.NewEncoder(w).Encode(map[string]interface{}{"results": results})
}
func updateContactsHandler(w http.ResponseWriter, r *http.Request) {
var req ContactsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
var results []map[string]interface{}
for _, contact := range req.Contacts {
crmId, _ := contact["crmId"].(string)
if existing, ok := contacts[crmId]; ok {
for k, v := range contact {
existing[k] = v
}
existing["updatedAt"] = time.Now().UTC().Format(time.RFC3339)
results = append(results, map[string]interface{}{"crmId": crmId, "success": true})
} else {
results = append(results, map[string]interface{}{"crmId": crmId, "success": false, "error": "Contact not found"})
}
}
json.NewEncoder(w).Encode(map[string]interface{}{"results": results})
}
func main() {
mux := http.NewServeMux()
// Health
mux.HandleFunc("GET /health", validateAPIKey(healthHandler))
// Users (optional)
mux.HandleFunc("GET /users", validateAPIKey(getUsersHandler))
// Export endpoints
mux.HandleFunc("POST /contacts", validateAPIKey(createContactsHandler))
mux.HandleFunc("POST /companies", validateAPIKey(createCompaniesHandler))
mux.HandleFunc("POST /associations", validateAPIKey(createAssociationsHandler))
// Sync & Update endpoints
mux.HandleFunc("POST /contacts/sync", validateAPIKey(syncContactsHandler))
mux.HandleFunc("PUT /contacts", validateAPIKey(updateContactsHandler))
// Activities endpoint
mux.HandleFunc("POST /activities", validateAPIKey(createActivityHandler))
// Task endpoints would go here...
http.ListenAndServe(":3000", mux)
}
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:Health Endpoint
Health Endpoint
curl -X GET https://your-api.com/health \
-H "X-API-Key: your-secret-key"
200 OK with {"status": "ok"}Get Users (Optional)
Get Users (Optional)
curl -X GET https://your-api.com/users \
-H "X-API-Key: your-secret-key"
200 OK with array of users:[
{"id": "user-1", "name": "John Smith", "email": "john@example.com"},
{"id": "user-2", "name": "Jane Doe", "email": "jane@example.com"}
]
404 Not Found.Create Contacts
Create Contacts
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": "test@example.com",
"firstName": "Test",
"lastName": "User"
}
]
}'
201 Created with {"results": [{"externalId": "123", "crmId": "..."}]}Create Companies
Create Companies
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"
}
]
}'
201 Created with {"results": [{"externalId": "456", "crmId": "..."}]}Create Associations
Create Associations
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"
}
]
}'
200 OK with {"success": true, "created": 1}Create Task
Create Task
curl -X POST https://your-api.com/tasks \
-H "X-API-Key: your-secret-key" \
-H "Content-Type: application/json" \
-d '{"subject": "Test task"}'
201 Created with task object containing idCreate Activity
Create Activity
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"}'
201 Created with activity object containing idCreate LinkedIn Activity
Create LinkedIn Activity
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"}'
201 Created with activity object containing idGet Tasks Batch
Get Tasks Batch
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"]}'
200 OK with array of tasksComplete Tasks Batch
Complete Tasks Batch
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}'
200 OK with array of updated tasksUpdate Engagement Fields
Update Engagement Fields
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"
}
]
}'
200 OK with {"results": [{"crmId": "...", "success": true}]}Update Engagement Fields (Company)
Update Engagement Fields (Company)
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"
}
]
}'
200 OK with {"results": [{"crmId": "...", "success": true}]}Sync Contacts
Sync Contacts
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": "existing@example.com",
"firstName": "Existing",
"lastName": "Contact"
}
]
}'
200 OK with {"results": [{"externalId": "123", "crmId": "...", "properties": {...}}]}
Note: Only returns contacts that exist in your CRMUpdate Contacts
Update Contacts
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": "updated@example.com",
"firstName": "Updated",
"lastName": "Name"
}
]
}'
200 OK with {"results": [{"crmId": "...", "success": true}]}Sync Companies
Sync Companies
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"
}
]
}'
200 OK with {"results": [{"externalId": "456", "crmId": "...", "properties": {...}}]}
Note: Only returns companies that exist in your CRMUpdate Companies
Update Companies
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
}
]
}'
200 OK with {"results": [{"crmId": "...", "success": true}]}Create Activity (LinkedIn)
Create Activity (LinkedIn)
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
}
}'
201 Created with activity object containing idCreate Activity (Email)
Create Activity (Email)
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"
}'
201 Created with activity object containing idUpdate Contact with Engagement Fields
Update Contact with Engagement Fields
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"
}
]
}'
200 OK with {"results": [{"crmId": "...", "success": true}]}
Note: Your CRM should store these custom engagement fieldsNext Steps
Connect Your CRM
Once your implementation is complete, connect your CRM to Enginy