Claude Code Plugins

Community-maintained marketplace

Feedback

Bi-directional synchronization between RealGeeks CRM and conversational AI systems. Use when implementing webhook handlers, creating sync pipelines, handling lead deduplication, mapping activity data, or building real-time CRM integration workflows. Includes webhook security, retry logic, and data transformation patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name realgeeks-sync
description Bi-directional synchronization between RealGeeks CRM and conversational AI systems. Use when implementing webhook handlers, creating sync pipelines, handling lead deduplication, mapping activity data, or building real-time CRM integration workflows. Includes webhook security, retry logic, and data transformation patterns.
allowed-tools Read, Write, Edit, Bash, Glob, Grep

RealGeeks Sync Skill

Expert guidance for implementing bi-directional synchronization between RealGeeks CRM and Next Level Real Estate AI calling platform. This skill provides patterns, best practices, and implementation strategies for seamless CRM integration.

When to Use This Skill

Invoke this skill when you need to:

  • ✅ Implement RealGeeks webhook handlers
  • ✅ Design lead sync workflows
  • ✅ Handle lead deduplication
  • ✅ Map activities between systems
  • ✅ Build real-time CRM integration
  • ✅ Configure webhook security (HMAC)
  • ✅ Implement retry and error handling
  • ✅ Transform data between formats

Sync Architecture Patterns

Pattern 1: Event-Driven Sync (Recommended)

Use When: Real-time updates needed, leads must flow immediately

RealGeeks → Webhook → Your App → Process → ElevenLabs
    ↓
  Kafka/Queue
    ↓
  Workers

Benefits:

  • Instant lead processing (<5 minutes)
  • Scalable (queue handles bursts)
  • Reliable (retry failed webhooks)
  • Audit trail (all events logged)

Implementation:

