OMX Logo
Documentation
Events

Events & Callbacks

Handle geotrigger events with webhooks, real-time updates, and custom callback functions for seamless integration.

Event Handling & Callbacks

Geotriggers generate events when users enter, exit, or dwell within defined boundaries. Learn how to handle these events with webhooks, real-time streams, and custom callbacks for seamless integration with your application.

Event Types

Understanding the different types of events that geotriggers can generate:

// Core event types
const eventTypes = {
  ENTER: 'geotrigger.enter',
  EXIT: 'geotrigger.exit',
  DWELL: 'geotrigger.dwell',
  
  // Extended events
  APPROACH: 'geotrigger.approach',
  DEPART: 'geotrigger.depart',
  REVISIT: 'geotrigger.revisit',
  
  // System events
  ACTIVATED: 'geotrigger.activated',
  DEACTIVATED: 'geotrigger.deactivated',
  ERROR: 'geotrigger.error'
};

// Event data structure
interface GeotriggerEvent {
  id: string;
  type: keyof typeof eventTypes;
  triggerId: string;
  userId: string;
  timestamp: string;
  location: {
    lat: number;
    lng: number;
    accuracy: number;
  };
  metadata: {
    dwellTime?: number;
    visitCount?: number;
    deviceInfo: {
      type: 'ios' | 'android' | 'web';
      version: string;
      userAgent?: string;
    };
    custom?: Record<string, any>;
  };
}

Webhook Configuration

Set up webhooks to receive real-time geotrigger events:

import { OMXClient } from '@omx-sdk/geotrigger';

const omx = new OMXClient({
  clientId: 'your_client_id',
  secretKey: 'your_secret_key'
});

// Configure webhook endpoint
const webhook = await omx.webhook.create({
  name: 'Geotrigger Events Handler',
  url: 'https://yourapp.com/webhooks/geotrigger',
  events: [
    'geotrigger.enter',
    'geotrigger.exit',
    'geotrigger.dwell'
  ],
  
  // Security settings
  secret: 'webhook_secret_key',
  signatureHeader: 'X-OMX-Signature',
  
  // Retry configuration
  retryPolicy: {
    maxRetries: 3,
    backoffMultiplier: 2,
    initialDelay: 1000
  },
  
  // Filtering
  filters: {
    triggerIds: ['trigger_123', 'trigger_456'],
    userSegments: ['premium_customers'],
    minimumAccuracy: 50 // meters
  }
});

// Subscribe specific triggers to webhooks
await omx.geotrigger.subscribeToWebhook('trigger_123', webhook.id);

Event Listeners

Register event listeners for real-time event processing:

// Register event listeners
omx.geotrigger.on('enter', async (event) => {
  console.log(`User ${event.userId} entered trigger ${event.triggerId}`);
  
  // Send welcome notification
  await omx.pushNotification.send({
    userId: event.userId,
    title: 'Welcome!',
    body: 'You're near our store. Check out today's deals!',
    data: { triggerId: event.triggerId }
  });
});

omx.geotrigger.on('exit', async (event) => {
  console.log(`User ${event.userId} exited trigger ${event.triggerId}`);
  
  // Send feedback request
  await omx.email.send({
    to: event.userId,
    template: 'feedback_request',
    data: {
      visitDuration: event.metadata.dwellTime,
      location: event.triggerId
    }
  });
});

omx.geotrigger.on('dwell', async (event) => {
  console.log(`User ${event.userId} dwelling in trigger ${event.triggerId}`);
  
  // Trigger special offer for long stays
  if (event.metadata.dwellTime > 300) { // 5 minutes
    await omx.campaign.trigger({
      campaignId: 'long_stay_discount',
      userId: event.userId,
      context: event
    });
  }
});

// Error handling
omx.geotrigger.on('error', (error) => {
  console.error('Geotrigger error:', error);
  // Send to error tracking service
  Sentry.captureException(error);
});

Real-time Event Streaming

Stream events in real-time using WebSockets or Server-Sent Events:

// WebSocket connection for real-time events
const eventStream = omx.geotrigger.stream({
  triggers: ['trigger_123', 'trigger_456'],
  events: ['enter', 'exit'],
  
  // Connection options
  reconnect: true,
  heartbeat: 30000, // 30 seconds
  
  // Event filtering
  filters: {
    userSegments: ['premium_customers'],
    timeWindow: {
      start: '09:00',
      end: '21:00',
      timezone: 'America/New_York'
    }
  }
});

// Handle incoming events
eventStream.on('event', (event) => {
  console.log('Real-time event:', event);
  updateUI(event);
});

eventStream.on('connect', () => {
  console.log('Connected to event stream');
});

eventStream.on('disconnect', () => {
  console.log('Disconnected from event stream');
});

// Server-Sent Events alternative
const sseConnection = omx.geotrigger.streamSSE({
  endpoint: '/events/geotrigger',
  triggers: ['trigger_123']
});

sseConnection.addEventListener('geotrigger.enter', (event) => {
  const data = JSON.parse(event.data);
  handleEnterEvent(data);
});

