Skip to main content

Event Feed API Reference

Complete API reference for Helix Event Feeds. All endpoints require authentication via API key.

Interactive playground

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:


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:

NameTypeRequiredDescription
namestringYesFeed name. Minimum 1 character. Unique within your organization, ignoring case and surrounding whitespace.
descriptionstringYesFeed description. Minimum 1 character.
enabledbooleanNoA 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.
configobjectNoFeed configuration options. Keys other than the ones below are refused with 400.
config.versionnumberNoConfig schema version. Must be 1. Defaults to 1.
config.media.retentionDaysAfterEndnumberNoDays 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:

FieldTypeDescription
idstring (UUID)Unique feed identifier
namestringFeed name
descriptionstringFeed description
organizationIdstring (UUID)The organization that owns the feed
enabledbooleanWhether the feed is active: an enabled feed collects events, a disabled one keeps its items but takes no new ones
enabledSourcestringHow 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.
enabledReasonstring|nullThe reason recorded when enabled was last set, if any. You cannot set it.
configobjectFeed 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.
createdAtstring (ISO 8601)Feed creation timestamp
updatedAtstring (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 is INVALID_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 is ALREADY_EXISTS.
  • 422: enabled: true. The error code is VALIDATION_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:

NameTypeDescription
idstring (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 is INVALID_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:

NameTypeDescription
idstring (UUID)Event feed ID

Body:

{
"name": "Updated Events Feed",
"description": "Updated description",
"enabled": false,
"config": {
"version": 1
}
}

Parameters:

NameTypeRequiredDescription
namestringNoNew feed name. Minimum 1 character. Unique within your organization, ignoring case and surrounding whitespace.
descriptionstringNoNew feed description. Minimum 1 character.
enabledbooleanNoEnable 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.
configobjectNoUpdated 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.versionnumberNoConfig schema version. Must be 1 if sent.
config.media.retentionDaysAfterEndnumberNoDays 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 is ALREADY_EXISTS.
  • 422: enabled: true on a feed that has no include filter. The error code is VALIDATION_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:

NameTypeDescription
idstring (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​

RouteDoes
GET /feeds/events/{id}/sourcesEvery source on the feed, newest first
POST /feeds/events/{id}/sourcesAdd 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​

RouteDoes
GET /feeds/events/{id}/filtersThe feed's own and opt-in filters
GET /feeds/events/{id}/filters/{filterId}One filter
POST /feeds/events/{id}/filtersAdd 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:

NameTypeDescription
feedIdstring (UUID)Event feed ID

Headers:

Authorization: Bearer YOUR_API_KEY

Query parameters:

NameTypeDefaultDescription
limitinteger20Items per page. Min 1, max 100.
offsetinteger0Items to skip.
sincedate (ISO 8601)nowStart of the occurrence window.
untildate (ISO 8601)since + 30 daysEnd of the occurrence window.
statusstringallOnly items with an occurrence of this status: scheduled, cancelled, postponed, rescheduled, completed.
addedSincedate (ISO 8601)-Only items added to the feed at or after this time.
updatedSincedate (ISO 8601)-Only items whose updatedAt is at or after this time, inside the occurrence window. See "Polling for changes".
patternstring-Only items with this schedule: once, recurring or ongoing.
hasOfficialLinkboolean-true: only items with a link published by the organizer, venue or a performer. false: only items without one.
hasCoverboolean-true: only items with a cover image. false: only items without one.
minCompletenessnumber 0 to 1-Only items whose completeness.score is at least this value.
orderBystringoccurrenceoccurrence: soonest first. created: newest added first.

The filters combine with AND, and with the occurrence window.

Occurrence window:

  1. No dates: occurrences from now until 30 days from now.
  2. Only since: from since for 30 days, at most until 30 days from now.
  3. Only until: from now until until. When until is already past, the window starts 7 days ago.
  4. 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"
}
}
}
]
}
}
FieldTypeDescription
idstringFeed item ID. Use it for feedback and per-feed deduplication.
eventIdstringID of the event. The same event can be in several feeds: use it for cross-feed deduplication.
addedToFeedAtstring (ISO 8601)When the event was added to this feed.
updatedAtstring (ISO 8601)When the item last changed in a way subscribers are notified of. See Polling for changes.
statusstringcancelled when the whole event has been cancelled, else active.
titlestringEvent title.
descriptionstringFull description.
shortDescriptionstringOne-sentence summary. Omitted when none.
completenessobject{ score, missing[] }. score is 0 to 1; missing names what we do not know. Omitted for events not yet scored. See below.
whenobjectWhen the event happens. See when.
whereobjectWhere it happens. Omitted when nothing is known. See where.
linksobject{ best, all[] }: the recommended "more info" link and every known URL. See links.
mediaobject{ cover, images[] }. See media.

