Skip to main content
Webhooks allow your application to receive real-time notifications when events occur in Winnerr CRM. Instead of continuously polling the API for changes, webhooks provide an efficient way to stay synchronized with your CRM data.

Webhook Overview

Real-time Events

Instant notifications for contacts, deals, properties, and communication events

Secure Delivery

HTTPS delivery with signature verification and retry mechanisms

Flexible Filtering

Subscribe to specific events and filter by conditions

Reliable Processing

Automatic retries with exponential backoff and dead letter queues

Supported Events

Contact Events

EventDescriptionPayload
contact.createdNew contact createdContact object
contact.updatedContact information updatedUpdated contact object
contact.deletedContact deletedContact ID
contact.taggedTags added to contactContact object with new tags
contact.untaggedTags removed from contactContact object with removed tags
contact.assignedContact assigned to agentContact object with assignment

Deal Events

EventDescriptionPayload
deal.createdNew deal createdDeal object
deal.updatedDeal information updatedUpdated deal object
deal.deletedDeal deletedDeal ID
deal.stage_changedDeal moved to different stageDeal object with stage change
deal.wonDeal marked as wonWon deal object
deal.lostDeal marked as lostLost deal object
deal.task_createdTask added to dealDeal object with new task
deal.task_completedTask completedDeal object with completed task

Property Events

EventDescriptionPayload
property.createdNew property listing createdProperty object
property.updatedProperty information updatedUpdated property object
property.deletedProperty listing deletedProperty ID
property.status_changedListing status changedProperty object with status
property.price_changedList price modifiedProperty object with price change
property.photos_uploadedNew photos addedProperty object with photo URLs
property.inquiry_receivedProperty inquiry submittedInquiry object

Communication Events

EventDescriptionPayload
communication.email_sentEmail sent to contactEmail object
communication.email_receivedEmail received from contactEmail object
communication.sms_sentSMS sent to contactSMS object
communication.sms_receivedSMS received from contactSMS object
communication.call_startedPhone call initiatedCall object
communication.call_endedPhone call completedCall object with duration
communication.voicemail_receivedVoicemail left by contactVoicemail object

System Events

EventDescriptionPayload
organization.updatedOrganization settings changedOrganization object
user.createdNew user added to organizationUser object
user.updatedUser information updatedUpdated user object
integration.connectedExternal integration connectedIntegration object
integration.disconnectedExternal integration disconnectedIntegration object

Creating Webhooks

Using the API

Create webhooks programmatically using the Winnerr API:
const webhook = await client.webhooks.create({
  url: 'https://your-app.com/webhooks/winnerr',
  events: [
    'contact.created',
    'contact.updated',
    'deal.stage_changed',
    'property.status_changed'
  ],
  secret: 'your-webhook-secret-key',
  enabled: true,
  description: 'Lead management webhook'
});

console.log('Webhook created:', webhook.id);

Using the Dashboard

You can also create and manage webhooks through the Winnerr dashboard:
  1. Navigate to Settings > Integrations > Webhooks
  2. Click “Create New Webhook”
  3. Enter your endpoint URL
  4. Select the events you want to subscribe to
  5. Configure filtering options (optional)
  6. Set up authentication and security settings
  7. Test the webhook and save

Webhook Configuration

Endpoint Requirements

Your webhook endpoint must meet these requirements:
  • HTTPS Only: Webhooks are only delivered to HTTPS endpoints
  • Response Time: Respond within 30 seconds
  • Status Code: Return 2xx status code for successful processing
  • Content-Type: Accept application/json content type

Event Filtering

Filter webhooks based on specific conditions:
// Example: Only receive contact events for high-value leads
const webhook = await client.webhooks.create({
  url: 'https://your-app.com/webhooks/high-value-leads',
  events: ['contact.created', 'contact.updated'],
  filters: {
    'contact.leadScore': { gte: 80 },
    'contact.source': { in: ['REFERRAL', 'WEBSITE'] }
  },
  secret: 'your-secret-key'
});

Webhook Headers

