realgeeks-sync

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.

About realgeeks-sync

realgeeks-sync is a Claude AI skill developed by onesmartguy. 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. This powerful Claude Code plugin helps developers automate workflows and enhance productivity with intelligent AI assistance.

0Stars
0Forks
2025-11-10

Why use realgeeks-sync? With 0 stars on GitHub, this skill has been trusted by developers worldwide. Install this Claude skill instantly to enhance your development workflow with AI-powered automation.

namerealgeeks-sync
descriptionBi-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-toolsRead, 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

IssueSymptomSolution
Webhooks not receivedNo incoming dataCheck webhook URL registration in RealGeeks
Signature validation fails401 errorsVerify webhook secret matches
Duplicate leads createdSame lead multiple timesImplement deduplication logic
Activities not appearingMissing call logsCheck activity type and format
Sync lag increasingOld webhooks unprocessedScale workers, check for bottlenecks
API rate limit hit429 errorsImplement request throttling

Resources


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

onesmartguy

onesmartguy

next-level-real-estate

View on GitHub

Download Skill Files

View Installation Guide

Download the complete skill directory including SKILL.md and all related files