completeness.missing is a closed list: startDate, startTime, venue, address, endTime, description, officialUrl, ticketUrl, image.

when​
FieldTypeDescription
patternstringonce (a date or a date range), recurring (repeats on a rule) or ongoing (runs continuously over a period).
timezonestringIANA 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.
displayTextstringHuman-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.
recurrenceobjectPresent only when pattern is recurring: description, startDate, endDate (omitted when open-ended), exceptions[] (dates the series skips) and patterns[].
nextobjectThe 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.
occurrencesobject[]On the list: the occurrences that overlap the query window. On the single-item endpoint: up to 50 upcoming occurrences.
upcomingCountintegerHow 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​
FieldTypeDescription
displayTextstringHuman-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.
venueobject{ name }. Omitted when unknown or when multipleVenues is true.
addressobject{ 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.
geoobject{ 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.
onlineobject{ url, platform? }: where to join or watch. Present for virtual and hybrid events.
multipleVenuesbooleantrue 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.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:

FieldTypeDescription
urlstringCanonical http(s) URL.
kindstringevent_page, tickets, registration, streaming, calendar, social_post, social_profile or other.
officialbooleantrue when the publisher is the organizer, the venue or a performer.
relationstringorigin: 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.
publisherobjectWho 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.
publishedAtstringWhen 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:

reasonMeaning
official_event_pageThe organizer's, venue's or performer's own page for the event.
official_ticketsAn official ticket or registration page.
official_social_postAn official social post about the event.
ticketing_pageA ticketing site's page for the event.
event_pageAny other event page or post, not from a curator or media site.
curator_or_media_pageA 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:

  1. An official event page.
  2. An official tickets or registration page.
  3. An official social post.
  4. A ticketing site's page: an event page or tickets link whose publisher has the ticketing role or is on a known ticketing site.
  5. Any other event page or social post, unless a curator or media site published it.
  6. A page or post by a curator or media site.
  7. 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​
FieldTypeDescription
coverobject|nullThe chosen event image, or null when there is none or it cannot be served right now. Official images are preferred.
imagesobject[]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:

  • url and expiresAt: 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 its source beside it.
    • reference_only: the image came from a curator, a media outlet or an unknown site. It is there for reference: credit its source and do not republish it as your own.
    • An image's usage can change when we learn more about where it came from. A change of usage alone (the same images in the same order) sends no event.item.updated: read usage every time you fetch the item, as you do for the signed url.
  • kind: flyer, photo, social_slide or video_thumbnail.
  • width, height: pixels (omitted when unknown).
  • source: where it came from. imageUrl is the original address, for provenance only: do not display it. pageUrl (the page the image came from) and publisher (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.
  • status matches the status of an occurrence. To drop events that were cancelled as a whole, check the item's top-level status.

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:

NameTypeDescription
feedIdstring (UUID)Event feed identifier
itemIdstring (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: include was 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"
}
}
Statuserror.codeWhen
400INVALID_BODYThe 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.
400INVALID_PARAMSA path parameter is invalid, such as a malformed feed or item UUID.
400INVALID_QUERYA query parameter is invalid: a bad pattern, a date outside the window, an unknown orderBy, or an include parameter. The message names the parameter.
401AUTHENTICATION_FAILEDMissing or invalid API key.
403PERMISSION_DENIEDYour organization subscribes to the feed but does not own it, and the request changes the feed or reads its filters.
404NOT_FOUNDThe feed, item, source or filter does not exist, is not on this feed, or belongs to a feed your organization has no access to.
409ALREADY_EXISTSAnother event feed of your organization has this name, or the feed already has this site.
422VALIDATION_FAILEDenabled: true on a feed without an include filter (including on create), or a source URL that cannot be added.
429RATE_LIMITEDToo many source adds (the place-search limit applies to /processors). Retry after the Retry-After header (seconds).
503SERVICE_UNAVAILABLEA service the request needs cannot be reached right now. Retry after the Retry-After header.
500INTERNAL_ERRORUnexpected 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:

NameTypeDescription
feedIdstring (UUID)Event feed identifier
itemIdstring (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:

NameTypeRequiredDescription
ratingstringYesFeedback rating: positive, negative, or neutral
commentstringNoOptional comment explaining the feedback. Maximum 5000 characters.

Response​

Status: 200 OK

{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440099"
}
}

Response fields:

FieldTypeDescription
idstring (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 length
  • 401: Missing or invalid API key
  • 404: 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​