// Webhook receiver
app.post('/webhooks/realgeeks', async (req, res) => {
  try {
    // 1. Validate signature immediately
    const signature = req.headers['x-lead-router-signature']
    if (!validateSignature(req.rawBody, signature, SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    // 2. Return 200 OK quickly
    res.status(200).json({ received: true })

    // 3. Process asynchronously
    const action = req.headers['x-lead-router-action']
    const messageId = req.headers['x-lead-router-message-id']

    await queue.publish('realgeeks.webhooks', {
      action,
      messageId,
      payload: req.body,
      receivedAt: new Date()
    })

  } catch (error) {
    console.error('Webhook error:', error)
    res.status(500).json({ error: 'Internal error' })
  }
})

// Worker processing
queue.subscribe('realgeeks.webhooks', async (message) => {
  const { action, messageId, payload } = message

  // Check if already processed (idempotency)
  if (await isProcessed(messageId)) {
    console.log(`Message ${messageId} already processed`)
    return
  }

  switch (action) {
    case 'created':
      await handleLeadCreated(payload)
      break
    case 'updated':
      await handleLeadUpdated(payload)
      break
    case 'activity_added':
      await handleActivityAdded(payload)
      break
  }

  // Mark as processed
  await markProcessed(messageId)
})

Pattern 2: Batch Sync

Use When: Historical data import, nightly reconciliation

Cron Job → Fetch from RealGeeks → Transform → Load to DB

Implementation:

// Nightly sync job
async function syncLeadsFromRealGeeks() {
  const leads = await fetchAllLeads(siteUuid)

  for (const lead of leads) {
    // Check if exists in our system
    const existing = await database.leads.findByEmail(lead.email)

    if (existing) {
      // Update if changed
      if (hasChanged(existing, lead)) {
        await database.leads.update(existing.id, transformLead(lead))
      }
    } else {
      // Create new
      await database.leads.insert(transformLead(lead))
    }
  }
}

// Run daily at 2 AM
cron.schedule('0 2 * * *', syncLeadsFromRealGeeks)

Webhook Security Implementation

HMAC-SHA256 Signature Validation

Critical: Always validate webhook signatures to prevent spoofing

import crypto from 'crypto'

function validateWebhookSignature(
  body: string | Buffer,
  signature: string,
  secret: string
): boolean {
  try {
    // Ensure body is string
    const bodyString = typeof body === 'string' ? body : body.toString()

    // Create HMAC with SHA256
    const hmac = crypto.createHmac('sha256', secret)
    hmac.update(bodyString)
    const calculatedSignature = hmac.digest('hex')

    // Constant-time comparison (prevents timing attacks)
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(calculatedSignature)
    )
  } catch (error) {
    console.error('Signature validation error:', error)
    return false
  }
}

// Usage in Express
app.use('/webhooks/realgeeks', express.raw({ type: 'application/json' }))

app.post('/webhooks/realgeeks', (req, res) => {
  const signature = req.headers['x-lead-router-signature']

  if (!validateWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // Signature valid, process webhook
  // ...
})

Webhook Headers Reference

interface RealGeeksWebhookHeaders {
  'x-lead-router-action': 'created' | 'updated' | 'activity_added' | 'user_updated'
  'x-lead-router-message-id': string  // Unique per message
  'x-lead-router-signature': string   // HMAC-SHA256 hex
  'user-agent': 'RealGeeks-LeadRouter/1.0'
}

Lead Deduplication Strategies

Strategy 1: Email-Based Deduplication

Assumption: Email is most reliable unique identifier

async function findOrCreateLead(leadData: any) {
  // 1. Try email first
  if (leadData.email) {
    const existing = await database.leads.findByEmail(leadData.email)
    if (existing) {
      console.log(`Lead found by email: ${existing.id}`)
      return { lead: existing, created: false }
    }
  }

  // 2. Try phone if no email match
  if (leadData.phone) {
    const existing = await database.leads.findByPhone(normalizePhone(leadData.phone))
    if (existing) {
      console.log(`Lead found by phone: ${existing.id}`)
      return { lead: existing, created: false }
    }
  }

  // 3. Try name + address
  if (leadData.first_name && leadData.last_name && leadData.address) {
    const existing = await database.leads.findByNameAddress(
      leadData.first_name,
      leadData.last_name,
      leadData.address
    )
    if (existing) {
      console.log(`Lead found by name+address: ${existing.id}`)
      return { lead: existing, created: false }
    }
  }

  // 4. No match, create new
  const newLead = await database.leads.create(leadData)
  console.log(`New lead created: ${newLead.id}`)
  return { lead: newLead, created: true }
}

Strategy 2: Fuzzy Matching

Use When: Dealing with typos, formatting differences

import { levenshtein } from 'fast-levenshtein'

function isFuzzyMatch(str1: string, str2: string, threshold = 0.8): boolean {
  const distance = levenshtein.get(str1.toLowerCase(), str2.toLowerCase())
  const maxLen = Math.max(str1.length, str2.length)
  const similarity = 1 - (distance / maxLen)
  return similarity >= threshold
}

async function findLeadFuzzy(leadData: any) {
  const candidates = await database.leads.search({
    first_name: leadData.first_name,
    last_name: leadData.last_name
  })

  for (const candidate of candidates) {
    // Check email similarity
    if (leadData.email && candidate.email) {
      if (isFuzzyMatch(leadData.email, candidate.email, 0.9)) {
        return candidate
      }
    }

    // Check phone similarity (after normalization)
    if (leadData.phone && candidate.phone) {
      const phone1 = normalizePhone(leadData.phone)
      const phone2 = normalizePhone(candidate.phone)
      if (phone1 === phone2) {
        return candidate
      }
    }
  }

  return null
}

Activity Mapping

ElevenLabs → RealGeeks Activity Mapping

function mapToRealGeeksActivity(elevenlabsEvent: any) {
  const activityMap = {
    'call_initiated': {
      type: 'called',
      description: (e) => `AI call initiated to ${e.phone}`,
    },
    'call_completed': {
      type: 'called',
      description: (e) => `AI call completed. Duration: ${e.duration}s. Sentiment: ${e.sentiment}. ${e.summary}`,
    },
    'call_failed': {
      type: 'note',
      description: (e) => `AI call failed: ${e.error}`,
    },
    'voicemail_left': {
      type: 'note',
      description: (e) => `Voicemail left: ${e.message}`,
    },
    'opt_out_requested': {
      type: 'opted_out',
      description: (e) => `Lead requested opt-out during AI call`,
    },
    'viewing_scheduled': {
      type: 'tour_requested',
      description: (e) => `Property viewing scheduled for ${e.date}`,
    },
  }

  const mapping = activityMap[elevenlabsEvent.type]
  if (!mapping) {
    console.warn(`Unknown event type: ${elevenlabsEvent.type}`)
    return null
  }

  return {
    type: mapping.type,
    source: 'ElevenLabs AI',
    description: mapping.description(elevenlabsEvent),
    created: elevenlabsEvent.timestamp
  }
}

// Usage
async function logCallToRealGeeks(call: ElevenLabsCall) {
  const activity = mapToRealGeeksActivity({
    type: 'call_completed',
    phone: call.to,
    duration: call.duration,
    sentiment: call.sentiment,
    summary: call.transcript_summary
  })

  if (activity) {
    await clients.realgeeks.addActivities(
      siteUuid,
      call.leadId,
      [activity]
    )
  }
}

RealGeeks → ElevenLabs Context Mapping

function prepareElevenLabsContext(realgeeksLead: any) {
  return {
    leadData: {
      name: `${realgeeksLead.first_name} ${realgeeksLead.last_name}`,
      email: realgeeksLead.email,
      phone: realgeeksLead.phone,
      urgency: realgeeksLead.urgency,
      timeline: realgeeksLead.timeframe,
      role: realgeeksLead.role,
      source: realgeeksLead.source,
    },
    propertyInterests: realgeeksLead.activities
      ?.filter(a => a.type === 'property_viewed')
      .map(a => ({
        address: a.property?.address,
        price: a.property?.list_price,
        beds: a.property?.beds,
        baths: a.property?.baths,
      })) || [],
    previousInteractions: realgeeksLead.activities
      ?.filter(a => ['called', 'contact_emailed'].includes(a.type))
      .map(a => ({
        type: a.type,
        date: a.created,
        description: a.description,
      })) || [],
    strategyRules: {
      isHotLead: realgeeksLead.urgency === 'Hot',
      isBuyer: realgeeksLead.role?.includes('Buyer'),
      isSeller: realgeeksLead.role?.includes('Seller'),
      hasViewed Properties: realgeeksLead.activities?.some(a => a.type === 'property_viewed'),
    },
  }
}

Retry and Error Handling

Webhook Retry Strategy

RealGeeks retries failed webhooks:

  • 1st retry: 10 minutes
  • 2nd retry: 30 minutes
  • 3rd retry: 1 hour
  • 4th-8th retry: 2-4 hours

Your Implementation:

// Track processed messages for idempotency
const processedMessages = new Set()

app.post('/webhooks/realgeeks', async (req, res) => {
  const messageId = req.headers['x-lead-router-message-id']

  // Idempotency check
  if (processedMessages.has(messageId)) {
    console.log(`Message ${messageId} already processed`)
    return res.status(200).json({ status: 'already_processed' })
  }

  try {
    await processWebhook(req.body)

    // Mark as processed
    processedMessages.add(messageId)

    res.status(200).json({ status: 'success' })

  } catch (error) {
    console.error('Webhook processing error:', error)

    // Permanent failure - stop retries
    if (error.code === 'PERMANENT_FAILURE') {
      return res.status(406).json({ error: 'Permanent failure' })
    }

    // Temporary failure - allow retries
    res.status(500).json({ error: 'Temporary failure' })
  }
})

// Clean up old message IDs (after 24 hours)
setInterval(() => {
  const cutoff = Date.now() - (24 * 60 * 60 * 1000)
  // Implement cleanup logic
}, 3600000)  // Every hour

API Retry with Exponential Backoff

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      if (attempt === maxRetries) {
        throw error
      }

      // Exponential backoff: 1s, 2s, 4s, 8s...
      const delay = baseDelay * Math.pow(2, attempt - 1)
      console.log(`Retry attempt ${attempt} after ${delay}ms`)

      await sleep(delay)
    }
  }

  throw new Error('Max retries exceeded')
}