Each webhook request includes these headers:
POST /webhooks/winnerr HTTP/1.1
Host: your-app.com
Content-Type: application/json
Content-Length: 1234
X-Winnerr-Event: contact.created
X-Winnerr-Signature: sha256=abc123...
X-Winnerr-Delivery: 12345678-1234-1234-1234-123456789abc
X-Winnerr-Timestamp: 1640995200
User-Agent: Winnerr-Webhooks/1.0

Webhook Payload Format

Standard Payload Structure

All webhook payloads follow this consistent structure:
{
  "id": "evt_123456789",
  "event": "contact.created",
  "apiVersion": "2024-01",
  "createdAt": "2024-01-15T10:30:00Z",
  "data": {
    "object": {
      "id": "contact_123",
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com",
      "phone": "+1-555-123-4567",
      "type": "LEAD",
      "status": "ACTIVE",
      "leadScore": 85,
      "source": "WEBSITE",
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    },
    "previousAttributes": {},
    "changes": ["firstName", "lastName", "email"]
  },
  "organization": {
    "id": "org_123",
    "name": "Premier Realty Group"
  },
  "user": {
    "id": "user_456",
    "name": "Sarah Johnson",
    "email": "sarah@premierrealty.com"
  }
}

Event-Specific Payloads

Contact Created Event

{
  "id": "evt_contact_created_123",
  "event": "contact.created",
  "apiVersion": "2024-01",
  "createdAt": "2024-01-15T10:30:00Z",
  "data": {
    "object": {
      "id": "contact_123",
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com",
      "phone": "+1-555-123-4567",
      "type": "LEAD",
      "status": "ACTIVE",
      "leadScore": 85,
      "source": "WEBSITE",
      "tags": ["website-lead", "first-time-buyer"],
      "preferences": {
        "propertyTypes": ["CONDO", "TOWNHOUSE"],
        "priceRange": { "min": 300000, "max": 600000 }
      },
      "createdAt": "2024-01-15T10:30:00Z"
    },
    "previousAttributes": null,
    "changes": null
  }
}

Deal Stage Changed Event

{
  "id": "evt_deal_stage_123",
  "event": "deal.stage_changed",
  "apiVersion": "2024-01",
  "createdAt": "2024-01-15T14:20:00Z",
  "data": {
    "object": {
      "id": "deal_456",
      "title": "Miami Waterfront Condo Sale",
      "stage": "CONTRACT",
      "previousStage": "NEGOTIATION",
      "amount": 750000,
      "probability": 90,
      "contactId": "contact_123",
      "assignedTo": "agent_789",
      "updatedAt": "2024-01-15T14:20:00Z"
    },
    "previousAttributes": {
      "stage": "NEGOTIATION",
      "probability": 75
    },
    "changes": ["stage", "probability"]
  }
}

Property Price Changed Event

{
  "id": "evt_property_price_123",
  "event": "property.price_changed",
  "apiVersion": "2024-01",
  "createdAt": "2024-01-15T16:45:00Z",
  "data": {
    "object": {
      "id": "property_789",
      "mlsId": "MLS12345678",
      "address": {
        "street": "123 Ocean Drive",
        "city": "Miami Beach",
        "state": "FL",
        "zipCode": "33139"
      },
      "listing": {
        "listPrice": 725000,
        "originalPrice": 795000,
        "priceReduction": 70000,
        "status": "ACTIVE"
      }
    },
    "previousAttributes": {
      "listing": {
        "listPrice": 750000
      }
    },
    "changes": ["listing.listPrice"],
    "priceChange": {
      "previousPrice": 750000,
      "newPrice": 725000,
      "changeAmount": -25000,
      "changePercentage": -0.033
    }
  }
}

Implementing Webhook Handlers

Basic Webhook Handler

Implement a basic webhook handler to process events:
const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/winnerr', (req, res) => {
  const signature = req.headers['x-winnerr-signature'];
  const timestamp = req.headers['x-winnerr-timestamp'];
  const event = req.headers['x-winnerr-event'];
  const payload = req.body;
  
  try {
    // Verify webhook signature
    if (!verifySignature(payload, signature, timestamp)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Parse event data
    const eventData = JSON.parse(payload);
    
    // Process event based on type
    processWebhookEvent(eventData);
    
    // Acknowledge receipt
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});

