> ## 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.

# Examples

> Complete implementation examples for Custom CRM integration

## Quick Navigation

<CardGroup cols={2}>
  <Card title="Export (Create)" icon="upload" href="#export-create-endpoints">
    Create new contacts and companies
  </Card>

  <Card title="Sync & Update" icon="rotate" href="#sync-update-endpoints">
    Sync existing records and push updates
  </Card>

  <Card title="Full Implementation" icon="code" href="#complete-implementation-examples">
    Complete working examples
  </Card>

  <Card title="Testing" icon="flask" href="#testing-checklist">
    Verify your implementation
  </Card>
</CardGroup>

***

## 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.

<Info>
  **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.).
</Info>

<CardGroup cols={2}>
  <Card title="Contacts Sync" icon="user" href="/api-reference/custom-crm/contacts-sync">
    Match contacts by email, LinkedIn URL, or phone. Detailed examples and matching strategies.
  </Card>

  <Card title="Companies Sync" icon="building" href="/api-reference/custom-crm/companies-sync">
    Match companies by domain, name, or LinkedIn URL. Includes domain normalization examples.
  </Card>
</CardGroup>

### 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.

<Card title="GitHub Repository" icon="github" href="https://github.com/Genesy-AI/custom-crm-example">
  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.
</Card>

<Info>
  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)
</Info>

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  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'));
  ```

  ```python Python (FastAPI) theme={null}
  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}
  ```

  ```go Go theme={null}
  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)
  }
  ```
</CodeGroup>

***

## Minimal Implementation

If you want to get started quickly, here's a minimal implementation with just the export endpoints:

```javascript Node.js (Minimal Export) theme={null}
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:

<AccordionGroup>
  <Accordion title="Health Endpoint">
    ```bash theme={null}
    curl -X GET https://your-api.com/health \
      -H "X-API-Key: your-secret-key"
    ```

    Expected: `200 OK` with `{"status": "ok"}`
  </Accordion>

  <Accordion title="Get Users (Optional)">
    ```bash theme={null}
    curl -X GET https://your-api.com/users \
      -H "X-API-Key: your-secret-key"
    ```

    Expected: `200 OK` with array of users:

    ```json theme={null}
    [
      {"id": "user-1", "name": "John Smith", "email": "john@example.com"},
      {"id": "user-2", "name": "Jane Doe", "email": "jane@example.com"}
    ]
    ```

    **Note**: This endpoint is optional. If you don't want owner selection, return `404 Not Found`.
  </Accordion>

  <Accordion title="Create Contacts">
    ```bash theme={null}
    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"
          }
        ]
      }'
    ```

    Expected: `201 Created` with `{"results": [{"externalId": "123", "crmId": "..."}]}`
  </Accordion>

  <Accordion title="Create Companies">
    ```bash theme={null}
    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": "..."}]}`
  </Accordion>

  <Accordion title="Create Associations">
    ```bash theme={null}
    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}`
  </Accordion>

  <Accordion title="Create Task">
    ```bash theme={null}
    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`
  </Accordion>

  <Accordion title="Create Activity">
    ```bash theme={null}
    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`
  </Accordion>

  <Accordion title="Create LinkedIn Activity">
    ```bash theme={null}
    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`
  </Accordion>

  <Accordion title="Get Tasks Batch">
    ```bash theme={null}
    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
  </Accordion>

  <Accordion title="Complete Tasks Batch">
    ```bash theme={null}
    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
  </Accordion>

  <Accordion title="Update Engagement Fields">
    ```bash theme={null}
    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}]}`
  </Accordion>

  <Accordion title="Update Engagement Fields (Company)">
    ```bash theme={null}
    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}]}`
  </Accordion>

  <Accordion title="Sync Contacts">
    ```bash theme={null}
    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"
          }
        ]
      }'
    ```

    Expected: `200 OK` with `{"results": [{"externalId": "123", "crmId": "...", "properties": {...}}]}`
    Note: Only returns contacts that exist in your CRM
  </Accordion>

  <Accordion title="Update Contacts">
    ```bash theme={null}
    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"
          }
        ]
      }'
    ```

    Expected: `200 OK` with `{"results": [{"crmId": "...", "success": true}]}`
  </Accordion>

  <Accordion title="Sync Companies">
    ```bash theme={null}
    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
  </Accordion>

  <Accordion title="Update Companies">
    ```bash theme={null}
    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}]}`
  </Accordion>

  <Accordion title="Create Activity (LinkedIn)">
    ```bash theme={null}
    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`
  </Accordion>

  <Accordion title="Create Activity (Email)">
    ```bash theme={null}
    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`
  </Accordion>

  <Accordion title="Update Contact with Engagement Fields">
    ```bash theme={null}
    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
  </Accordion>
</AccordionGroup>

***

## Next Steps

<Card title="Connect Your CRM" icon="plug" href="/essentials/custom-crm#connecting-your-crm">
  Once your implementation is complete, connect your CRM to Enginy
</Card>
