Event Feeds Workflows
Learn how to build common event features with step-by-step examples. These workflows show you how to fetch, filter, and display events from your feeds. For what every field means, see Concepts and the API reference.
Building an Event Listing Page
Scenario: Display upcoming events for the next 30 days on your website.
Step 1: Fetch upcoming events
# Get events for the next 30 days
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
Response (one item shown, shortened):
{
"success": true,
"data": {
"items": [
{
"id": "5d1f0c2a-8b3e-4f7a-9c6d-2e4b1a0f7c35",
"eventId": "a3c9e1f7-2b4d-4e8a-9f1c-6d0b5e2a8c47",
"addedToFeedAt": "2026-10-20T14:03:00.000Z",
"updatedAt": "2026-10-24T09:12:00.000Z",
"status": "active",
"title": "Riverfront Music Night",
"description": "Outdoor concert with local and national artists on the riverfront stage.",
"shortDescription": "Outdoor concert on the riverfront stage",
"when": {
"pattern": "once",
"timezone": "America/Los_Angeles",
"displayText": "Fri, Nov 13, 2026, 6:00 PM – 11:00 PM",
"next": {
"start": "2026-11-14T02:00:00.000Z",
"end": "2026-11-14T07:00:00.000Z",
"allDay": false,
"status": "scheduled"
},
"occurrences": [
{
"start": "2026-11-14T02:00:00.000Z",
"end": "2026-11-14T07:00:00.000Z",
"allDay": false,
"status": "scheduled"
}
],
"upcomingCount": 1
},
"where": {
"displayText": "City Park Amphitheater, 123 Main Street, Portland, OR 97201",
"venue": { "name": "City Park Amphitheater" },
"address": {
"display": "123 Main Street, Portland, Oregon 97201, United States",
"street": "123 Main Street",
"city": "Portland",
"region": "Oregon",
"postcode": "97201",
"country": "United States",
"countryCode": "US"
},
"geo": { "lat": 45.5152, "lng": -122.6784, "precision": "address" }
},
"links": {
"best": {
"url": "https://example.com/riverfront-music-night",
"kind": "event_page",
"official": true,
"relation": "origin",
"publisher": {
"name": "Example Festivals",
"domain": "example.com",
"platform": "web",
"role": "organizer",
"roleConfidence": "inferred"
},
"publishedAt": "2026-10-19T16:00:00.000Z",
"reason": "official_event_page"
},
"all": [
{
"url": "https://example.com/riverfront-music-night",
"kind": "event_page",
"official": true,
"relation": "origin",
"publisher": {
"name": "Example Festivals",
"domain": "example.com",
"platform": "web",
"role": "organizer",
"roleConfidence": "inferred"
},
"publishedAt": "2026-10-19T16:00:00.000Z"
}
]
},
"media": { "cover": null, "images": [] }
}
],
"total": 45,
"pagination": { "limit": 20, "offset": 0, "hasMore": true }
}
}
links.best is also the first entry of links.all.
Step 2: Display each event
# - Title: item.title
# - When: item.when.displayText (human-readable)
# - Where: item.where?.displayText
# - Image: item.media.cover?.url (cover can be null, even when
# item.media.images has entries: then show no image, e.g.
# image = item.media.cover?.url ?? placeholderImage)
# - Link: item.links.best?.url (null when there is nothing to link to)
# - Cancelled: item.status === "cancelled"
Linking to More Information
Use links.best, the recommended "more info" link. It is chosen in this order: an official event page, an official tickets or registration page, an official social post, a ticketing page, any other event page, and last a curator's or media page.
const link = item.links.best;
if (link === null) {
// No URL worth sending people to: show no link
} else if (link.reason === 'curator_or_media_page') {
// Nothing better exists: label it as third-party
show('Read more (third-party page)', link.url);
} else {
show('More info', link.url);
}
To offer tickets separately, look in links.all:
const tickets = item.links.all.find(
(l) => l.official && (l.kind === 'tickets' || l.kind === 'registration')
);
Handling Different Event Patterns
One-Time Events
when.pattern is once and there is one occurrence.
{
"title": "Tech Conference 2026",
"when": {
"pattern": "once",
"timezone": "America/Los_Angeles",
"displayText": "Fri, Nov 20, 2026, 9:00 AM – 5:00 PM",
"next": {
"start": "2026-11-20T17:00:00.000Z",
"end": "2026-11-21T01:00:00.000Z",
"allDay": false,
"status": "scheduled"
},
"occurrences": [
{
"start": "2026-11-20T17:00:00.000Z",
"end": "2026-11-21T01:00:00.000Z",
"allDay": false,
"status": "scheduled"
}
],
"upcomingCount": 1
}
}
Display: "Tech Conference 2026 - Fri, Nov 20, 2026, 9:00 AM – 5:00 PM"
Multi-Day Events
A multi-day event usually has one occurrence per day. A date range can also be stored as one spanning occurrence, so read when.displayText for the range.
{
"title": "Food & Wine Festival",
"when": {
"pattern": "once",
"timezone": "America/Chicago",
"displayText": "Fri, Nov 13 – Sun, Nov 15, 2026",
"upcomingCount": 1
}
}
(next and occurrences are left out of this example. Here the festival is stored as one occurrence spanning the three days; when it is stored as one occurrence per day, displayText shows the next day.)
Display: "Food & Wine Festival - Fri, Nov 13 – Sun, Nov 15, 2026"
Recurring Events
when.pattern is recurring. when.recurrence describes the rule, and when.occurrences lists the dates that overlap your query window.
{
"title": "Weekly Farmers Market",
"when": {
"pattern": "recurring",
"timezone": "America/Los_Angeles",
"displayText": "Sundays, 9:00 AM",
"recurrence": {
"description": "Every Sunday at 9 a.m.",
"startDate": "2026-04-05",
"exceptions": ["2026-11-29"],
"patterns": [
{
"rule": "FREQ=WEEKLY;BYDAY=SU",
"startTime": "09:00",
"description": "Sundays"
}
]
},
"upcomingCount": 12
}
}
(next and occurrences are left out of this example.) The skipped date 2026-11-29 is in exceptions, and an occurrence on that date is left out of occurrences and next.
Display: "Weekly Farmers Market - Sundays, 9:00 AM (next: Nov 8)"
Ongoing Events
when.pattern is ongoing for something that runs continuously over a period, such as an exhibition. Show when.displayText.
Filtering by Date Range
Get events for a specific weekend:
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?since=2026-11-20T00:00:00Z&until=2026-11-22T23:59:59Z&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
Important: since and until must be between 7 days in the past and 30 days in the future, and since must be before until. Outside that, the request returns 400 with code INVALID_QUERY.
Narrowing the List
The filters combine with AND:
# Recurring events that have an official link and a cover
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?pattern=recurring&hasOfficialLink=true&hasCover=true" \
-H "Authorization: Bearer YOUR_API_KEY"
# Only well-described events
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?minCompleteness=0.8" \
-H "Authorization: Bearer YOUR_API_KEY"
# Events that lack an official link, to review them
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?hasOfficialLink=false" \
-H "Authorization: Bearer YOUR_API_KEY"
Keeping a Local Copy Up to Date
Poll with updatedSince to fetch only what changed since your last poll. updatedAt changes when the item changes in a way subscribers are notified of.
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?updatedSince=2026-10-24T09:00:00Z&orderBy=created&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
updatedSince combines with the occurrence window, so items with no occurrence overlapping the window are not returned. Use addedSince to fetch only newly added items.
To be told instead of polling, subscribe with Event Delivery and fetch the item on every event.item.updated (see Single item).
Filtering by Event Status
Whole event cancelled: check the item's top-level status:
{ "title": "Annual Gala", "status": "cancelled" }
Only items with a scheduled occurrence:
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?status=scheduled&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
status matches the status of an occurrence (scheduled, cancelled, postponed, rescheduled, completed).
Display logic:
# Pseudo-code for display:
if item.status === "cancelled":
show_badge("CANCELLED")
apply_strikethrough_styling()
else:
# Occurrences carry the event's status
status = item.when.next?.status
if status === "postponed":
show_badge("POSTPONED")
elif status === "rescheduled":
show_badge("RESCHEDULED") # a new occurrence has status "scheduled"
Handling Different Location Types
Physical Events
{
"title": "Community Meetup",
"where": {
"displayText": "Downtown Library, 456 Oak St, Seattle, WA 98101",
"venue": { "name": "Downtown Library" },
"address": {
"display": "456 Oak St, Seattle, WA 98101, United States",
"street": "456 Oak St",
"city": "Seattle",
"region": "Washington",
"postcode": "98101",
"country": "United States",
"countryCode": "US"
},
"geo": { "lat": 47.6062, "lng": -122.3321, "precision": "address" }
}
}
Display: "Downtown Library, 456 Oak St, Seattle, WA 98101" + a map pin. Pin only when geo.precision is address; a locality point is a town or neighborhood, not the door.
Online Events
{
"title": "Virtual Workshop",
"where": {
"displayText": "Online",
"online": { "url": "https://zoom.us/j/123456789", "platform": "Zoom" }
}
}
Display: "Online via Zoom" + [Join meeting button]
Hybrid Events
{
"title": "Tech Conference",
"where": {
"displayText": "Convention Center, 789 Pine St, San Francisco, CA 94102",
"venue": { "name": "Convention Center" },
"address": { "display": "789 Pine St, San Francisco, CA 94102" },
"online": {
"url": "https://streamyard.com/watch/xyz",
"platform": "StreamYard"
}
}
}
Display: "Convention Center, San Francisco + Virtual attendance available"
Several Venues
where.multipleVenues is true and venue, address and geo are omitted. Show where.displayText.
Sorting Events
By occurrence date (default - soonest first):
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?orderBy=occurrence&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
By recently added:
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?orderBy=created&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
There is no relevance ordering.
Pagination
Load More Events (Infinite Scroll)
# Initial load
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
# Returns: { data: { total: 150, pagination: { offset: 0, limit: 20, hasMore: true } } }
# Load next page
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=20&offset=20" \
-H "Authorization: Bearer YOUR_API_KEY"
# Continue until hasMore === false
Get Complete Dataset
# Fetch all events in batches
OFFSET=0
LIMIT=100
while true; do
RESPONSE=$(curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=$LIMIT&offset=$OFFSET" \
-H "Authorization: Bearer YOUR_API_KEY")
# Process events from RESPONSE
HAS_MORE=$(echo $RESPONSE | jq -r '.data.pagination.hasMore')
if [ "$HAS_MORE" != "true" ]; then
break
fi
OFFSET=$((OFFSET + LIMIT))
done
Building a Calendar View
Scenario: Display events in a monthly calendar.
# Step 1: Get the events in your window (at most 7 days back and 30 days ahead;
# replace the example dates with ones in that window)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?since=2026-11-01T00:00:00Z&until=2026-11-28T23:59:59Z&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
# Step 2: Group by date
# For each event:
# - Read when.occurrences[] (the occurrences that overlap the window, including one already in progress at `since`)
# - Group by the date of start
# - Display on the matching calendar day
Display logic:
# Pseudo-code:
calendar = {}
for item in items:
for occurrence in item.when.occurrences:
date = occurrence.start.split('T')[0] # YYYY-MM-DD, UTC
if date not in calendar:
calendar[date] = []
calendar[date].append({
'title': item.title,
'start': occurrence.start,
'allDay': occurrence.allDay,
'status': occurrence.status,
'url': item.links.best?.url
})
# Render calendar with events grouped by date
Occurrence times are UTC. To place an occurrence on the right local day, convert start to when.timezone first. When when.timezone is omitted, the event has no known zone and the times are what its source gave, which for a Facebook event without a location is UTC.
Geographic Filtering
Find Events in a City
const inSanFrancisco = items.filter(
(item) => item.where?.address?.city === 'San Francisco'
);
Using Coordinates (Client-Side Filtering)
Events with a where.geo point can be filtered by distance:
const userLocation = { lat: 37.7749, lng: -122.4194 }; // San Francisco
const nearby = items.filter((item) => {
const geo = item.where?.geo;
if (!geo || geo.precision === 'locality') return false;
return distanceMiles(userLocation, geo) < 10;
});
Keep a locality point if you only need town-level matching; it is the centre of a town or neighborhood.
Building a "This Weekend" Widget
#!/bin/bash
# Calculate this weekend's dates
FRIDAY=$(date -d "next friday" +%Y-%m-%dT00:00:00Z)
SUNDAY=$(date -d "next sunday" +%Y-%m-%dT23:59:59Z)
# Fetch weekend events
EVENTS=$(curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?since=$FRIDAY&until=$SUNDAY&status=scheduled&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY")
# Display formatted output
echo "$EVENTS" | jq -r '.data.items[] |
"• \(.title)
When: \(.when.displayText)
Where: \(.where.displayText // "To be announced")
\(.links.best.url // "")
"'
Output:
• Riverfront Music Night
When: Fri, Nov 13, 2026, 6:00 PM – 11:00 PM
Where: City Park Amphitheater, 123 Main Street, Portland, OR 97201
https://example.com/riverfront-music-night
• Food Truck Festival
When: Sat, Nov 14, 2026, 11:00 AM – 4:00 PM
Where: Downtown Plaza, Portland, OR
https://example.com/food-trucks
Best Practices
1. Use Appropriate Date Ranges
# Example dates: use a window from at most 7 days back to 30 days ahead
# ✓ Good: up to 30 days ahead for general event browsing
?since=2026-11-01T00:00:00Z&until=2026-11-28T23:59:59Z
# ✓ Good: Specific weekend
?since=2026-11-20T00:00:00Z&until=2026-11-22T23:59:59Z
# ✗ Bad: Exceeds the 30-day future limit (400 INVALID_QUERY)
?since=2026-11-01T00:00:00Z&until=2027-01-15T23:59:59Z
2. Handle Cancelled Events
Check item.status === "cancelled" for a cancellation. A single cancelled date of a series is not represented: the item shows only whole-event cancellation.
3. Handle Pagination Properly
# ✓ Good: Check hasMore before requesting next page
if (response.data.pagination.hasMore) {
fetchNextPage(response.data.pagination.offset + limit)
}
# ✗ Bad: Assuming fixed total pages
for (let page = 1; page <= 10; page++) { ... }
4. Use displayText for Dates
# ✓ Good: Use the pre-formatted text
item.when.displayText // "Alternating Wednesdays, 9:00 AM – 9:45 AM"
# ✓ For the next date, format it in the event's timezone
new Date(item.when.next.start).toLocaleString('en-US', { timeZone: item.when.timezone })
5. Show Event Type Indicators
if (item.when.pattern === 'recurring'):
show_icon('🔄') // Recurring
elif (item.when.pattern === 'ongoing'):
show_icon('♾') // Ongoing
elif (item.when.upcomingCount > 1):
show_icon('📅') // Multi-day
else:
show_icon('📍') // One-time
6. Do Not Store Image Links
media.cover.url and media.images[].url are signed and expire at expiresAt. Show them straight away, and fetch the item again for a current link rather than storing the URL.
7. Cache and Refresh Strategically
# ✓ Good: Cache for reasonable duration
cache_duration = 5 minutes # For frequently updated feeds
cache_duration = 1 hour # For stable event listings
# ✓ Good: Refresh on user action
# Provide manual refresh button for users who want latest data
Next Steps
- Event Feeds Overview: Learn about event feeds
- API Reference: Complete endpoint documentation
- Event Feed Concepts: Understanding the event data model
- Event Delivery Concepts: How deliveries, destinations, and subscriptions work