function verifySignature(payload, signature, timestamp) {
  // Prevent replay attacks (reject requests older than 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 300) {
    return false;
  }
  
  // Verify signature
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(timestamp + '.' + payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature.replace('sha256=', '')),
    Buffer.from(expectedSignature)
  );
}

async function processWebhookEvent(event) {
  switch (event.event) {
    case 'contact.created':
      await handleContactCreated(event.data.object);
      break;
    case 'deal.stage_changed':
      await handleDealStageChanged(event.data.object);
      break;
    case 'property.price_changed':
      await handlePriceChanged(event.data.object);
      break;
    default:
      console.log('Unhandled event type:', event.event);
  }
}

Event-Specific Handlers

Implement specific handlers for different event types:
// Contact created handler
async function handleContactCreated(contact) {
  console.log('New contact created:', contact.id);
  
  // Add to external CRM
  await addToExternalCRM(contact);
  
  // Send welcome email
  if (contact.email) {
    await sendWelcomeEmail(contact);
  }
  
  // Create follow-up task
  await createFollowUpTask(contact);
  
  // Update lead scoring
  await updateLeadScore(contact);
}

// Deal stage changed handler
async function handleDealStageChanged(deal) {
  console.log(`Deal ${deal.id} moved to ${deal.stage}`);
  
  // Update external systems
  await updateExternalDeal(deal);
  
  // Send notifications
  await notifyTeamMembers(deal);
  
  // Trigger stage-specific workflows
  switch (deal.stage) {
    case 'CONTRACT':
      await initiateContractWorkflow(deal);
      break;
    case 'CLOSING':
      await prepareClosingDocuments(deal);
      break;
    case 'WON':
      await processClosedDeal(deal);
      break;
  }
}

// Property price changed handler
async function handlePriceChanged(property) {
  console.log(`Property ${property.id} price changed to $${property.listing.listPrice}`);
  
  // Update property listings on external sites
  await updateExternalListings(property);
  
  // Notify interested clients
  await notifyInterestedClients(property);
  
  // Update market analytics
  await updateMarketData(property);
  
  // Send price alert emails
  if (property.listing.listPrice < property.listing.originalPrice) {
    await sendPriceReductionAlerts(property);
  }
}

Advanced Webhook Features

Idempotency Handling

Handle duplicate webhook deliveries gracefully:
// Idempotency tracking
const processedEvents = new Set();

async function processWebhookEvent(event) {
  const eventId = event.id;
  
  // Check if event already processed
  if (processedEvents.has(eventId)) {
    console.log('Event already processed:', eventId);
    return;
  }
  
  try {
    // Process event
    await handleEvent(event);
    
    // Mark as processed
    processedEvents.add(eventId);
    
    // Clean up old events (keep last 1000)
    if (processedEvents.size > 1000) {
      const eventsArray = Array.from(processedEvents);
      processedEvents.clear();
      eventsArray.slice(-500).forEach(id => processedEvents.add(id));
    }
    
  } catch (error) {
    console.error('Event processing failed:', error);
    throw error;
  }
}

Webhook Retry Logic

Implement robust retry handling:
// Webhook retry configuration
const RETRY_CONFIG = {
  maxRetries: 5,
  baseDelay: 1000,
  maxDelay: 30000,
  backoffMultiplier: 2
};

