Skip to main content

Events Webhooks

Receive real-time notifications when events are added, updated, or removed from your feeds.

Important: Webhooks as Synchronization Signals

Webhooks Are Signals, Not Source of Truth

Webhooks should be treated as signals to synchronize your system, not as the authoritative source of data.

Event webhooks are designed to notify you that something has changed, prompting you to query the API for the current state. This design has several important implications:

  1. Always query the API for current state - When you receive a webhook, use the provided feedId and eventId to fetch the complete, up-to-date event data from the Event Feed API.

  2. Webhooks may be debounced - Multiple rapid changes to the same event may result in a single webhook notification. The webhook tells you "something changed" not "exactly what changed."

  3. Webhooks can arrive out of order - Network conditions may cause webhooks to arrive in a different order than the changes occurred. The API always has the authoritative order.

  4. Webhooks may be delivered multiple times - Your endpoint should be idempotent. Use the eventId to detect and handle duplicate notifications.

  5. Don't cache webhook payloads as truth - The minimal payload is intentional. Fetch fresh data from the API to ensure you have the latest state.

app.post('/webhooks/events', async (req, res) => {
const { event, data } = req.body;

// Acknowledge immediately - don't block on processing
res.status(200).send('OK');

// Queue for async processing
await queue.add('process-event-webhook', {
eventType: event,
feedId: data.feedId,
eventId: data.eventId,
receivedAt: Date.now(),
});
});

// Async processor
async function processEventWebhook({ eventType, feedId, eventId }) {
if (eventType === 'event.item_added' || eventType === 'event.item_updated') {
// Fetch current state from API - this is the source of truth
const currentEvent = await helixApi.getEvent(feedId, eventId);
await syncEventToDatabase(currentEvent);
} else if (eventType === 'event.item_removed') {
// Mark as removed in your system
await markEventAsRemoved(eventId);
}
}

Event Types

Events webhooks support three event types:

Event TypeDescriptionTrigger
event.item_addedA new event was added to the feedNew event discovered and processed
event.item_updatedAn existing event was updatedEvent data changed (times, details, etc.)
event.item_removedOne or more events were removedEvent deleted or no longer relevant

event.item_added

Triggered when a new event is discovered and successfully added to your event feed.

When This Webhook Fires

  • A new event is discovered from any of your configured sources (sites, sitemaps, index pages, Instagram)
  • Event data is extracted including dates, times, locations, and other structured information
  • The event is successfully added to your event feed

Payload Structure

{
"eventType": "event.item_added",
"timestamp": 1704123456789,
"data": {
"timestamp": 1704123456789,
"feedId": "987e6543-e21b-12d3-a456-426614174111",
"eventId": "123e4567-e89b-12d3-a456-426614174000"
}
}

Payload Fields

FieldTypeDescription
timestampnumberUnix timestamp in milliseconds when triggered
feedIdstring (UUID)ID of the event feed that was updated
eventIdstring (UUID)ID of the event that was added

Example Handler

app.post('/webhooks/events', async (req, res) => {
const { eventType, data } = req.body;

if (eventType === 'event.item_added') {
console.log(`New event added to feed ${data.feedId}`);

// Fetch the complete event data from the API
const event = await helixApi.getFeedItem(data.feedId, data.eventId);

// Process the full event
await processNewEvent(event);
}

res.status(200).send('OK');
});

event.item_updated

Triggered when an existing event in your feed has been updated. This occurs when the source data changes and the event is re-processed.

When This Webhook Fires

  • Event details change on the source website (time, location, description, etc.)
  • Event status changes (cancelled, rescheduled, etc.)
  • Additional information is discovered about an existing event

Payload Structure

{
"eventType": "event.item_updated",
"timestamp": 1704123456789,
"data": {
"timestamp": 1704123456789,
"feedId": "987e6543-e21b-12d3-a456-426614174111",
"eventId": "123e4567-e89b-12d3-a456-426614174000"
}
}

