Skip to content

Feed API

Global mixed-content feed returning published events, places, and stories in a single ranked response. Authentication is optional.

Status

Implemented — GET /api/v1/feed is live. Tracked under KUB-174.


Get Feed

http
GET /api/v1/feed

Returns a mixed-content feed built from published events, places, and stories. Unauthenticated and guest callers receive the shared global ranking with the anonymous rate limit; signed-in callers receive the same ranking with the higher authenticated budget.

Authentication

Optional. Pass either a Bearer token in the Authorization header or a valid Better Auth session cookie to receive an authenticated response and the higher rate limit.

Rate Limits

Caller typeRequests / minute
Unauthenticated30
Authenticated120

Rate limit state is communicated via response headers (see below).

Query Parameters

ParameterTypeDefaultDescription
limitnumber20Number of items to return. Maximum 100.
typesstringComma-separated list of content types to include: event, place, story. Omit to include all.

Response Headers

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix epoch seconds when the current window resets.

Response 200 OK

json
{
	"data": [
		{
			"id": "event:101",
			"type": "event",
			"title": "Carnival Road March",
			"subtitle": "Annual Dominica Carnival celebration",
			"image_url": null,
			"deeplink": "kubuli://events/101",
			"published_at": "2025-02-10T08:00:00.000Z",
			"rank": 1,
			"data": {
				"id": "101",
				"slug": "carnival-road-march",
				"title": "Carnival Road March",
				"starts_at": "2025-02-10T08:00:00.000Z"
			},
			"context": { "location": "Roseau" }
		},
		{
			"id": "place:201",
			"type": "place",
			"title": "Trafalgar Falls",
			"subtitle": "Twin waterfalls in the Roseau Valley",
			"image_url": null,
			"deeplink": "kubuli://places/201",
			"published_at": "2025-01-22T09:00:00.000Z",
			"rank": 2,
			"data": {
				"id": "201",
				"slug": "trafalgar-falls",
				"name": "Trafalgar Falls",
				"place_kind": "poi"
			},
			"context": { "location": "Roseau Valley", "kind": "poi" }
		}
	],
	"meta": {
		"cursor": null
	}
}

Feed Item Schema

FieldTypeNullableDescription
idstringNoUnique item identifier.
typeevent | place | storyNoContent type.
titlestringNoDisplay title.
subtitlestringYesSecondary display text.
image_urlstringYesAbsolute URL to cover image.
deeplinkstringYesApp deeplink URI for navigation.
published_atstring (ISO 8601)YesPublication timestamp.
rankintegerNoSort rank within the feed response.
dataobjectNoOriginal source item payload.
contextobjectNoType-specific metadata (arbitrary keys).

Response 429 Too Many Requests

Returned when the caller exceeds their rate limit. Includes the standard X-RateLimit-* headers so clients can back off correctly.

json
{
	"error": {
		"code": "rate_limited",
		"message": "Too many requests. Please slow down.",
		"status": 429
	}
}

Feed Dashboard

http
GET /api/v1/feed/dashboard

Returns five aggregate sections for the main discovery dashboard in a single response.

Authentication

Optional. Signed-in callers may authenticate with either a bearer JWT or a valid Better Auth session cookie to receive the higher dashboard rate-limit budget.

Rate Limits

Caller typeRequests / minute
Unauthenticated30
Authenticated120

Query Parameters

ParameterTypeRequiredDescription
latnumberNoLatitude. Required together with lng to populate nearby_places.
lngnumberNoLongitude. Required together with lat.

Response 200 OK

json
{
	"data": {
		"popular_categories": [
			{
				"id": "1",
				"name": "Beaches",
				"slug": "beaches",
				"icon": null,
				"color": null,
				"place_count": 18
			}
		],
		"nearby_places": [
			{
				"id": "201",
				"name": "Trafalgar Falls",
				"slug": "trafalgar-falls",
				"avg_rating": null,
				"distance_meters": 500.0
			}
		],
		"popular_places": [
			{
				"id": "201",
				"name": "Trafalgar Falls",
				"slug": "trafalgar-falls",
				"avg_rating": null,
				"distance_meters": null
			}
		],
		"upcoming_events": [
			{
				"id": "101",
				"title": "Carnival Road March",
				"slug": "carnival-road-march",
				"starts_at": "2025-02-10T08:00:00.000Z",
				"is_all_day": false,
				"timezone": "America/Dominica"
			}
		],
		"relevant_stories": [
			{
				"id": "301",
				"title": "The History of the Kalinago People",
				"slug": "history-of-the-kalinago-people",
				"published_at": "2025-02-10T08:00:00.000Z",
				"read_time_minutes": 3,
				"culture_topic": {
					"id": "21",
					"name": "Dominica Heritage",
					"slug": "dominica-heritage"
				}
			}
		]
	}
}

Response Sections

SectionLimitDescription
popular_categories6Place categories ordered by place count descending.
nearby_places6Published places nearest to the supplied coordinates; empty array when coords absent.
popular_places6Published places ordered by average rating and review count descending.
upcoming_events6Published events ordered by start time ascending.
relevant_stories5Published culture stories ordered by display order then publication date.

Response 400 Bad Request

Returned when only one of lat/lng is supplied.

Response 429 Too Many Requests

Returned when the caller exceeds the dashboard rate limit. Successful and rate-limited responses include the standard X-RateLimit-* headers.


Notes

  • The feed is assembled from published events, places, and stories in the canonical content tables.
  • Signed-in users receive the higher authenticated budget. Guest sessions remain on the anonymous budget.
  • Cursor-based pagination is reserved for future use; meta.cursor is always null and no cursor query parameter is supported yet.
  • The types filter silently ignores unrecognised type values; if all values are unrecognised the filter is not applied.

Built with VitePress