Event Feed API Reference
Complete API reference for Helix Event Feeds. All endpoints require authentication via API key.
An interactive OpenAPI playground is available at api.feeds.onhelix.ai/docs. The spec is generated from the live route validators, so it cannot drift from the running implementation — useful for testing requests, inspecting response shapes, and generating client SDKs.
Base URL: https://api.feeds.onhelix.ai
Authentication
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
See the Authentication Guide for details on obtaining and managing API keys.
Quick Start
Get your first events in five steps:
1. Create an Event Feed
curl -X POST https://api.feeds.onhelix.ai/feeds/events \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Events",
"description": "Events I want to track"
}'
Save the returned id - you'll need it for the next steps. The feed starts
disabled, and stays disabled until you enable it in step 4.
2. Add Websites
Add each website your events come from (the whole site, whatever the path):
curl -X POST https://api.feeds.onhelix.ai/feeds/events/{feedId}/sources \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "site",
"url": "https://www.carolinatheatre.org/events"
}'
Listing pages, sitemaps and social accounts are added by our team on request. Adds to your event and news feeds share a limit of 30 per minute per organization. See Sources.
3. Decide What It Carries
Create an area as processors (POST /processors) and add filters to the feed:
our opt-in filters, a set of them, or your own. A new feed carries nothing
until it has an include filter. POST/PATCH /processors requests that
search places (a place by name, or by an id none of your areas holds) share a
limit of 30 per minute per organization. The Quickstart
builds a feed end to end; Feed Configuration
documents every route.
4. Enable It
Once the feed has its sources and an include filter, enable it. Enabling the feed brings in, from each of its enabled sources, the events the source brought in during the past 14 days that pass its filters (once per source):
curl -X PATCH https://api.feeds.onhelix.ai/feeds/events/{feedId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'
5. Get Your Events
Retrieve events from your feed:
# Get upcoming events (next 30 days by default)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items" \
-H "Authorization: Bearer YOUR_API_KEY"
Common Query Patterns:
# Events in a specific date range: replace the dates with a range that lies
# within the last 7 days and the next 30 (an illustrative two weeks here)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?since=YYYY-MM-01T00:00:00Z&until=YYYY-MM-14T23:59:59Z" \
-H "Authorization: Bearer YOUR_API_KEY"
# Only scheduled events (exclude cancelled)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?status=scheduled" \
-H "Authorization: Bearer YOUR_API_KEY"
# Only items with an official link and a cover
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?hasOfficialLink=true&hasCover=true" \
-H "Authorization: Bearer YOUR_API_KEY"
# Sort by when added to feed (newest first)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?orderBy=created" \
-H "Authorization: Bearer YOUR_API_KEY"
Next Steps:
- See Event Feed Concepts to understand the event data model
- Review Get Feed Items below for all query options and response examples
- Set up Event Delivery to receive real-time notifications instead of polling
Event Feed Endpoints
Create an Event Feed
Creates a new event feed to organize events from multiple sources.
POST /feeds/events
Request
Headers:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Body:
{
"name": "Community Events",
"description": "Local community happenings and activities",
"config": {
"version": 1
}
}
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Feed name. Minimum 1 character. Unique within your organization, ignoring case and surrounding whitespace. |
description | string | Yes | Feed description. Minimum 1 character. |
enabled | boolean | No | A new feed starts disabled. true is refused with 422: enabling needs an include filter, and a new feed has none. Send false or leave it out. |
config | object | No | Feed configuration options. Keys other than the ones below are refused with 400. |
config.version | number | No | Config schema version. Must be 1. Defaults to 1. |
config.media.retentionDaysAfterEnd | number | No | Days after an event ends that its images stay available. Whole number from 1 to 365. Defaults to 14. |
A new event feed starts disabled, with one filter, the completeness
opt-in filter, and no include filter. Add
an include (see Filters), then enable the feed with
Update Event Feed.
Response
Status: 200 OK
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Community Events",
"description": "Local community happenings and activities",
"organizationId": "660e8400-e29b-41d4-a716-446655440001",
"enabled": false,
"enabledSource": "system",
"enabledReason": null,
"config": {
"version": 1
},
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}
Response fields:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique feed identifier |
name | string | Feed name |
description | string | Feed description |
organizationId | string (UUID) | The organization that owns the feed |
enabled | boolean | Whether the feed is active: an enabled feed collects events, a disabled one keeps its items but takes no new ones |
enabledSource | string | How enabled was last set: system (the default), override (a change through this API or by our team), cascade (turned off with what it depends on) or error (turned off after failures). You cannot set it. |
enabledReason | string|null | The reason recorded when enabled was last set, if any. You cannot set it. |
config | object | Feed configuration: version and, when set, media.retentionDaysAfterEnd. Which events the feed carries is set by its filters, not here. You can send it back as is. |
createdAt | string (ISO 8601) | Feed creation timestamp |
updatedAt | string (ISO 8601) | Last update timestamp |
Example
curl -X POST https://api.feeds.onhelix.ai/feeds/events \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "City Events",
"description": "Events happening around the city"
}'
Errors
400: Missing or invalid fields. The error code isINVALID_BODY.401: Missing or invalid API key.409: Another event feed of your organization already has this name. Names are compared ignoring case and surrounding whitespace, so"City Events"and" city events "are the same name. The error code isALREADY_EXISTS.422:enabled: true. The error code isVALIDATION_FAILED: a new feed has no include filter. Nothing is created. Create the feed, add an include filter, then enable it.
List Event Feeds
Retrieves the event feeds your organization owns and those it subscribes to, newest first. The list is not paginated.
GET /feeds/events
Request
Headers:
Authorization: Bearer YOUR_API_KEY
Response
Status: 200 OK
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Community Events",
"description": "Local community happenings",
"organizationId": "660e8400-e29b-41d4-a716-446655440001",
"enabled": true,
"enabledSource": "override",
"enabledReason": "Enabled through the API",
"config": {
"version": 1
},
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
]
}
Example
curl https://api.feeds.onhelix.ai/feeds/events \
-H "Authorization: Bearer YOUR_API_KEY"
Get Event Feed
Retrieves a specific event feed by ID: one your organization owns or subscribes to.
GET /feeds/events/:id
Request
Headers:
Authorization: Bearer YOUR_API_KEY
Path Parameters:
| Name | Type | Description |
|---|---|---|
id | string (UUID) | Event feed ID |
Response
Status: 200 OK
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Community Events",
"description": "Local community happenings",
"organizationId": "660e8400-e29b-41d4-a716-446655440001",
"enabled": true,
"enabledSource": "override",
"enabledReason": "Enabled through the API",
"config": {
"version": 1
},
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}
Example
curl https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer YOUR_API_KEY"
Errors
400: A malformed UUID. The error code isINVALID_PARAMS.401: Missing or invalid API key.404: Feed not found, or your organization has no access to it.
Update Event Feed
Updates an existing event feed. All fields are optional, including the config keys.
PATCH /feeds/events/:id
Request
Headers:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Path Parameters:
| Name | Type | Description |
|---|---|---|
id | string (UUID) | Event feed ID |
Body:
{
"name": "Updated Events Feed",
"description": "Updated description",
"enabled": false,
"config": {
"version": 1
}
}
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
name | string | No | New feed name. Minimum 1 character. Unique within your organization, ignoring case and surrounding whitespace. |
description | string | No | New feed description. Minimum 1 character. |
enabled | boolean | No | Enable or disable the feed. Enabling needs at least one include filter on the feed (see Filters). Enabling a feed adds, once per source, the events each enabled source brought in during the past 14 days that pass its filters: a source already brought in is not brought in again when you enable the feed later, and a source added while the feed was disabled is brought in when you enable it. A catch-up that stopped before it finished is started again by the next enable. Disabling stops crawling of the feed's sources that no other enabled feed uses. |
config | object | No | Updated feed configuration. Keys other than the ones below are refused with 400. config keys you leave out keep their current values; to go back to the default retention, send 14. |
config.version | number | No | Config schema version. Must be 1 if sent. |
config.media.retentionDaysAfterEnd | number | No | Days after an event ends that its images stay available. Whole number from 1 to 365. Defaults to 14. |
A request whose enabled changes the feed's status records enabledSource: "override" with enabledReason "Enabled through the API" or "Disabled through the API". A request that leaves the status as it is leaves both as they are.
Response
Status: 200 OK
Returns the updated feed object (same structure as Get Event Feed).
Example
curl -X PATCH https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Events Feed",
"enabled": true
}'
Errors
A refused update changes nothing, including the fields that were valid.
400: Invalid fields or a malformed UUID.401: Missing or invalid API key.403: Your organization subscribes to the feed but does not own it.404: Feed not found, or your organization has no access to it.409: Another event feed of your organization already has the new name (compared ignoring case and surrounding whitespace). The error code isALREADY_EXISTS.422:enabled: trueon a feed that has no include filter. The error code isVALIDATION_FAILED. Add an include filter to the feed first.
Delete Event Feed
Permanently deletes an event feed with its sources, filters, items and the feedback on them. The sites and accounts behind its sources stay, as other feeds may use them.
Deleting an event feed publishes
event.item.removed
for every event item that was in the feed, and removes the feed from every
Event Delivery subscription's pins. A feeds-scoped subscription left with
zero pins becomes inactive with inactiveReason: "feed deleted". See
Event Delivery: Feed deletion.
DELETE /feeds/events/:id
Request
Headers:
Authorization: Bearer YOUR_API_KEY
Path Parameters:
| Name | Type | Description |
|---|---|---|
id | string (UUID) | Event feed ID |
Response
Status: 200 OK
{
"success": true
}
Example
curl -X DELETE https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer YOUR_API_KEY"
Errors
400: A malformed UUID.401: Missing or invalid API key.403: Your organization subscribes to the feed but does not own it.404: Feed not found, or your organization has no access to it.
Sources
| Route | Does |
|---|---|
GET /feeds/events/{id}/sources | Every source on the feed, newest first |
POST /feeds/events/{id}/sources | Add a website: { "type": "site", "url": … } |
PATCH /feeds/events/{id}/sources/{sourceId} | Enable or disable a source: { "enabled": … } |
DELETE /feeds/events/{id}/sources/{sourceId} | Remove a source from this feed |
The same routes and source shape serve news feeds. Only websites can be added
through the API (422 SOURCE_TYPE_NOT_ALLOWED for another type); you can
list, enable, disable and remove every source. A site the feed already has,
with or without www., is a 409. Adding a website starts a 14-day backfill
of it; on an enabled feed, the events the site already brought in during the
past 14 days are added too (a disabled feed receives them when you enable
it), as they are when you enable a source of an enabled feed that has not
brought them in yet. Each source is caught up this way once. Subscribers to
the feed can list its sources but not change them.
Adding sources is limited to 30 per minute per organization, one budget
shared by your event and news feeds. Over it, the add is refused with 429
RATE_LIMITED and a Retry-After header (seconds); while the limit cannot be
counted, it is refused with 503 SERVICE_UNAVAILABLE and a Retry-After.
See Sources for the rules, the source shape and
every error.
curl -X POST https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000/sources \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "site",
"url": "https://www.carolinatheatre.org/events"
}'
{
"success": true,
"data": {
"id": "9b2f6c1e-4d3a-4e8b-9c7d-2a1b0f3e5d64",
"type": "site",
"identity": {
"domain": "www.carolinatheatre.org"
},
"enabled": true,
"addedAt": "2026-10-28T14:05:00.000Z"
}
}
Filters
| Route | Does |
|---|---|
GET /feeds/events/{id}/filters | The feed's own and opt-in filters |
GET /feeds/events/{id}/filters/{filterId} | One filter |
POST /feeds/events/{id}/filters | Add your own filter, or one of our opt-in filters |
PATCH /feeds/events/{id}/filters/{filterId} | Change one of your own filters |
DELETE /feeds/events/{id}/filters/{filterId} | Remove a filter |
POST /feeds/events/{id}/filter-sets/{setKey} | Add a set of opt-in filters, e.g. community_events |
An event is in the feed when no exclude filter matches and at least one
include filter does; the safety floor always applies and is not listed. Only
the feed's owner can read or change its filters. Filters read
processors (GET /processors). See
Filters and
Opt-in filters for the condition format,
the catalog and every error.
Event Feed Items Endpoint
Get Feed Items
Returns the events in a feed. Each item says when the event happens (when), where (where), which pages to send people to (links, with a recommended best link) and its images (media).
GET /feeds/events/{feedId}/items
Request
Path parameters:
| Name | Type | Description |
|---|---|---|
feedId | string (UUID) | Event feed ID |
Headers:
Authorization: Bearer YOUR_API_KEY
Query parameters:
| Name | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Items per page. Min 1, max 100. |
offset | integer | 0 | Items to skip. |
since | date (ISO 8601) | now | Start of the occurrence window. |
until | date (ISO 8601) | since + 30 days | End of the occurrence window. |
status | string | all | Only items with an occurrence of this status: scheduled, cancelled, postponed, rescheduled, completed. |
addedSince | date (ISO 8601) | - | Only items added to the feed at or after this time. |
updatedSince | date (ISO 8601) | - | Only items whose updatedAt is at or after this time, inside the occurrence window. See "Polling for changes". |
pattern | string | - | Only items with this schedule: once, recurring or ongoing. |
hasOfficialLink | boolean | - | true: only items with a link published by the organizer, venue or a performer. false: only items without one. |
hasCover | boolean | - | true: only items with a cover image. false: only items without one. |
minCompleteness | number 0 to 1 | - | Only items whose completeness.score is at least this value. |
orderBy | string | occurrence | occurrence: soonest first. created: newest added first. |
The filters combine with AND, and with the occurrence window.
Occurrence window:
- No dates: occurrences from now until 30 days from now.
- Only
since: fromsincefor 30 days, at most until 30 days from now. - Only
until: from now untiluntil. Whenuntilis already past, the window starts 7 days ago. - Both: exactly that range.
since and until must fall between 7 days in the past and 30 days in the future, and since must be before until. An item is returned when it has an occurrence that overlaps the window: it starts at or before until, and it has not ended by since. An occurrence ends at its end time, or at its start when it has no end; a date with no time lasts until that date is over everywhere; a time given without a timezone lasts until its listed date is over. So an event already in progress is listed, for example today's event once it has started, or an ongoing exhibition stored as one long occurrence. when.occurrences lists the occurrences that overlap the window.
Invalid parameters: an invalid value (a bad pattern, a date outside the limits, an orderBy other than occurrence or created) and an include parameter return 400 with code INVALID_QUERY:
{
"success": false,
"error": {
"code": "INVALID_QUERY",
"message": "Invalid query parameter 'include': `include` is not supported on event feed items"
}
}
Response
Status: 200 OK
{
"success": true,
"data": {
"items": [
/* event feed items, see below */
],
"total": 42,
"pagination": { "limit": 20, "offset": 0, "hasMore": true }
}
}
total counts every item matching the query across all pages. Check pagination.hasMore to see whether there is another page.
The item
Optional fields are omitted when unknown, never sent as null (the exceptions are links.best and media.cover, which are null when there is nothing to show). Arrays are always present, except when.recurrence.exceptions, which is omitted when there are none.
{
"id": "3f6c1d52-7a0e-4b8e-9d1c-5a2b8e4f6a10",
"eventId": "9b2e7c44-1d3a-4f65-8a7b-0c5d2e9f1b33",
"addedToFeedAt": "2026-10-20T14:03:00.000Z",
"updatedAt": "2026-10-24T09:12:00.000Z",
"status": "active",
"title": "Toddler Story Time",
"description": "Stories, songs and rhymes for children ages 1 to 3 and their caregivers at the Main Library. Free, no registration needed.",
"shortDescription": "Stories and songs for ages 1 to 3.",
"completeness": {
"score": 0.85,
"missing": ["ticketUrl"]
},
"when": {
"pattern": "recurring",
"timezone": "America/New_York",
"displayText": "Alternating Wednesdays, 9:00 AM – 9:45 AM",
"recurrence": {
"description": "9-9:45 a.m. on alternating Wednesdays",
"startDate": "2026-03-04",
"endDate": "2026-12-31",
"exceptions": ["2026-07-08"],
"patterns": [
{
"rule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=WE",
"startTime": "09:00",
"endTime": "09:45",
"description": "Alternating Wednesdays"
}
]
},
"next": {
"start": "2026-11-11T14:00:00.000Z",
"end": "2026-11-11T14:45:00.000Z",
"allDay": false,
"status": "scheduled"
},
"occurrences": [
{
"start": "2026-11-11T14:00:00.000Z",
"end": "2026-11-11T14:45:00.000Z",
"allDay": false,
"status": "scheduled"
},
{
"start": "2026-11-25T14:00:00.000Z",
"end": "2026-11-25T14:45:00.000Z",
"allDay": false,
"status": "scheduled"
},
{
"start": "2026-12-09T14:00:00.000Z",
"end": "2026-12-09T14:45:00.000Z",
"allDay": false,
"status": "scheduled"
},
{
"start": "2026-12-23T14:00:00.000Z",
"end": "2026-12-23T14:45:00.000Z",
"allDay": false,
"status": "scheduled"
}
],
"upcomingCount": 4
},
"where": {
"displayText": "Main Library, 310 North Tryon Street, Charlotte, NC 28202",
"venue": {
"name": "Main Library"
},
"address": {
"display": "310 North Tryon Street, Charlotte, North Carolina 28202, United States",
"street": "310 North Tryon Street",
"city": "Charlotte",
"region": "North Carolina",
"postcode": "28202",
"country": "United States",
"countryCode": "US"
},
"geo": {
"lat": 35.2271,
"lng": -80.8431,
"precision": "address"
}
},
"links": {
"best": {
"url": "https://www.cmlibrary.org/events/toddler-story-time",
"kind": "event_page",
"official": true,
"relation": "confirmed",
"publisher": {
"name": "Charlotte Mecklenburg Library",
"domain": "cmlibrary.org",
"platform": "web",
"role": "organizer",
"roleConfidence": "verified"
},
"publishedAt": "2026-10-01T12:00:00.000Z",
"reason": "official_event_page"
},
"all": [
{
"url": "https://www.cmlibrary.org/events/toddler-story-time",
"kind": "event_page",
"official": true,
"relation": "confirmed",
"publisher": {
"name": "Charlotte Mecklenburg Library",
"domain": "cmlibrary.org",
"platform": "web",
"role": "organizer",
"roleConfidence": "verified"
},
"publishedAt": "2026-10-01T12:00:00.000Z"
}
]
},
"media": {
"cover": {
"url": "https://example.r2.cloudflarestorage.com/media/2026-10/abc.jpg?X-Amz-Signature=...",
"expiresAt": "2026-10-28T12:00:00.000Z",
"usage": "cover_eligible",
"kind": "flyer",
"width": 1080,
"height": 1350,
"source": {
"imageUrl": "https://www.cmlibrary.org/sites/default/files/story-time.jpg",
"pageUrl": "https://www.cmlibrary.org/events/toddler-story-time",
"publisher": {
"name": "Charlotte Mecklenburg Library",
"domain": "cmlibrary.org",
"platform": "web",
"role": "organizer",
"roleConfidence": "verified"
}
}
},
"images": [
{
"url": "https://example.r2.cloudflarestorage.com/media/2026-10/abc.jpg?X-Amz-Signature=...",
"expiresAt": "2026-10-28T12:00:00.000Z",
"usage": "cover_eligible",
"kind": "flyer",
"width": 1080,
"height": 1350,
"source": {
"imageUrl": "https://www.cmlibrary.org/sites/default/files/story-time.jpg",
"pageUrl": "https://www.cmlibrary.org/events/toddler-story-time",
"publisher": {
"name": "Charlotte Mecklenburg Library",
"domain": "cmlibrary.org",
"platform": "web",
"role": "organizer",
"roleConfidence": "verified"
}
}
}
]
}
}
| Field | Type | Description |
|---|---|---|
id | string | Feed item ID. Use it for feedback and per-feed deduplication. |
eventId | string | ID of the event. The same event can be in several feeds: use it for cross-feed deduplication. |
addedToFeedAt | string (ISO 8601) | When the event was added to this feed. |
updatedAt | string (ISO 8601) | When the item last changed in a way subscribers are notified of. See Polling for changes. |
status | string | cancelled when the whole event has been cancelled, else active. |
title | string | Event title. |
description | string | Full description. |
shortDescription | string | One-sentence summary. Omitted when none. |
completeness | object | { score, missing[] }. score is 0 to 1; missing names what we do not know. Omitted for events not yet scored. See below. |
when | object | When the event happens. See when. |
where | object | Where it happens. Omitted when nothing is known. See where. |
links | object | { best, all[] }: the recommended "more info" link and every known URL. See links. |
media | object | { cover, images[] }. See media. |
completeness.missing is a closed list: startDate, startTime, venue, address, endTime, description, officialUrl, ticketUrl, image.
when
| Field | Type | Description |
|---|---|---|
pattern | string | once (a date or a date range), recurring (repeats on a rule) or ongoing (runs continuously over a period). |
timezone | string | IANA timezone, for example America/New_York. The zone the source lists when it is a real region zone, otherwise the venue's. A fixed label from the source, such as EST, stays when the venue cannot be placed. Omitted only when the event has none of these; the clock of such a floating event is what its source gave, which for a Facebook event without a location is UTC. |
displayText | string | Human-readable schedule. Generated in English for one-time and ongoing events (Tue, Nov 3, 2026, 7:00 PM – 10:00 PM, Through Dec 31, 2026). For a recurring event it is the source's description of the pattern, as stored (in English when extraction wrote English), with its times: Alternating Wednesdays, 9:00 AM – 9:45 AM. On an ongoing event a date range with one pair of times (Feb 14 – Feb 16, 2026, 6:00 PM – 9:00 PM) can mean one continuous span from the first day's start to the last day's end, as a Facebook event running 24 hours or more does; the occurrence's start and end give the instants. |
recurrence | object | Present only when pattern is recurring: description, startDate, endDate (omitted when open-ended), exceptions[] (dates the series skips) and patterns[]. |
next | object | The earliest occurrence that has not ended, whatever the query window. Omitted when there is none. When that occurrence falls on a date in recurrence.exceptions, next is the next occurrence in this response that is not on an exception date; on the list, where occurrences is limited to the window, it can then be omitted. |
occurrences | object[] | On the list: the occurrences that overlap the query window. On the single-item endpoint: up to 50 upcoming occurrences. |
upcomingCount | integer | How many occurrences are upcoming, not limited by the window or the 50 cap. |
Each occurrence is { start, end?, allDay, status }. start and end are UTC (ISO 8601); end is omitted when unknown. allDay is true for a date with no time of day. status is scheduled, cancelled, postponed, rescheduled or completed. Occurrences have no id.
Each entry of recurrence.patterns is { rule, startTime?, endTime?, description? }: rule is an iCalendar recurrence rule such as FREQ=WEEKLY;INTERVAL=2;BYDAY=WE, and the times are local HH:mm. An event that repeats on two rules has two entries. Occurrences whose local date (in when.timezone, else UTC) is listed in recurrence.exceptions are left out of occurrences and next. upcomingCount can still count one stored before the exception was recorded, until the series is regenerated.
Upcoming means "has not ended". An occurrence stays upcoming until it ends. A date with no time of day stays upcoming until that date is over everywhere. A time given without a timezone is shown as its source gave it (when.timezone is omitted) and stays upcoming until its listed date is over.
where
| Field | Type | Description |
|---|---|---|
displayText | string | Human-readable location: venue and short address (venue, street, city, region postcode, a US state as its two-letter code, no country) when the street is known or the point is town-level (locality, no street); else the venue and the stored address without its country (a US state shortened to its code); else the text as stated, or Online. |
venue | object | { name }. Omitted when unknown or when multipleVenues is true. |
address | object | { display, street?, city?, region?, postcode?, country?, countryCode? }. display is the full normalized address; the rest are omitted when unknown. countryCode is ISO 3166-1 alpha-2. Omitted when unknown or when multipleVenues is true. |
geo | object | { lat, lng, precision? }. precision is address (a street address or a building) or locality (a town, neighborhood, district or postcode area). Omitted when the location was not geocoded or when multipleVenues is true. |
online | object | { url, platform? }: where to join or watch. Present for virtual and hybrid events. |
multipleVenues | boolean | true when the event takes place at several venues. venue, address and geo are omitted then. |
street, postcode, countryCode and precision appear as venues are (re)geocoded. Older events may lack them.
links
links.all holds every URL we know for the event, once each, best first. links.best is the recommended "more info" link: the first entry of all that qualifies (see the order below), plus a reason. It is null when the event has no URL, or no URL that qualifies.
Each link has:
| Field | Type | Description |
|---|---|---|
url | string | Canonical http(s) URL. |
kind | string | event_page, tickets, registration, streaming, calendar, social_post, social_profile or other. |
official | boolean | true when the publisher is the organizer, the venue or a performer. |
relation | string | origin: the document the event was found in. confirmed: a page we read and judged to be about this event. supporting: a related page we read. mentioned: a URL stated on a page we read, not read itself. |
publisher | object | Who published the page: name, domain (websites), handle (social accounts), platform (web, instagram, facebook, tiktok or twitter), role and roleConfidence. name, domain, handle and roleConfidence are omitted when unknown. |
publishedAt | string | When the page or post was published (ISO 8601). Omitted when unknown, and for URLs we did not read. |
publisher.role is organizer, venue, performer, ticketing (sells tickets or lists events), curator, media (these two write about events) or unknown. organizer, venue and performer are official. roleConfidence is verified when a link confirms the role (the publisher's profile links to the page, or the page links back), and inferred otherwise. It is almost always inferred: treat role as a good guess.
links.best has the same fields plus reason:
reason | Meaning |
|---|---|
official_event_page | The organizer's, venue's or performer's own page for the event. |
official_tickets | An official ticket or registration page. |
official_social_post | An official social post about the event. |
ticketing_page | A ticketing site's page for the event. |
event_page | Any other event page or post, not from a curator or media site. |
curator_or_media_page | A page by a curator or media site. It is the best link only when nothing else exists. |
Ranking. Links are ranked by tier, best first:
- An official event page.
- An official tickets or registration page.
- An official social post.
- A ticketing site's page: an event page or tickets link whose publisher has the
ticketingrole or is on a known ticketing site. - Any other event page or social post, unless a curator or media site published it.
- A page or post by a curator or media site.
- Everything else: streaming links, calendars, social profiles, other URLs, and tickets or registration links that are not official and not on a ticketing site.
Within a tier, verified roles come before inferred ones, then by relation: confirmed, origin, mentioned, supporting. links.best comes from tiers 1 to 6, so a curator's or media page is best only when nothing else exists, and reason says so. Tier 7 links are never best.
media
| Field | Type | Description |
|---|---|---|
cover | object|null | The chosen event image, or null when there is none or it cannot be served right now. Official images are preferred. |
images | object[] | Every image the event serves, whatever its usage, in rank order (official images first). Empty when none. |
cover can be null while images has entries: the chosen image cannot be served right now (its stored copy is gone or its link could not be made). Show no event image then; do not promote another image to cover. Read the cover null-safely (media.cover?.url).
The cover is an official image when one shows the event; an image from a curator, a media outlet or an unknown site (reference_only) is the cover only when no official one does. Check usage before you show an image as your own.
Each image has:
urlandexpiresAt: the image link and when it stops working. The link is signed and temporary: never store it. Fetch the item again for a current one.usage: what you may do with the image.cover_eligible: an official image (the organizer's, venue's, performer's or ticket seller's website); may be shown as the event image.display_with_attribution: an official social post's image; show it only with itssourcebeside it.reference_only: the image came from a curator, a media outlet or an unknown site. It is there for reference: credit itssourceand do not republish it as your own.- An image's
usagecan change when we learn more about where it came from. A change ofusagealone (the same images in the same order) sends noevent.item.updated: readusageevery time you fetch the item, as you do for the signedurl.
kind:flyer,photo,social_slideorvideo_thumbnail.width,height: pixels (omitted when unknown).source: where it came from.imageUrlis the original address, for provenance only: do not display it.pageUrl(the page the image came from) andpublisher(same fields as a link's publisher) are omitted when unknown.
An image the safety floor does not allow (see Which Events a Feed Carries) is not served: it is absent from cover and images, and another image becomes the cover when one is available. An event that serves no image has media: { "cover": null, "images": [] }.
Only http(s) URLs
Every URL in an item is an http or https URL. Other URLs are dropped: a link entry, where.online, or a media source.pageUrl. An image whose original URL is not http(s) is left out of media.
Examples
# Upcoming events (next 30 days)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items" \
-H "Authorization: Bearer YOUR_API_KEY"
# Recurring events with an official link and a cover
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?pattern=recurring&hasOfficialLink=true&hasCover=true&minCompleteness=0.7" \
-H "Authorization: Bearer YOUR_API_KEY"
# Items that changed since your last poll (inside the default window)
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?updatedSince=2026-10-24T00:00:00Z&orderBy=created" \
-H "Authorization: Bearer YOUR_API_KEY"
# Events without an official link, to review
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?hasOfficialLink=false" \
-H "Authorization: Bearer YOUR_API_KEY"
Polling for changes
updatedSince is combined with the occurrence window like every other filter, so an item whose dates moved out of the window, or whose last occurrence has ended, is not returned even though it changed. For complete change tracking, including removals, use Event Delivery webhooks.
updatedAt is stamped just before the change is committed, so a change committed during your poll can carry a time slightly before it. Poll from a few seconds before your previous poll's time (for example updatedSince = last poll time minus 10 seconds) and de-duplicate by id and updatedAt.
Pagination
Use limit (max 100) and offset. Check pagination.hasMore to know whether another page exists.
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=20&offset=0" \
-H "Authorization: Bearer YOUR_API_KEY"
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items?limit=20&offset=20" \
-H "Authorization: Bearer YOUR_API_KEY"
Understanding query behavior
- The window filters on occurrence dates, not on when an event was added. An event added months ago appears if it has an occurrence overlapping the window.
- A recurring event appears when any occurrence overlaps the window. A multi-day event appears when one of its occurrences does: a date range stored as one spanning occurrence appears for as long as any part of the range falls inside the window.
statusmatches the status of an occurrence. To drop events that were cancelled as a whole, check the item's top-levelstatus.
Get a Single Feed Item
Returns one item by its itemId, in the same shape as each element of Get Feed Items. Use it to re-fetch the latest version of an event, for example after an event.item.updated webhook.
GET /feeds/events/{feedId}/items/{itemId}
Request
Path parameters:
| Name | Type | Description |
|---|---|---|
feedId | string (UUID) | Event feed identifier |
itemId | string (UUID) | Feed item identifier (from the id field) |
The endpoint takes no query parameters. An include parameter returns 400.
Headers:
Authorization: Bearer YOUR_API_KEY
Response
Status: 200 OK
data is one item. There is no window: when.occurrences holds up to the next 50 upcoming occurrences, and when.next and when.upcomingCount cover every upcoming one.
curl "https://api.feeds.onhelix.ai/feeds/events/{feedId}/items/{itemId}" \
-H "Authorization: Bearer YOUR_API_KEY"
Errors
400:includewas sent, or a malformed UUID.401: Missing or invalid API key.404: Feed or item not found, the item belongs to a different feed, or your organization has no access to the feed.
Error Responses
Every error uses the same envelope. error.code says what went wrong and
error.message gives the detail, naming the field or parameter at fault.
{
"success": false,
"error": {
"code": "INVALID_BODY",
"message": "Invalid request body: Name is required"
}
}
| Status | error.code | When |
|---|---|---|
400 | INVALID_BODY | The request body is malformed or fails validation (a missing or invalid field, an unknown config key). Also 415 for an unsupported content type and 413 for a body too large. |
400 | INVALID_PARAMS | A path parameter is invalid, such as a malformed feed or item UUID. |
400 | INVALID_QUERY | A query parameter is invalid: a bad pattern, a date outside the window, an unknown orderBy, or an include parameter. The message names the parameter. |
401 | AUTHENTICATION_FAILED | Missing or invalid API key. |
403 | PERMISSION_DENIED | Your organization subscribes to the feed but does not own it, and the request changes the feed or reads its filters. |
404 | NOT_FOUND | The feed, item, source or filter does not exist, is not on this feed, or belongs to a feed your organization has no access to. |
409 | ALREADY_EXISTS | Another event feed of your organization has this name, or the feed already has this site. |
422 | VALIDATION_FAILED | enabled: true on a feed without an include filter (including on create), or a source URL that cannot be added. |
429 | RATE_LIMITED | Too many source adds (the place-search limit applies to /processors). Retry after the Retry-After header (seconds). |
503 | SERVICE_UNAVAILABLE | A service the request needs cannot be reached right now. Retry after the Retry-After header. |
500 | INTERNAL_ERROR | Unexpected server error. Retry the request; if it persists, contact support. |
The filter and source routes have their own codes (SOURCE_TYPE_NOT_ALLOWED,
INVALID_CONDITION, LAST_INCLUDE_FILTER and others), listed in
Sources and Filters.
A 401:
{
"success": false,
"error": {
"code": "AUTHENTICATION_FAILED",
"message": "Authentication failed for strategy: api-key"
}
}
A 404:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "EventFeed with id '550e8400-e29b-41d4-a716-446655440000' not found in database"
}
}
Best Practices
Date Range Queries
Use specific date ranges for better performance:
// Good: Specific date range (the next three days, from now)
const since = new Date();
const until = new Date(since.getTime() + 3 * 24 * 60 * 60 * 1000);
const nextThreeDays = await fetch(
`https://api.feeds.onhelix.ai/feeds/events/${feedId}/items?since=${since.toISOString()}&until=${until.toISOString()}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
// Also good: Let API calculate default 30-day range
const upcoming = await fetch(
`https://api.feeds.onhelix.ai/feeds/events/${feedId}/items`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
Remember date range limits:
- Maximum 7 days in the past
- Maximum 30 days in the future
Working with Occurrences
Pick what to show from when:
function formatEventDates(item) {
const { when } = item;
if (item.status === 'cancelled') return 'Cancelled';
if (!when.next) return 'No upcoming dates';
// One date: the schedule text says it best
if (when.upcomingCount === 1) return when.displayText;
// Several: the next date and how many remain
const next = new Date(when.next.start).toLocaleDateString();
return `${when.displayText} (next: ${next}, ${when.upcomingCount} upcoming)`;
}
when.occurrences lists the occurrences that overlap your query window, and when.upcomingCount counts every upcoming one, so use the count, not the array length, to say how many remain.
Picking the more-info link:
const moreInfoUrl = item.links.best?.url ?? null;
Use links.best when you show one link. Show nothing when it is null. See Concepts.
Pagination
When retrieving large numbers of events, use pagination:
async function getAllFeedEvents(feedId, dateRange) {
const allEvents = [];
let offset = 0;
const limit = 100; // Maximum allowed
while (true) {
const url = new URL(
`https://api.feeds.onhelix.ai/feeds/events/${feedId}/items`
);
url.searchParams.set('limit', limit);
url.searchParams.set('offset', offset);
if (dateRange.since) url.searchParams.set('since', dateRange.since);
if (dateRange.until) url.searchParams.set('until', dateRange.until);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const result = await response.json();
if (result.data.items.length === 0) break;
allEvents.push(...result.data.items);
// Check if more items available
if (!result.data.pagination.hasMore) break;
offset += result.data.items.length;
}
return allEvents;
}
Filtering by Status
Get only items with a scheduled occurrence (status matches an occurrence's status; a whole-event cancellation is the item's top-level status):
const activeEvents = await fetch(
`https://api.feeds.onhelix.ai/feeds/events/${feedId}/items?status=scheduled`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
Handle cancelled events in UI:
function EventCard({ event }) {
if (event.status === 'cancelled') {
return (
<div className="event-card cancelled">
<h3>{event.title}</h3>
<p className="status-badge">CANCELLED</p>
{/* Show event details but mark as cancelled */}
</div>
);
}
return <div className="event-card">{/* Normal event display */}</div>;
}
Submit Item Feedback
Submits feedback for a specific event feed item. Use this to indicate whether an event was relevant or useful.
POST /feeds/events/{feedId}/items/{itemId}/feedback
Request
Path parameters:
| Name | Type | Description |
|---|---|---|
feedId | string (UUID) | Event feed identifier |
itemId | string (UUID) | Event feed item identifier (from id field) |
Headers:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Body:
{
"rating": "positive",
"comment": "This event was exactly what I was looking for."
}
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
rating | string | Yes | Feedback rating: positive, negative, or neutral |
comment | string | No | Optional comment explaining the feedback. Maximum 5000 characters. |
Response
Status: 200 OK
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440099"
}
}
Response fields:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique feedback identifier |
Examples
Positive feedback with comment:
curl -X POST https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000/items/770e8400-e29b-41d4-a716-446655440005/feedback \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rating": "positive",
"comment": "Perfect event for our community calendar!"
}'
Negative feedback without comment:
curl -X POST https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000/items/770e8400-e29b-41d4-a716-446655440005/feedback \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rating": "negative"
}'
Neutral feedback:
curl -X POST https://api.feeds.onhelix.ai/feeds/events/550e8400-e29b-41d4-a716-446655440000/items/770e8400-e29b-41d4-a716-446655440005/feedback \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rating": "neutral",
"comment": "Event was okay but not quite what we needed."
}'
Errors
400: Invalid rating value, invalid UUID format, or comment exceeds maximum length401: Missing or invalid API key404: Feed or item not found, or you don't have access to this feed
Notes
- Feedback is additive: you can submit multiple feedback entries for the same item
- You can only submit feedback for items in feeds you own or are subscribed to
- Feedback helps improve event relevance recommendations over time
Next Steps
- Event Feed Overview: Understand event feeds and source types
- Feed Configuration: Processors, filters, opt-in filters, areas and sources
- Event Feed Concepts: Learn about the event data model and occurrence aggregation
- Quickstart Guide: Build a feed end to end
- Event Delivery: Configure destinations and subscriptions to receive events in real time
- Authentication: API key best practices