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

onesmartguy
next-level-real-estate
Download Skill Files
View Installation GuideDownload the complete skill directory including SKILL.md and all related files