async function processWithRetry(operation, eventId) {
  let attempt = 0;
  
  while (attempt < RETRY_CONFIG.maxRetries) {
    try {
      await operation();
      return;
    } catch (error) {
      attempt++;
      
      if (attempt >= RETRY_CONFIG.maxRetries) {
        // Send to dead letter queue
        await sendToDeadLetterQueue(eventId, error);
        throw error;
      }
      
      // Calculate retry delay with exponential backoff
      const delay = Math.min(
        RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt - 1),
        RETRY_CONFIG.maxDelay
      );
      
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Webhook Filtering

Filter events on the server side:
// Server-side event filtering
function shouldProcessEvent(event, filters) {
  if (!filters) return true;
  
  for (const [field, condition] of Object.entries(filters)) {
    const value = getNestedValue(event.data.object, field);
    
    if (!matchesCondition(value, condition)) {
      return false;
    }
  }
  
  return true;
}

function matchesCondition(value, condition) {
  if (typeof condition === 'object') {
    for (const [operator, operand] of Object.entries(condition)) {
      switch (operator) {
        case 'eq':
          return value === operand;
        case 'ne':
          return value !== operand;
        case 'gt':
          return value > operand;
        case 'gte':
          return value >= operand;
        case 'lt':
          return value < operand;
        case 'lte':
          return value <= operand;
        case 'in':
          return Array.isArray(operand) && operand.includes(value);
        case 'nin':
          return Array.isArray(operand) && !operand.includes(value);
        default:
          return false;
      }
    }
  }
  
  return value === condition;
}

// Example usage
const filters = {
  'leadScore': { gte: 80 },
  'source': { in: ['WEBSITE', 'REFERRAL'] },
  'type': { eq: 'LEAD' }
};

if (shouldProcessEvent(event, filters)) {
  await processEvent(event);
}

Webhook Management

List Webhooks

Retrieve all configured webhooks:
const webhooks = await client.webhooks.list();

webhooks.data.forEach(webhook => {
  console.log(`Webhook ${webhook.id}:`);
  console.log(`  URL: ${webhook.url}`);
  console.log(`  Events: ${webhook.events.join(', ')}`);
  console.log(`  Status: ${webhook.enabled ? 'Enabled' : 'Disabled'}`);
  console.log(`  Created: ${webhook.createdAt}`);
});

Update Webhook

Modify webhook configuration:
const updatedWebhook = await client.webhooks.update('webhook_123', {
  events: [
    'contact.created',
    'contact.updated',
    'deal.created',
    'deal.stage_changed',
    'property.created'
  ],
  enabled: true,
  description: 'Updated webhook for comprehensive event handling'
});

Delete Webhook

Remove a webhook configuration:
await client.webhooks.delete('webhook_123');
console.log('Webhook deleted successfully');

Testing Webhooks

Webhook Testing Tool

Use the built-in testing tool to verify your webhook endpoint:
// Test webhook endpoint
const testResult = await client.webhooks.test('webhook_123', {
  event: 'contact.created',
  data: {
    id: 'contact_test_123',
    firstName: 'Test',
    lastName: 'Contact',
    email: 'test@example.com'
  }
});

console.log('Test result:', testResult);

Local Development

Use tools like ngrok for local webhook testing:
# Install ngrok
npm install -g ngrok

# Expose local server
ngrok http 3000

# Use the generated URL for webhook endpoint
# https://abc123.ngrok.io/webhooks/winnerr

Webhook Simulator

Create a webhook simulator for testing:
// Webhook simulator for testing
class WebhookSimulator {
  constructor(endpoint, secret) {
    this.endpoint = endpoint;
    this.secret = secret;
  }
  
  async simulateEvent(eventType, data) {
    const event = {
      id: `evt_${Date.now()}`,
      event: eventType,
      apiVersion: '2024-01',
      createdAt: new Date().toISOString(),
      data: { object: data },
      organization: { id: 'org_test', name: 'Test Organization' }
    };
    
    const payload = JSON.stringify(event);
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = this.generateSignature(payload, timestamp);
    
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Winnerr-Event': eventType,
        'X-Winnerr-Signature': `sha256=${signature}`,
        'X-Winnerr-Timestamp': timestamp.toString(),
        'X-Winnerr-Delivery': `delivery_${Date.now()}`
      },
      body: payload
    });
    
    return {
      status: response.status,
      response: await response.text()
    };
  }
  
  generateSignature(payload, timestamp) {
    const crypto = require('crypto');
    return crypto
      .createHmac('sha256', this.secret)
      .update(`${timestamp}.${payload}`)
      .digest('hex');
  }
}

// Usage
const simulator = new WebhookSimulator(
  'https://your-app.com/webhooks/winnerr',
  'your-webhook-secret'
);

const result = await simulator.simulateEvent('contact.created', {
  id: 'contact_test_123',
  firstName: 'Test',
  lastName: 'Contact',
  email: 'test@example.com'
});

console.log('Simulation result:', result);

Monitoring & Debugging

Webhook Logs