Payload Fields

FieldTypeDescription
timestampnumberUnix timestamp in milliseconds when triggered
feedIdstring (UUID)ID of the event feed that was updated
eventIdstring (UUID)ID of the event that was updated

Example Handler

app.post('/webhooks/events', async (req, res) => {
const { eventType, data } = req.body;

if (eventType === 'event.item_updated') {
console.log(`Event ${data.eventId} was updated`);

// Fetch the latest event data from the API
const updatedEvent = await helixApi.getFeedItem(data.feedId, data.eventId);

// Update your local copy with fresh data
await updateExistingEvent(data.eventId, updatedEvent);
}

res.status(200).send('OK');
});

Common Update Scenarios

  • Time changes: Event start/end times or dates changed
  • Venue changes: Location or venue information updated
  • Cancellation: Event status changed to cancelled or postponed
  • Capacity updates: Available tickets or capacity changed
  • Description updates: Event details or description modified

event.item_removed

Triggered when one or more events are removed from your feed. This can happen when events are no longer relevant, have been deleted from the source, or no longer match your feed criteria.

When This Webhook Fires

  • Events are removed because they no longer match feed criteria
  • Source website removes or deletes the event
  • Events are cleaned up due to being stale or expired
  • Bulk removal during source re-processing

Payload Structure

{
"eventType": "event.item_removed",
"timestamp": 1704123456789,
"data": {
"timestamp": 1704123456789,
"feedId": "987e6543-e21b-12d3-a456-426614174111",
"eventIds": [
"123e4567-e89b-12d3-a456-426614174000",
"234e5678-e89b-12d3-a456-426614174001"
]
}
}

Payload Fields

FieldTypeDescription
timestampnumberUnix timestamp in milliseconds when triggered
feedIdstring (UUID)ID of the event feed that was updated
eventIdsstring[] (UUID)Array of event IDs that were removed

Example Handler

app.post('/webhooks/events', async (req, res) => {
const { eventType, data } = req.body;

if (eventType === 'event.item_removed') {
console.log(
`${data.eventIds.length} event(s) removed from feed ${data.feedId}`
);

// Remove or archive these events from your system
for (const eventId of data.eventIds) {
await archiveOrRemoveEvent(eventId);
}
}

res.status(200).send('OK');
});

Important Considerations

  • Multiple events per webhook: Unlike item_added and item_updated, this webhook can contain multiple event IDs in a single notification
  • Handle gracefully: The event may already be gone from your system - make removal idempotent
  • Consider archiving: You may want to archive rather than delete, keeping a record that the event was once in your feed

How Webhooks Are Sent

All event webhooks follow the standard Helix webhook protocol:

  • HTTP Method: POST request to your configured endpoint
  • Content Type: application/json
  • Headers: Includes signature, timestamp, webhook ID, and event ID for verification
  • Retry Policy: Failed deliveries are retried with exponential backoff (up to 5 attempts)
  • Security: HMAC-SHA256 signature for request verification
  • Debouncing: Rapid changes are debounced with a 2-minute window per feed

See the Webhook Overview for complete details on security verification, retry policy, and HTTP headers.


Debouncing Behavior