// Usage
await retryWithBackoff(async () => {
  return await clients.realgeeks.createLead(siteUuid, leadData)
}, 3, 1000)

Data Transformation

Phone Number Normalization

function normalizePhone(phone: string): string {
  // Remove all non-digit characters
  const digits = phone.replace(/\D/g, '')

  // US/Canada number (10 digits)
  if (digits.length === 10) {
    return `+1${digits}`
  }

  // Already has country code
  if (digits.length === 11 && digits[0] === '1') {
    return `+${digits}`
  }

  // International
  if (digits.length > 11) {
    return `+${digits}`
  }

  // Invalid
  console.warn(`Invalid phone number: ${phone}`)
  return phone
}

// Comparison
function phonesMatch(phone1: string, phone2: string): boolean {
  return normalizePhone(phone1) === normalizePhone(phone2)
}

Date/Time Handling

// RealGeeks uses ISO 8601
function formatDateForRealGeeks(date: Date): string {
  return date.toISOString()
}

// Parse RealGeeks date
function parseRealGeeksDate(dateString: string): Date {
  return new Date(dateString)
}

// Compare dates
function isRecent(dateString: string, hoursAgo: number): boolean {
  const date = parseRealGeeksDate(dateString)
  const cutoff = Date.now() - (hoursAgo * 60 * 60 * 1000)
  return date.getTime() > cutoff
}

