Events Webhooks
Receive updates when new events are published to your feeds.
Event Types
event.item_added
Triggered when a new event is discovered and successfully added to your event feed.
When Webhooks Send
Event webhooks fire immediately when:
- 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
How Webhooks Send
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
See the Webhook Overview for complete details on security verification, retry policy, and HTTP headers.
Payload Structure
The webhook delivers the complete event data in the same format as the Get Feed Items API:
{
"event": "event.item_added",
"timestamp": "2025-11-17T12:34:56.789Z",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440005",
"eventId": "880e8400-e29b-41d4-a716-446655440006",
"addedToFeedAt": "2025-11-17T12:34:56.789Z",
"publishedAt": null,
"title": "Community Art Fair",
"description": "<p>Annual art fair featuring local artists and craftspeople...</p>",
"shortDescription": "Annual community art fair with local artists",
"occurrence": {
"nextOccurrence": {
"id": "aa0e8400-e29b-41d4-a716-446655440008",
"instanceId": "990e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-12-15T10:00:00.000Z",
"endDateTime": "2025-12-15T18:00:00.000Z",
"status": "scheduled"
},
"occurrencesInRange": [
{
"id": "aa0e8400-e29b-41d4-a716-446655440008",
"instanceId": "990e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-12-15T10:00:00.000Z",
"endDateTime": "2025-12-15T18:00:00.000Z",
"status": "scheduled"
},
{
"id": "aa0e8400-e29b-41d4-a716-446655440009",
"instanceId": "990e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-12-16T10:00:00.000Z",
"endDateTime": "2025-12-16T18:00:00.000Z",
"status": "scheduled"
}
],
"totalOccurrences": 2,
"upcomingOccurrences": 2
},
"time": {
"temporalPattern": "singular",
"timezone": "America/Los_Angeles",
"displayText": "Dec 15-16, 2025 at 10:00 AM - 6:00 PM PST"
},
"location": {
"venue": {
"name": "Downtown Community Park",
"address": "123 Main St, San Francisco, CA 94102"
},
"coordinates": {
"lat": 37.7749,
"lng": -122.4194
},
"displayAddress": "Downtown Community Park, 123 Main St, San Francisco, CA 94102"
},
"capacity": {
"total": 500,
"available": 350,
"unlimited": false
},
"organizer": {
"name": "SF Arts Council",
"url": "https://sfartscouncil.org",
"contact": "[email protected]"
},
"primaryImage": "https://example.com/images/art-fair-2025.jpg",
"supportingImages": [
"https://example.com/images/art-fair-booth1.jpg",
"https://example.com/images/art-fair-booth2.jpg"
],
"primaryUrl": "https://example.com/events/art-fair-2025",
"urls": [
"https://example.com/events/art-fair-2025",
"https://sfartscouncil.org/community-fair"
],
"geoLocations": [
{
"id": "cc0e8400-e29b-41d4-a716-44665544000a",
"description": "Downtown Community Park",
"relationType": "LOCATION",
"score": 95
}
]
}
}
Payload Fields
Event Metadata
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique feed item identifier |
eventId | string (UUID) | Event identifier (same event can appear in multiple feeds) |
addedToFeedAt | string (ISO 8601) | When event was added to this feed |
publishedAt | string (ISO 8601)|null | When event was originally published (currently always null) |
Core Event Data
| Field | Type | Description |
|---|---|---|
title | string | Event title (may contain HTML) |
description | string | Full event description (may contain HTML) |
shortDescription | string|null | Brief summary of the event |
Occurrence Information
The occurrence object aggregates all instances and occurrences:
| Field | Type | Description |
|---|---|---|
nextOccurrence | object|null | Earliest occurrence (see structure below) |
occurrencesInRange | array | All occurrences (array of objects with same structure as nextOccurrence) |
totalOccurrences | number | Total number of occurrences |
upcomingOccurrences | number | Number of future occurrences (from current time) |
Next Occurrence Fields:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Occurrence identifier |
instanceId | string (UUID) | Parent instance identifier |
startDateTime | string (ISO 8601) | When the occurrence starts |
endDateTime | string (ISO 8601)|null | When the occurrence ends |
status | string | Instance status: scheduled, cancelled, postponed, rescheduled, completed |
Structured Event Details
Time Information (time object|null):
| Field | Type | Description |
|---|---|---|
temporalPattern | string | singular (one-time), recurring (repeats), or ongoing (continuous) |
timezone | string | IANA timezone (e.g., America/Los_Angeles) |
displayText | string | Human-readable time (e.g., "Oct 20, 2025 at 7:00 PM PST") |
Location Information (location object|null):
| Field | Type | Description |
|---|---|---|
venue.name | string | Venue name |
venue.address | string | Street address |
coordinates.lat | number | Latitude |
coordinates.lng | number | Longitude |
online.platform | string | Platform name (e.g., "Zoom") |
online.url | string | Event URL |
displayAddress | string | Human-readable address for display |
Capacity Information (capacity object|null):
| Field | Type | Description |
|---|---|---|
total | number | Total capacity/seats |
available | number | Currently available seats |
unlimited | boolean | Whether capacity is unlimited |
Organizer Information (organizer object|null):
| Field | Type | Description |
|---|---|---|
name | string | Organizer name |
url | string | Organizer website |
contact | string | Contact information (email/phone) |
Media & Links
| Field | Type | Description |
|---|---|---|
primaryImage | string|null | Main event image URL |
supportingImages | string[] | Additional image URLs |
primaryUrl | string|null | Main event URL/website |
urls | string[] | All related URLs |
Geographic Relevance
The geoLocations array contains all geographic locations:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Geographic location identifier |
description | string | Location description (e.g., venue name, city) |
relationType | string | LOCATION (specific venue) or RELEVANCE_REGION (broader area) |
score | number | Relevance score (0-100) |
Example Handler
app.post('/webhooks/events', async (req, res) => {
const { event, data } = req.body;
if (event === 'event.item_added') {
console.log(`New event added: ${data.title}`);
// Access next occurrence
if (data.occurrence.nextOccurrence) {
console.log(
`Next occurrence: ${data.occurrence.nextOccurrence.startDateTime}`
);
console.log(`Status: ${data.occurrence.nextOccurrence.status}`);
}
// Access location
if (data.location?.venue) {
console.log(`Venue: ${data.location.venue.name}`);
console.log(`Address: ${data.location.displayAddress}`);
}
// Access time information
if (data.time) {
console.log(`Pattern: ${data.time.temporalPattern}`);
console.log(`Timezone: ${data.time.timezone}`);
}
// Access organizer
if (data.organizer) {
console.log(`Organized by: ${data.organizer.name}`);
}
// Process the event
await processEvent(data);
}
res.status(200).send('OK');
});
Common Use Cases
Display Upcoming Events
function displayEvent(event) {
const { title, occurrence, location, time } = event;
console.log(`\n${title}`);
// Show next occurrence
if (occurrence.nextOccurrence) {
const startDate = new Date(occurrence.nextOccurrence.startDateTime);
console.log(`When: ${startDate.toLocaleString()}`);
console.log(`Status: ${occurrence.nextOccurrence.status}`);
}
// Show location
if (location) {
console.log(`Where: ${location.displayAddress}`);
}
// Show multiple occurrences
if (occurrence.totalOccurrences > 1) {
console.log(
`This event has ${occurrence.totalOccurrences} occurrences in the date range`
);
}
}
Filter by Event Type
app.post('/webhooks/events', async (req, res) => {
const { data } = req.body;
// Check if it's a recurring event
if (data.time?.temporalPattern === 'recurring') {
console.log('Recurring event detected');
await handleRecurringEvent(data);
}
// Check if it's an online event
if (data.location?.online) {
console.log(`Online event on ${data.location.online.platform}`);
await handleOnlineEvent(data);
}
// Check if tickets are limited
if (data.capacity && !data.capacity.unlimited) {
const availablePercent =
(data.capacity.available / data.capacity.total) * 100;
if (availablePercent < 20) {
console.log('Low ticket availability - send alert!');
await sendLowAvailabilityAlert(data);
}
}
res.status(200).send('OK');
});
Handle Multi-Occurrence Events
function handleEventOccurrences(event) {
const { occurrence } = event;
// Get all occurrence dates
const dates = occurrence.occurrencesInRange.map((occ) =>
new Date(occ.startDateTime).toLocaleDateString()
);
console.log(`Event occurs on: ${dates.join(', ')}`);
// Check for cancelled occurrences
const cancelled = occurrence.occurrencesInRange.filter(
(occ) => occ.status === 'cancelled'
);
if (cancelled.length > 0) {
console.log(`Warning: ${cancelled.length} occurrence(s) are cancelled`);
}
// Count upcoming vs total
console.log(
`${occurrence.upcomingOccurrences} upcoming of ${occurrence.totalOccurrences} total`
);
}
Timezone Handling
All timestamps are in UTC (ISO 8601 format). Convert to local timezone:
function formatEventTime(event) {
const { occurrence, time } = event;
if (!occurrence.nextOccurrence || !time) {
return 'Time TBD';
}
const startTime = new Date(occurrence.nextOccurrence.startDateTime);
// Use the event's timezone
const localTime = startTime.toLocaleString('en-US', {
timeZone: time.timezone,
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
return `${localTime} (${time.timezone})`;
}
Geographic Filtering
app.post('/webhooks/events', async (req, res) => {
const { data } = req.body;
// Check if event is in San Francisco
const inSF = data.geoLocations.some((geo) =>
geo.description.toLowerCase().includes('san francisco')
);
if (inSF) {
console.log('Event in San Francisco detected');
// Check if it's a specific venue or broader region
const venues = data.geoLocations.filter(
(geo) => geo.relationType === 'LOCATION'
);
const regions = data.geoLocations.filter(
(geo) => geo.relationType === 'RELEVANCE_REGION'
);
console.log(`Venues: ${venues.map((v) => v.description).join(', ')}`);
console.log(`Regions: ${regions.map((r) => r.description).join(', ')}`);
}
res.status(200).send('OK');
});
Security Best Practices
Always verify webhook signatures before processing:
import crypto from 'crypto';
function verifyWebhookSignature(req, webhookSecret) {
const signature = req.headers['x-helix-signature'];
const timestamp = req.headers['x-helix-timestamp'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return signature === expectedSignature;
}
app.post('/webhooks/events', async (req, res) => {
// Verify signature first
if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const { event, data } = req.body;
await processEvent(data);
res.status(200).send('OK');
});
Testing Webhooks
Use the webhook testing endpoint to send test events:
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"
}'
Next Steps
- Event Feed API Reference: Complete API documentation
- Event Feed Concepts: Understanding the event data model
- Webhook Overview: General webhook documentation
- Authentication: API key management