Skip to content

Content API

The Content API returns your topics’ content as JSON, in the same order your widgets show it: pinned items first, then the newest posts. Use it to build your own pages, apps or newsletters.

https://q.loop-feed.com/v1/
EndpointAuthWhat it returns
GET /v1/topic/{topicId}/contentNoneA topic’s content
GET /v1/projectsAPI keyYour organization’s projects
GET /v1/project/{projectId}/topicsAPI keyA project’s active topics
GET /v1/topics/searchAPI keyTopics by team, competition or custom field
GET /v1/topic/{topicId}/count DeprecatedNoneA capped item count
  1. Copy a topic’s address from Topic JSON Feed on its Details tab in the dashboard, or list your topics with an API key.

  2. Request it:

    Terminal window
    curl "https://q.loop-feed.com/v1/topic/your-topic-id/content?limit=10"
  3. Show the items. Link each one to its url, and credit its author. See Branding.

A topic’s content is public: anyone with its topic ID can read it. The endpoints that list or search your projects and topics need an API key. Create one in the dashboard under Settings → API Keys and send it in the X-Api-Key header:

X-Api-Key: your-api-key

Requests with a missing or invalid key get 401 with a plain-text body: API key is required or Invalid API key.


GET https://q.loop-feed.com/v1/projects
X-Api-Key: your-api-key

Returns your organization’s projects, in no particular order.

[
{ "id": "cmh3y8k2p0001demo5web00001", "name": "Website" },
{ "id": "cmh3y8k2p0002demo5app00002", "name": "Mobile App" }
]

GET https://q.loop-feed.com/v1/project/{projectId}/topics
X-Api-Key: your-api-key

Returns the project’s active topics, sorted by name. {projectId} can also be the project’s name. A project that doesn’t exist, or belongs to another organization, returns an empty array.

[
{
"id": "cmhujzde8gxd6ncf10epf91dh",
"name": "Arsenal",
"slug": "arsenal",
"url": "https://q.loop-feed.com/v1/topic/cmhujzde8gxd6ncf10epf91dh/content",
"metadata": {
"sport": "football",
"teams": [{ "id": "9", "name": "Arsenal" }],
"tournaments": [{ "id": "6409", "name": "England Premier League" }],
"teamIds": ["9"],
"tournamentIds": ["6409"],
"custom": { "desk": "clubs" }
}
}
]

What you set on the topic’s Details tab in the dashboard:

PropertyTypeDescription
sportStringfootball, rugby (rugby league) or rugby-union
teamsArrayTeams from Football Association, each { id, name }
tournamentsArrayLeagues and tournaments from Football Association, each { id, name }
matchesArrayMatches from Sports Matches, each with matchId, teams, competition and startDateTime
teamIdsArrayThe IDs of teams, plus both teams of every attached match
tournamentIdsArrayThe IDs of tournaments, plus the tournament of every attached match
matchIds, leagueIdsArrayThe IDs of the attached matches and their leagues
customObjectYour Custom Fields. Values are strings or null.

Team, tournament, league and match IDs are those of LoopFeed’s sports data provider: the same IDs the Feed API and AI curation use.


GET https://q.loop-feed.com/v1/topics/search?teamId=9
X-Api-Key: your-api-key

Finds your organization’s topics by team, competition or custom field. Returns the same shape as Get topics for a project.

ParameterDescription
teamIdTopics whose teamIds include this team
tournamentIdTopics whose tournamentIds include this tournament
leagueIdSame as tournamentId. Ignored when tournamentId is given.
key and valueTopics whose custom field key equals value exactly. value may be empty but must be present.
projectIdOnly topics of this project

Give at least one of teamId, tournamentId, leagueId or key. Several filters must all match.

# Topics about a team
GET /v1/topics/search?teamId=9
# Topics about a competition
GET /v1/topics/search?tournamentId=6409
# Topics with a custom field, in one project
GET /v1/topics/search?key=desk&value=clubs&projectId=cmh3y8k2p0001demo5web00001

Errors (400):

  • {"error": "At least one filter is required: teamId, tournamentId/leagueId, or key+value"}
  • {"error": "value is required when key is provided"}

GET https://q.loop-feed.com/v1/topic/{topicId}/content

Returns the topic’s items: all pinned items first, in your order, then the newest items by publication date. Hidden items are never returned. No API key is needed.

ParameterDefaultDescription
limit20Newest items to return, up to 100.
offset0Newest items to skip, for paging.
sinceOnly items published at or after this time (Unix seconds).
untilOnly items published at or before this time (Unix seconds).
serviceOnly one platform: youtube, twitter, instagram or rss.
contentTypeOnly one content type, such as highlights. Needs AI curation.
matchIdOnly items about this match (its numeric ID). Needs AI curation or matches set in the queue.
pinnedOnlytrue returns only the pinned items.
includemeta wraps the response as { teamIds, tournamentIds, content } with the topic’s team and tournament IDs.

With AI curation, every item has a content type and, often, a match. Filter by them:

Terminal window
# Only highlights
curl "https://q.loop-feed.com/v1/topic/your-topic-id/content?contentType=highlights"
# Everything about one match
curl "https://q.loop-feed.com/v1/topic/your-topic-id/content?matchId=518066"
[
{
"id": "cmh0x1y2z0001abcdpost00001",
"externalId": "m0G_UKNoSqY",
"service": "youtube",
"type": "video",
"title": "MADUEKE THE MATCHWINNER! | EXTENDED HIGHLIGHTS | Nottingham Forest vs Arsenal (0-1)",
"text": "Noni Madueke's late goal was enough to secure a valuable 1-0 win away to Nottingham Forest...",
"url": "https://www.youtube.com/watch?v=m0G_UKNoSqY",
"duration": 612,
"media": [
{ "type": "embed", "embed": "<iframe src='https://www.youtube.com/embed/m0G_UKNoSqY'></iframe>", "url": null, "width": null, "height": null, "duration": 612 },
{ "type": "image", "embed": null, "url": "https://i.ytimg.com/vi/m0G_UKNoSqY/maxresdefault.jpg", "width": 1280, "height": 720, "duration": null }
],
"language": "en",
"date": "2026-10-19T23:00:00Z",
"created": "2026-10-19T23:04:12.521Z",
"author": {
"name": "Arsenal",
"userid": "UCpryVRk_VDudG8SHXgWcG0w",
"username": "Arsenal",
"url": "https://youtube.com/channel/UCpryVRk_VDudG8SHXgWcG0w",
"image": null,
"verified": true
},
"metadata": {
"contentType": "highlights",
"matches": [
{
"sport": "football", "matchId": "518066",
"homeTeamId": "11", "homeTeamName": "Nottingham Forest",
"awayTeamId": "9", "awayTeamName": "Arsenal",
"leagueId": "10001", "leagueName": "England - Premier League",
"tournamentId": "6409", "startDateTime": "2026-10-18T15:30:00.000Z",
"source": "ai"
}
],
"entities": [
{ "sport": "football", "kind": "team", "id": "9", "name": "Arsenal", "source": "ai" },
{ "sport": "football", "kind": "player", "id": "4410", "name": "Noni Madueke", "source": "ai" }
],
"custom": {}
},
"kudos": { "score": 0 },
"pinned": true,
"sortOrder": 1
}
]

A topic that doesn’t exist returns an empty array.

PropertyTypeDescription
idStringLoopFeed’s ID for the post
externalIdStringThe platform’s own ID, such as the YouTube video ID
serviceString or nullyoutube, twitter, instagram or rss
typeStringvideo (YouTube), status (X), link (RSS), photo or video (Instagram)
titleString or nullThe title, when the platform has one
textString or nullThe text or description. May contain HTML such as <br/>.
urlStringThe original post
durationInteger or nullVideo length in seconds
mediaArray or nullPictures and players. Each has type (image, photo, video or embed), url, embed (HTML), width, height and duration; unused fields are null.
languageStringISO 639-1 language code, such as en
dateStringWhen the post was published (RFC 3339, UTC)
createdStringWhen LoopFeed stored it
authorObjectname, userid, username, url, image and verified. url, image and verified may be null.
metadataObject or nullcontentType, matches, entities and custom, described below
kudosObjectReserved. Its values are currently always 0.
pinnedBooleanWhether the item is pinned
sortOrderIntegerThe pinned item’s position (1 is first); 0 for other items

metadata holds the item’s labels:

PropertyDescription
contentTypeThe content type. Present only when set.
matchesThe match the item is about: matchId, both teams, competition and kick-off startDateTime. source is ai (AI curation), review (LoopFeed reviewers) or manual (your editors). Match status and score, when present, are a snapshot from when the match was attached.
entitiesTeams, players and competitions the item is about: sport, kind (team, player or competition), id, name and source. Present only when set.
customThe item’s Custom Fields from the queue.

Responses are cached for up to a minute, by LoopFeed and its CDN. Changes you make in LoopFeed, such as pinning or hiding an item, clear them within seconds; new posts appear within a minute. Browsers can keep a response for up to 15 seconds (Cache-Control: public, max-age=15, s-maxage=60, both counting down to when the cached copy expires).

  • Requests with the same URL share a cached response. On a page many people open, request the same URL for every visitor, for example ?limit=20, not one with a since of each visitor’s own.
  • Each response has an ETag. Send it back as If-None-Match to get 304 Not Modified with no body when nothing changed.

Invalid parameters return 400 with a JSON message, for example:

{ "error": "invalid limit parameter: must be a valid integer" }
MessageCause
invalid since parameter, invalid until parameter, invalid limit parameter, invalid offset parameterNot a whole number
invalid contentType parameterNot a content-type key
invalid matchId parameterNot a numeric match ID

5. Get content count Deprecated

Section titled “5. Get content count ”
GET https://q.loop-feed.com/v1/topic/{topicId}/count

Returns { "count": 42 }. The count isn’t a total: it’s the number of pinned items plus up to 100 of the newest items matching service, since and until. It will be removed in a future version. Use the content endpoint instead.


To pick up new items regularly, poll the content endpoint:

// Every few minutes: fetch what was published since the last run, with an hour's overlap
const since = Math.floor(lastRun / 1000) - 3600;
const response = await fetch(`https://q.loop-feed.com/v1/topic/${topicId}/content?since=${since}&limit=100`);
const items = await response.json();
for (const item of items) {
if (!seen.has(item.id)) {
seen.add(item.id);
save(item);
}
}
  • Overlap and de-duplicate. since uses the time a post was published. Some posts reach LoopFeed a little after they’re published, so overlap your windows and keep only IDs you haven’t seen.
  • Pinned items come every time. Treat them separately (pinned: true) or de-duplicate them by id.
  • Page with offset for more than 100 items, and stop when a page has fewer regular items than limit.
  • Cache responses for at least a minute.

When you show content from social platforms, follow their rules:

  • Link every item to its original post (url) and show its author.
  • Use the platforms’ official logos and names. See the guidelines for YouTube, X and Instagram.
  • Play YouTube videos with YouTube’s player (the embed media). Don’t download or re-host them.