Monitoring and Observability

Key Metrics to Track

interface SyncMetrics {
  // Webhook metrics
  webhooksReceived: number
  webhooksProcessed: number
  webhooksFailed: number
  signatureValidationFailures: number

  // Lead metrics
  leadsCreated: number
  leadsUpdated: number
  leadsDuplicate: number

  // Activity metrics
  activitiesLogged: number
  activitiesFailed: number

  // Performance
  avgWebhookProcessingTime: number
  avgAPIResponseTime: number

  // Sync lag
  oldestUnprocessedWebhook: number  // milliseconds
}

// Prometheus-style metrics
const metrics = {
  webhooksReceived: new Counter('realgeeks_webhooks_received_total'),
  webhookProcessingTime: new Histogram('realgeeks_webhook_processing_seconds'),
  apiRequestDuration: new Histogram('realgeeks_api_request_duration_seconds'),
  syncErrors: new Counter('realgeeks_sync_errors_total', ['error_type']),
}

// Usage
app.post('/webhooks/realgeeks', async (req, res) => {
  const start = Date.now()
  metrics.webhooksReceived.inc()

  try {
    await processWebhook(req.body)

    const duration = (Date.now() - start) / 1000
    metrics.webhookProcessingTime.observe(duration)

    res.status(200).json({ success: true })
  } catch (error) {
    metrics.syncErrors.inc({ error_type: error.code })
    res.status(500).json({ error: error.message })
  }
})

Health Check Endpoint

app.get('/health/realgeeks', async (req, res) => {
  const health = {
    status: 'healthy',
    checks: {
      apiConnectivity: false,
      recentWebhooks: false,
      syncLag: 0,
    },
    timestamp: new Date().toISOString(),
  }

  try {
    // Test API
    await clients.realgeeks.listUsers(siteUuid)
    health.checks.apiConnectivity = true
  } catch (error) {
    health.status = 'unhealthy'
    console.error('API health check failed:', error)
  }

  // Check recent webhook activity
  const lastWebhook = await getLastWebhookTimestamp()
  if (lastWebhook && Date.now() - lastWebhook < 3600000) {
    health.checks.recentWebhooks = true
  }

  // Calculate sync lag
  health.checks.syncLag = await calculateSyncLag()

  if (health.checks.syncLag > 300000) {  // 5 minutes
    health.status = 'degraded'
  }

  res.status(health.status === 'healthy' ? 200 : 503).json(health)
})

