| 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:
app.post('/webhooks/realgeeks', async (req, res) => {
try {
const signature = req.headers['x-lead-router-signature']
if (!validateSignature(req.rawBody, signature, SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
res.status(200).json({ received: true })
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' })
}
})
queue.subscribe('realgeeks.webhooks', async (message) => {
const { action, messageId, payload } = message
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
}
await markProcessed(messageId)
})
Pattern 2: Batch Sync
Use When: Historical data import, nightly reconciliation
Cron Job → Fetch from RealGeeks → Transform → Load to DB
Implementation:
async function syncLeadsFromRealGeeks() {
const leads = await fetchAllLeads(siteUuid)
for (const lead of leads) {
const existing = await database.leads.findByEmail(lead.email)
if (existing) {
if (hasChanged(existing, lead)) {
await database.leads.update(existing.id, transformLead(lead))
}
} else {
await database.leads.insert(transformLead(lead))
}
}
}
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 {
const bodyString = typeof body === 'string' ? body : body.toString()
const hmac = crypto.createHmac('sha256', secret)
hmac.update(bodyString)
const calculatedSignature = hmac.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(calculatedSignature)
)
} catch (error) {
console.error('Signature validation error:', error)
return false
}
}
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' })
}
})
Webhook Headers Reference
interface RealGeeksWebhookHeaders {
'x-lead-router-action': 'created' | 'updated' | 'activity_added' | 'user_updated'
'x-lead-router-message-id': string
'x-lead-router-signature': string
'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) {
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 }
}
}
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 }
}
}
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 }
}
}
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) {
if (leadData.email && candidate.email) {
if (isFuzzyMatch(leadData.email, candidate.email, 0.9)) {
return candidate
}
}
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
}
}
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:
const processedMessages = new Set()
app.post('/webhooks/realgeeks', async (req, res) => {
const messageId = req.headers['x-lead-router-message-id']
if (processedMessages.has(messageId)) {
console.log(`Message ${messageId} already processed`)
return res.status(200).json({ status: 'already_processed' })
}
try {
await processWebhook(req.body)
processedMessages.add(messageId)
res.status(200).json({ status: 'success' })
} catch (error) {
console.error('Webhook processing error:', error)
if (error.code === 'PERMANENT_FAILURE') {
return res.status(406).json({ error: 'Permanent failure' })
}
res.status(500).json({ error: 'Temporary failure' })
}
})
setInterval(() => {
const cutoff = Date.now() - (24 * 60 * 60 * 1000)
}, 3600000)
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
}
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')
}
await retryWithBackoff(async () => {
return await clients.realgeeks.createLead(siteUuid, leadData)
}, 3, 1000)
Data Transformation
Phone Number Normalization
function normalizePhone(phone: string): string {
const digits = phone.replace(/\D/g, '')
if (digits.length === 10) {
return `+1${digits}`
}
if (digits.length === 11 && digits[0] === '1') {
return `+${digits}`
}
if (digits.length > 11) {
return `+${digits}`
}
console.warn(`Invalid phone number: ${phone}`)
return phone
}
function phonesMatch(phone1: string, phone2: string): boolean {
return normalizePhone(phone1) === normalizePhone(phone2)
}
Date/Time Handling
function formatDateForRealGeeks(date: Date): string {
return date.toISOString()
}
function parseRealGeeksDate(dateString: string): Date {
return new Date(dateString)
}
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 {
webhooksReceived: number
webhooksProcessed: number
webhooksFailed: number
signatureValidationFailures: number
leadsCreated: number
leadsUpdated: number
leadsDuplicate: number
activitiesLogged: number
activitiesFailed: number
avgWebhookProcessingTime: number
avgAPIResponseTime: number
oldestUnprocessedWebhook: number
}
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']),
}
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 {
await clients.realgeeks.listUsers(siteUuid)
health.checks.apiConnectivity = true
} catch (error) {
health.status = 'unhealthy'
console.error('API health check failed:', error)
}
const lastWebhook = await getLastWebhookTimestamp()
if (lastWebhook && Date.now() - lastWebhook < 3600000) {
health.checks.recentWebhooks = true
}
health.checks.syncLag = await calculateSyncLag()
if (health.checks.syncLag > 300000) {
health.status = 'degraded'
}
res.status(health.status === 'healthy' ? 200 : 503).json(health)
})
Testing Strategies
Webhook Testing with Ngrok
npm run dev
ngrok http 3000
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
Reliability
Data Integrity
Monitoring
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.