Monitor webhook delivery attempts:
// Get webhook delivery logs
const logs = await client.webhooks.getLogs('webhook_123', {
  startDate: '2024-01-01T00:00:00Z',
  endDate: '2024-01-31T23:59:59Z',
  status: 'failed' // or 'success', 'pending'
});

logs.data.forEach(log => {
  console.log(`Delivery ${log.id}:`);
  console.log(`  Event: ${log.event}`);
  console.log(`  Status: ${log.status}`);
  console.log(`  Response: ${log.responseCode}`);
  console.log(`  Attempts: ${log.attempts}`);
  console.log(`  Next Retry: ${log.nextRetryAt}`);
});

Error Handling

Implement comprehensive error logging:
// Webhook error logging
class WebhookLogger {
  constructor() {
    this.errors = [];
  }
  
  logError(event, error) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      eventId: event.id,
      eventType: event.event,
      error: {
        message: error.message,
        stack: error.stack,
        code: error.code
      },
      data: event.data
    };
    
    this.errors.push(logEntry);
    
    // Send to monitoring service
    this.sendToMonitoring(logEntry);
  }
  
  async sendToMonitoring(logEntry) {
    // Send to your monitoring service (e.g., Sentry, DataDog)
    try {
      await fetch('https://monitoring-service.com/webhook-errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(logEntry)
      });
    } catch (error) {
      console.error('Failed to send error to monitoring service:', error);
    }
  }
  
  getRecentErrors(hours = 24) {
    const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
    return this.errors.filter(error => 
      new Date(error.timestamp) > cutoff
    );
  }
}

const logger = new WebhookLogger();

// Use in webhook handler
try {
  await processWebhookEvent(event);
} catch (error) {
  logger.logError(event, error);
  throw error;
}

Best Practices

1. Security

Implement proper security measures:
// Security best practices
const securityMiddleware = (req, res, next) => {
  // Rate limiting
  if (isRateLimited(req.ip)) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  
  // IP whitelist (optional)
  const allowedIPs = process.env.WEBHOOK_ALLOWED_IPS?.split(',') || [];
  if (allowedIPs.length > 0 && !allowedIPs.includes(req.ip)) {
    return res.status(403).json({ error: 'IP not allowed' });
  }
  
  // Signature verification (handled in main webhook handler)
  next();
};

2. Performance

Optimize webhook processing:
// Asynchronous processing
const queue = require('bull');
const webhookQueue = new queue('webhook processing');

app.post('/webhooks/winnerr', async (req, res) => {
  // Verify signature and respond quickly
  if (!verifySignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Add to processing queue
  await webhookQueue.add('process-webhook', {
    event: JSON.parse(req.body),
    headers: req.headers
  });
  
  // Respond immediately
  res.status(200).json({ received: true });
});

// Process webhooks asynchronously
webhookQueue.process('process-webhook', async (job) => {
  const { event, headers } = job.data;
  await processWebhookEvent(event);
});

3. Reliability

Ensure reliable webhook processing:
// Reliability patterns
const reliableWebhookProcessor = {
  // Circuit breaker pattern
  circuitBreaker: {
    failures: 0,
    threshold: 5,
    timeout: 60000,
    state: 'closed' // closed, open, half-open
  },
  
  async processWithCircuitBreaker(operation) {
    if (this.circuitBreaker.state === 'open') {
      if (Date.now() - this.circuitBreaker.lastFailure > this.circuitBreaker.timeout) {
        this.circuitBreaker.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }
    
    try {
      const result = await operation();
      
      if (this.circuitBreaker.state === 'half-open') {
        this.circuitBreaker.state = 'closed';
        this.circuitBreaker.failures = 0;
      }
      
      return result;
    } catch (error) {
      this.circuitBreaker.failures++;
      
      if (this.circuitBreaker.failures >= this.circuitBreaker.threshold) {
        this.circuitBreaker.state = 'open';
        this.circuitBreaker.lastFailure = Date.now();
      }
      
      throw error;
    }
  }
};

Webhooks are essential for building responsive real estate applications. Always implement proper security, error handling, and monitoring to ensure reliable webhook processing. Test thoroughly in the sandbox environment before deploying to production.