Testing Strategies

Webhook Testing with Ngrok

# 1. Start your local server
npm run dev

# 2. Expose with ngrok
ngrok http 3000

# 3. Configure RealGeeks webhook URL
# Use the ngrok URL: https://abc123.ngrok.io/webhooks/realgeeks

# 4. Trigger test webhook from RealGeeks
# Or use curl to simulate:
curl -X POST https://localhost:3000/webhooks/realgeeks \
  -H "Content-Type: application/json" \
  -H "X-Lead-Router-Action: created" \
  -H "X-Lead-Router-Message-Id: test-123" \
  -H "X-Lead-Router-Signature: $(echo -n '{"test":"data"}' | openssl dgst -sha256 -hmac 'your_secret' | cut -d' ' -f2)" \
  -d '{"test":"data"}'

Unit Testing

import { describe, it, expect, vi } from 'vitest'

describe('RealGeeks Sync', () => {
  it('should validate webhook signature', () => {
    const body = JSON.stringify({ test: 'data' })
    const secret = 'test_secret'
    const validSignature = createHmacSignature(body, secret)

    expect(validateWebhookSignature(body, validSignature, secret)).toBe(true)
    expect(validateWebhookSignature(body, 'invalid', secret)).toBe(false)
  })

  it('should deduplicate leads by email', async () => {
    const lead1 = { email: 'test@example.com', first_name: 'John' }
    const lead2 = { email: 'test@example.com', first_name: 'Johnny' }

    const result1 = await findOrCreateLead(lead1)
    const result2 = await findOrCreateLead(lead2)

    expect(result1.created).toBe(true)
    expect(result2.created).toBe(false)
    expect(result1.lead.id).toBe(result2.lead.id)
  })

  it('should map ElevenLabs events to RealGeeks activities', () => {
    const callEvent = {
      type: 'call_completed',
      duration: 180,
      sentiment: 'positive',
      summary: 'Lead qualified'
    }

    const activity = mapToRealGeeksActivity(callEvent)

    expect(activity.type).toBe('called')
    expect(activity.source).toBe('ElevenLabs AI')
    expect(activity.description).toContain('180s')
    expect(activity.description).toContain('positive')
  })
})

Best Practices Checklist

Security

  • Always validate HMAC signatures
  • Use HTTPS for webhook endpoints
  • Rotate webhook secrets regularly
  • Log all webhook attempts
  • Rate limit webhook endpoint

Reliability

  • Implement idempotency (check message_id)
  • Return 200 OK within 30 seconds
  • Process webhooks asynchronously
  • Use queue for high volume
  • Implement retry with exponential backoff
  • Handle API rate limits

Data Integrity

  • Deduplicate leads before creating
  • Normalize phone numbers
  • Validate required fields
  • Handle partial data gracefully
  • Maintain audit trail
  • Sync activities bidirectionally

Monitoring

  • Track webhook receipt and processing
  • Monitor sync lag
  • Alert on high error rates
  • Log all API calls
  • Dashboard for key metrics
  • Health check endpoint

Troubleshooting Guide

Issue Symptom Solution
Webhooks not received No incoming data Check webhook URL registration in RealGeeks
Signature validation fails 401 errors Verify webhook secret matches
Duplicate leads created Same lead multiple times Implement deduplication logic
Activities not appearing Missing call logs Check activity type and format
Sync lag increasing Old webhooks unprocessed Scale workers, check for bottlenecks
API rate limit hit 429 errors Implement request throttling

Resources


Remember: Robust sync is critical for lead response time. Prioritize reliability, security, and observability in your implementation.