Skip to main content

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

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​