Custom Callback Functions

Define custom business logic for specific geotrigger events:

// Define custom callback functions
const customCallbacks = {
  loyaltyProgram: async (event) => {
    if (event.type === 'enter') {
      // Award loyalty points for store visits
      await awardLoyaltyPoints(event.userId, {
        points: 10,
        reason: 'Store visit',
        locationId: event.triggerId
      });
      
      // Check for nearby friends
      const nearbyFriends = await findNearbyFriends(
        event.userId, 
        event.location, 
        1000 // 1km radius
      );
      
      if (nearbyFriends.length > 0) {
        await omx.pushNotification.send({
          userId: event.userId,
          title: 'Friends Nearby!',
          body: `${nearbyFriends.length} of your friends are nearby`,
          data: { friends: nearbyFriends }
        });
      }
    }
  },
  
  dynamicPricing: async (event) => {
    if (event.type === 'dwell' && event.metadata.dwellTime > 180) {
      // Implement dynamic pricing based on dwell time
      const currentOccupancy = await getCurrentStoreOccupancy(event.triggerId);
      const priceMultiplier = calculatePriceMultiplier(currentOccupancy);
      
      await updateDynamicPricing(event.triggerId, priceMultiplier);
    }
  },
  
  analyticsTracker: async (event) => {
    // Send to analytics platform
    await analytics.track('Geotrigger Event', {
      eventType: event.type,
      triggerId: event.triggerId,
      userId: event.userId,
      timestamp: event.timestamp,
      location: event.location,
      dwellTime: event.metadata.dwellTime,
      visitCount: event.metadata.visitCount
    });
  }
};

// Register callbacks with triggers
await omx.geotrigger.registerCallback('trigger_123', [
  customCallbacks.loyaltyProgram,
  customCallbacks.analyticsTracker
]);

await omx.geotrigger.registerCallback('trigger_456', [
  customCallbacks.dynamicPricing,
  customCallbacks.analyticsTracker
]);

Conditional Event Processing

Process events based on specific conditions and business rules:

// Define conditional processors
const conditionalProcessors = [
  {
    name: 'VIP Customer Handler',
    condition: (event) => {
      return event.metadata.custom?.customerTier === 'VIP';
    },
    handler: async (event) => {
      // Special VIP treatment
      await omx.pushNotification.send({
        userId: event.userId,
        title: 'VIP Welcome',
        body: 'Welcome back! Your personal assistant will be with you shortly.',
        priority: 'high'
      });
      
      // Notify staff
      await notifyStaff({
        message: `VIP customer ${event.userId} has arrived`,
        location: event.triggerId
      });
    }
  },
  
  {
    name: 'Repeat Visitor Bonus',
    condition: (event) => {
      return event.type === 'enter' && 
             event.metadata.visitCount > 5 &&
             getDaysSinceLastVisit(event.userId, event.triggerId) > 30;
    },
    handler: async (event) => {
      // Welcome back bonus for returning customers
      await omx.campaign.trigger({
        campaignId: 'welcome_back_bonus',
        userId: event.userId,
        data: {
          visitCount: event.metadata.visitCount,
          bonusAmount: Math.min(event.metadata.visitCount * 5, 50)
        }
      });
    }
  },
  
  {
    name: 'Competitor Proximity Alert',
    condition: async (event) => {
      if (event.type !== 'exit') return false;
      
      // Check if user is heading towards competitor
      const nearbyCompetitors = await findNearbyCompetitors(
        event.location, 
        500 // 500m radius
      );
      
      return nearbyCompetitors.length > 0;
    },
    handler: async (event) => {
      // Send retention offer
      await omx.pushNotification.send({
        userId: event.userId,
        title: 'Wait! Special Offer Just for You',
        body: 'Get 15% off your next purchase if you return within 1 hour',
        data: {
          offerCode: generateOfferCode(),
          expiresAt: Date.now() + 3600000 // 1 hour
        }
      });
    }
  }
];

// Register conditional processors
await omx.geotrigger.registerConditionalProcessors(conditionalProcessors);

Event Batching and Processing

Handle high-volume events efficiently with batching and queuing:

// Configure event batching
const batchProcessor = omx.geotrigger.createBatchProcessor({
  batchSize: 100,
  batchTimeout: 5000, // 5 seconds
  
  processor: async (events) => {
    console.log(`Processing batch of ${events.length} events`);
    
    // Group events by type
    const groupedEvents = events.reduce((acc, event) => {
      if (!acc[event.type]) acc[event.type] = [];
      acc[event.type].push(event);
      return acc;
    }, {});
    
    // Process each event type
    await Promise.all([
      processEnterEvents(groupedEvents.enter || []),
      processExitEvents(groupedEvents.exit || []),
      processDwellEvents(groupedEvents.dwell || [])
    ]);
  },
  
  errorHandler: (error, events) => {
    console.error('Batch processing error:', error);
    // Retry failed events individually
    events.forEach(event => {
      eventQueue.push(event);
    });
  }
});