Event webhooks implement intelligent debouncing to prevent overwhelming your endpoint during periods of high activity:

  • Debounce window: 2 minutes per organization per feed
  • Scope: Each event is debounced independently (updating event A won't delay notifications about event B)
  • Behavior: If multiple updates occur within the window, you'll receive one notification prompting you to sync

This means:

  • You should always fetch the current state from the API rather than relying solely on webhook payloads
  • Your system should be designed to handle any state when you sync, not just incremental changes

Comprehensive Example

Handle all event webhook types with proper error handling:

import crypto from 'crypto';

// Verify webhook authenticity
function verifyWebhookSignature(req, webhookSecret) {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = JSON.stringify(req.body);

const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(`${timestamp}.${payload}`)
.digest('hex');

return signature === `v1=${expectedSignature}`;
}

app.post('/webhooks/events', async (req, res) => {
// Always verify signature first
if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

// Check timestamp to prevent replay attacks
const timestamp = parseInt(req.headers['x-webhook-timestamp']);
const age = Date.now() - timestamp;
if (age > 5 * 60 * 1000) {
// 5 minutes
console.error('Webhook timestamp too old');
return res.status(400).send('Timestamp too old');
}

const { eventType, data } = req.body;

// Acknowledge receipt immediately
res.status(200).send('OK');

try {
switch (eventType) {
case 'event.item_added':
await handleEventAdded(data.feedId, data.eventId);
break;

case 'event.item_updated':
await handleEventUpdated(data.feedId, data.eventId);
break;

case 'event.item_removed':
await handleEventsRemoved(data.feedId, data.eventIds);
break;

default:
console.warn(`Unknown event type: ${eventType}`);
}
} catch (error) {
// Log but don't fail - we already acknowledged
console.error('Error processing webhook:', error);
}
});

async function handleEventAdded(feedId, eventId) {
// Fetch complete event data from API
const event = await helixApi.getFeedItem(feedId, eventId);
await database.events.upsert({
where: { helixEventId: eventId },
create: mapToLocalEvent(event),
update: mapToLocalEvent(event),
});
}

async function handleEventUpdated(feedId, eventId) {
// Fetch latest event data from API
const event = await helixApi.getFeedItem(feedId, eventId);
await database.events.update({
where: { helixEventId: eventId },
data: mapToLocalEvent(event),
});
}

async function handleEventsRemoved(feedId, eventIds) {
// Mark events as removed (soft delete)
await database.events.updateMany({
where: { helixEventId: { in: eventIds } },
data: { removedAt: new Date() },
});
}

Best Practices

1. Respond Quickly

Return a 200 response immediately and process asynchronously:

app.post('/webhooks/events', async (req, res) => {
// Respond immediately
res.status(200).send('OK');

// Process in background
setImmediate(() => processWebhook(req.body));
});

2. Be Idempotent

Handle duplicate deliveries gracefully:

async function handleEventAdded(feedId, eventId) {
// Use upsert to handle duplicates
await database.events.upsert({
where: { helixEventId: eventId },
create: {
/* ... */
},
update: {
/* ... */
},
});
}

3. Use the API as Source of Truth

Always fetch current state rather than relying on cached webhook data:

// Good: Fetch current state
const event = await helixApi.getFeedItem(feedId, eventId);

// Bad: Use only webhook payload data
// The payload is intentionally minimal

4. Handle Missing Events

Events may already be removed when you try to fetch them:

async function handleEventUpdated(feedId, eventId) {
try {
const event = await helixApi.getFeedItem(feedId, eventId);
await updateEvent(event);
} catch (error) {
if (error.status === 404) {
// Event was removed - treat as removal
await markEventRemoved(eventId);
} else {
throw error;
}
}
}

5. Log for Debugging

Keep webhook logs for troubleshooting:

app.post('/webhooks/events', async (req, res) => {
const webhookEventId = req.headers['x-webhook-event-id'];

console.log('Received webhook', {
webhookEventId,
eventType: req.body.eventType,
feedId: req.body.data?.feedId,
timestamp: req.body.timestamp,
});

// ... process ...
});

Testing Webhooks

Use the webhook testing endpoint to send test events:

# Test event.item_added
curl -X POST https://api.feeds.onhelix.ai/webhooks/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/events",
"event": "event.item_added"
}'

# Test event.item_updated
curl -X POST https://api.feeds.onhelix.ai/webhooks/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/events",
"event": "event.item_updated"
}'

# Test event.item_removed
curl -X POST https://api.feeds.onhelix.ai/webhooks/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/events",
"event": "event.item_removed"
}'

Next Steps