// Queue configuration for high availability
const eventQueue = omx.geotrigger.createQueue({
  name: 'geotrigger-events',
  concurrency: 10,
  
  // Redis configuration for distributed processing
  redis: {
    host: 'redis.yourapp.com',
    port: 6379,
    keyPrefix: 'geotrigger:'
  },
  
  // Dead letter queue for failed events
  deadLetterQueue: {
    enabled: true,
    maxRetries: 5,
    retryDelay: 30000 // 30 seconds
  }
});

// Process queued events
eventQueue.process(async (job) => {
  const event = job.data;
  
  try {
    await processGeotriggerEvent(event);
    return { status: 'success', eventId: event.id };
  } catch (error) {
    throw new Error(`Failed to process event ${event.id}: ${error.message}`);
  }
});

Event Filtering and Deduplication

Implement sophisticated filtering to reduce noise and prevent duplicate processing:

// Advanced event filtering
const eventFilter = omx.geotrigger.createFilter({
  // Duplicate detection
  deduplication: {
    enabled: true,
    timeWindow: 30000, // 30 seconds
    keyGenerator: (event) => `${event.userId}:${event.triggerId}:${event.type}`
  },
  
  // Accuracy filtering
  accuracyFilter: {
    maxRadius: 100, // meters
    rejectLowAccuracy: true
  },
  
  // Time-based filtering
  timeFilter: {
    businessHours: {
      enabled: true,
      start: '09:00',
      end: '21:00',
      timezone: 'America/New_York',
      weekdays: [1, 2, 3, 4, 5, 6] // Mon-Sat
    },
    
    // Rate limiting per user
    rateLimit: {
      perUser: {
        maxEvents: 10,
        timeWindow: 3600000 // 1 hour
      },
      perTrigger: {
        maxEvents: 100,
        timeWindow: 60000 // 1 minute
      }
    }
  },
  
  // Custom filters
  customFilters: [
    // Filter out rapid enter/exit events (possibly GPS noise)
    (event, previousEvent) => {
      if (!previousEvent) return true;
      
      const timeDiff = new Date(event.timestamp) - new Date(previousEvent.timestamp);
      const isOppositeEvent = (
        (event.type === 'enter' && previousEvent.type === 'exit') ||
        (event.type === 'exit' && previousEvent.type === 'enter')
      );
      
      return !(isOppositeEvent && timeDiff < 10000); // 10 seconds
    },
    
    // Filter events from test users
    (event) => {
      return !event.metadata.custom?.isTestUser;
    }
  ]
});

// Apply filters to event stream
omx.geotrigger.applyFilter(eventFilter);

Error Handling and Monitoring

Implement robust error handling and monitoring for event processing:

// Error handling strategies
const errorHandler = {
  // Webhook delivery failures
  webhookError: async (error, event, webhook) => {
    console.error(`Webhook delivery failed: ${error.message}`);
    
    // Implement exponential backoff
    const retryDelay = Math.pow(2, webhook.retryCount) * 1000;
    
    if (webhook.retryCount < 3) {
      setTimeout(() => {
        omx.webhook.retry(webhook.id, event);
      }, retryDelay);
    } else {
      // Send to dead letter queue
      await omx.deadLetterQueue.add({
        type: 'webhook_failure',
        webhookId: webhook.id,
        event: event,
        error: error.message
      });
    }
  },
  
  // Callback execution failures
  callbackError: async (error, event, callbackName) => {
    console.error(`Callback '${callbackName}' failed: ${error.message}`);
    
    // Log to monitoring service
    await monitoring.logError({
      type: 'callback_error',
      callback: callbackName,
      event: event,
      error: error,
      timestamp: new Date().toISOString()
    });
    
    // Alert development team for critical callbacks
    if (callbackName === 'paymentProcessor') {
      await alerting.sendAlert({
        severity: 'critical',
        message: `Payment callback failed for event ${event.id}`,
        context: { event, error }
      });
    }
  }
};

// Performance monitoring
const performanceMonitor = omx.geotrigger.createPerformanceMonitor({
  metrics: [
    'event_processing_time',
    'webhook_delivery_time',
    'callback_execution_time',
    'event_throughput'
  ],
  
  thresholds: {
    processingTime: 1000, // ms
    deliveryTime: 5000, // ms
    errorRate: 0.05 // 5%
  },
  
  alerts: {
    email: 'alerts@yourapp.com',
    slack: 'webhook_url_here'
  }
});

// Health checks
setInterval(async () => {
  const health = await omx.geotrigger.checkEventProcessingHealth();
  
  if (!health.healthy) {
    console.warn('Event processing health check failed:', health.issues);
    await performanceMonitor.alert(health);
  }
}, 60000); // Every minute

Best Practices

  • Event Filtering: Implement robust filtering to reduce noise and processing overhead
  • Idempotency: Ensure event handlers are idempotent to handle duplicate events safely
  • Error Recovery: Implement retry logic with exponential backoff for transient failures
  • Monitoring: Set up comprehensive monitoring and alerting for event processing
  • Performance: Use batching and queuing for high-volume event processing
  • Security: Validate webhook signatures and authenticate event sources

Next Steps